diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2008-03-21 11:49:34 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2008-03-21 11:49:34 +0100 |
commit | 086ae52d12011746a75f5588e877347bc0457352 (patch) | |
tree | e73263c7a29d0f94fafb874562610e16eb292ba8 /includes | |
parent | 749e7fb2bae7bbda855de3c9e319435b9f698ff7 (diff) |
Update auf MediaWiki 1.12.0
Diffstat (limited to 'includes')
210 files changed, 22407 insertions, 6189 deletions
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 4fb76dcc..ffd3168a 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -73,64 +73,87 @@ function code2utf($num){ return ''; } +define( 'AJAX_SEARCH_VERSION', 2 ); //AJAX search cache version + function wfSajaxSearch( $term ) { - global $wgContLang, $wgOut; + global $wgContLang, $wgOut, $wgUser, $wgCapitalLinks, $wgMemc; $limit = 16; + $sk = $wgUser->getSkin(); + $output = ''; + + $term = trim( $term ); + $term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) ); + if ( $wgCapitalLinks ) + $term = $wgContLang->ucfirst( $term ); + $term_title = Title::newFromText( $term ); + + $memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) ); + $cached = $wgMemc->get($memckey); + if( is_array( $cached ) && $cached['version'] == AJAX_SEARCH_VERSION ) { + $response = new AjaxResponse( $cached['html'] ); + $response->setCacheDuration( 30*60 ); + return $response; + } - $l = new Linker; - - $term = str_replace( ' ', '_', $wgContLang->ucfirst( - $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) ) - ) ); - - if ( strlen( str_replace( '_', '', $term ) )<3 ) - return; - - $db = wfGetDB( DB_SLAVE ); - $res = $db->select( 'page', 'page_title', - array( 'page_namespace' => 0, - "page_title LIKE '". $db->strencode( $term) ."%'" ), - "wfSajaxSearch", - array( 'LIMIT' => $limit+1 ) - ); - - $r = ""; + $r = $more = ''; + $canSearch = true; + + $results = PrefixSearch::titleSearch( $term, $limit + 1 ); + foreach( array_slice( $results, 0, $limit ) as $titleText ) { + $r .= '<li>' . $sk->makeKnownLink( $titleText ) . "</li>\n"; + } + + // Hack to check for specials + if( $results ) { + $t = Title::newFromText( $results[0] ); + if( $t && $t->getNamespace() == NS_SPECIAL ) { + $canSearch = false; + if( count( $results ) > $limit ) { + $more = '<i>' . + $sk->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Specialpages' ), + wfMsgHtml( 'moredotdotdot' ) ) . + '</i>'; + } + } else { + if( count( $results ) > $limit ) { + $more = '<i>' . + $sk->makeKnownLinkObj( + SpecialPage::getTitleFor( "Allpages", $term ), + wfMsgHtml( 'moredotdotdot' ) ) . + '</i>'; + } + } + } - $i=0; - while ( ( $row = $db->fetchObject( $res ) ) && ( ++$i <= $limit ) ) { - $nt = Title::newFromDBkey( $row->page_title ); - $r .= '<li>' . $l->makeKnownLinkObj( $nt ) . "</li>\n"; + $valid = (bool) $term_title; + $term_url = urlencode( $term ); + $term_diplay = htmlspecialchars( $valid ? $term_title->getFullText() : $term ); + $subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' ); + $subtitle = wfMsgWikiHtml( $subtitlemsg, $term_diplay ); + $html = '<div id="searchTargetHide"><a onclick="Searching_Hide_Results();">' + . wfMsgHtml( 'hideresults' ) . '</a></div>' + . '<h1 class="firstHeading">'.wfMsgHtml('search') + . '</h1><div id="contentSub">'. $subtitle . '</div>'; + if( $canSearch ) { + $html .= '<ul><li>' + . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), + wfMsgHtml( 'searchcontaining', $term_diplay ), + "search={$term_url}&fulltext=Search" ) + . '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), + wfMsgHtml( 'searchnamed', $term_diplay ) , + "search={$term_url}&go=Go" ) + . "</li></ul>"; } - if ( $i > $limit ) { - $more = '<i>' . $l->makeKnownLink( $wgContLang->specialPage( "Allpages" ), - wfMsg('moredotdotdot'), - "namespace=0&from=" . wfUrlEncode ( $term ) ) . - '</i>'; - } else { - $more = ''; + if( $r ) { + $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_diplay ) . "</h2>" + . '<ul>' .$r .'</ul>' . $more; } - $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); - $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); #FIXME: parser is missing mTitle ! - - $term = urlencode( $term ); - $html = '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' - . wfMsg( 'hideresults' ) . '</a></div>' - . '<h1 class="firstHeading">'.wfMsg('search') - . '</h1><div id="contentSub">'. $subtitle . '</div><ul><li>' - . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsg( 'searchcontaining', $term ), - "search=$term&fulltext=Search" ) - . '</li><li>' . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsg( 'searchnamed', $term ) , - "search=$term&go=Go" ) - . "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>" - . '<ul>' .$r .'</ul>'.$more; + $wgMemc->set( $memckey, array( 'version' => AJAX_SEARCH_VERSION, 'html' => $html ), 30 * 60 ); $response = new AjaxResponse( $html ); - $response->setCacheDuration( 30*60 ); - return $response; } @@ -154,7 +177,7 @@ function wfAjaxWatch($pagename = "", $watch = "") { } $watch = 'w' === $watch; - $title = Title::newFromText($pagename); + $title = Title::newFromDBkey($pagename); if(!$title) { // Invalid title return '<err#>'; diff --git a/includes/Article.php b/includes/Article.php index 7ba55c54..0544db7d 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -37,23 +37,11 @@ class Article { /**@}}*/ /** - * Constants used by internal components to get rollback results - */ - const SUCCESS = 0; // Operation successful - const PERM_DENIED = 1; // Permission denied - const BLOCKED = 2; // User has been blocked - const READONLY = 3; // Wiki is in read-only mode - const BAD_TOKEN = 4; // Invalid token specified - const BAD_TITLE = 5; // $this is not a valid Article - const ALREADY_ROLLED = 6; // Someone else already rolled this back. $from and $summary will be set - const ONLY_AUTHOR = 7; // User is the only author of the page - - /** * Constructor and clear the article * @param $title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ - function __construct( &$title, $oldId = null ) { + function __construct( Title $title, $oldId = null ) { $this->mTitle =& $title; $this->mOldId = $oldId; $this->clear(); @@ -135,6 +123,7 @@ class Article { $this->mRevIdFetched = 0; $this->mRedirectUrl = false; $this->mLatest = false; + $this->mPreparedEdit = false; } /** @@ -622,8 +611,9 @@ class Article { */ function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; - global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser; + global $wgEnableParserCache, $wgStylePath, $wgParser; global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; + global $wgDefaultRobotPolicy; $sk = $wgUser->getSkin(); wfProfileIn( __METHOD__ ); @@ -645,6 +635,7 @@ class Article { $rcid = $wgRequest->getVal( 'rcid' ); $rdfrom = $wgRequest->getVal( 'rdfrom' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + $purge = $wgRequest->getVal( 'action' ) == 'purge'; $wgOut->setArticleFlag( true ); @@ -657,8 +648,7 @@ class Article { # Honour customised robot policies for this namespace $policy = $wgNamespaceRobotPolicies[$ns]; } else { - # Default to encourage indexing and following links - $policy = 'index,follow'; + $policy = $wgDefaultRobotPolicy; } $wgOut->setRobotPolicy( $policy ); @@ -668,7 +658,7 @@ class Article { if ( !is_null( $diff ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -780,11 +770,11 @@ class Article { $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); return; } else { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); // and we are allowed to see... } } @@ -862,12 +852,12 @@ class Article { # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page if( $ns == NS_USER_TALK && User::isIP( $this->mTitle->getText() ) ) { - $wgOut->addWikiText( wfMsg('anontalkpagetext') ); + $wgOut->addWikiMsg('anontalkpagetext'); } # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) { + if( !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) && $this->mTitle->exists() ) { $wgOut->addHTML( "<div class='patrollink'>" . wfMsgHtml( 'markaspatrolledlink', @@ -914,31 +904,29 @@ class Article { $o->tb_name, $rmvtxt); } - $wgOut->addWikitext(wfMsg('trackbackbox', $tbtext)); + $wgOut->addWikiMsg( 'trackbackbox', $tbtext ); } function deletetrackback() { global $wgUser, $wgRequest, $wgOut, $wgTitle; if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) { - $wgOut->addWikitext(wfMsg('sessionfailure')); + $wgOut->addWikiMsg( 'sessionfailure' ); return; } - if ((!$wgUser->isAllowed('delete'))) { - $wgOut->permissionRequired( 'delete' ); - return; - } + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if (wfReadOnly()) { - $wgOut->readOnlyPage(); + if (count($permission_errors)>0) + { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } $db = wfGetDB(DB_MASTER); $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); $wgTitle->invalidateCache(); - $wgOut->addWikiText(wfMsg('trackbackdeleteok')); + $wgOut->addWikiMsg('trackbackdeleteok'); } function render() { @@ -960,7 +948,7 @@ class Article { } } else { $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) ); - $action = $this->mTitle->escapeLocalURL( 'action=purge' ); + $action = htmlspecialchars( $_SERVER['REQUEST_URI'] ); $button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) ); $msg = str_replace( '$1', "<form method=\"post\" action=\"$action\">\n" . @@ -990,6 +978,15 @@ class Article { $update = SquidUpdate::newSimplePurge( $this->mTitle ); $update->doUpdate(); } + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + global $wgMessageCache; + if ( $this->getID() == 0 ) { + $text = false; + } else { + $text = $this->getContent(); + } + $wgMessageCache->replace( $this->mTitle->getDBkey(), $text ); + } $this->view(); } @@ -1200,10 +1197,11 @@ class Article { /** * @deprecated use Article::doEdit() */ - function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { + function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) { $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $isminor ? EDIT_MINOR : 0 ) | - ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ); + ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); # If this is a comment, add the summary as headline if ( $comment && $summary != "" ) { @@ -1322,7 +1320,7 @@ class Article { # Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); - $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT ); + $bot = $flags & EDIT_FORCE_BOT; $oldtext = $this->getContent(); $oldsize = strlen( $oldtext ); @@ -1331,7 +1329,8 @@ class Article { if ($flags & EDIT_AUTOSUMMARY && $summary == '') $summary = $this->getAutosummary( $oldtext, $text, $flags ); - $text = $this->preSaveTransform( $text ); + $editInfo = $this->prepareTextForEdit( $text ); + $text = $editInfo->pst; $newsize = strlen( $text ); $dbw = wfGetDB( DB_MASTER ); @@ -1347,8 +1346,10 @@ class Article { $lastRevision = 0; $revisionId = 0; + + $changed = ( strcmp( $text, $oldtext ) != 0 ); - if ( 0 != strcmp( $text, $oldtext ) ) { + if ( $changed ) { $this->mGoodAdjustment = (int)$this->isCountable( $text ) - (int)$this->isCountable( $oldtext ); $this->mTotalAdjustment = 0; @@ -1413,9 +1414,8 @@ class Article { # Invalidate cache of this article and all pages using this article # as a template. Partly deferred. Article::onArticleEdit( $this->mTitle ); - + # Update links tables, site stats, etc. - $changed = ( strcmp( $oldtext, $text ) != 0 ); $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } } else { @@ -1451,7 +1451,7 @@ class Article { $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, '', strlen( $text ), $revisionId ); # Mark as patrolled if the user can - if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { + if( ($GLOBALS['wgUseRCPatrol'] || $GLOBALS['wgUseNPPatrol']) && $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); PatrolLog::record( $rcid, true ); } @@ -1509,28 +1509,42 @@ class Article { } /** - * Mark this particular edit as patrolled + * Mark this particular edit/page as patrolled */ function markpatrolled() { - global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser; + global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser; $wgOut->setRobotPolicy( 'noindex,nofollow' ); - # Check RC patrol config. option - if( !$wgUseRCPatrol ) { + # Check patrol config options + + if ( !($wgUseNPPatrol || $wgUseRCPatrol)) { $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + return; + } + + # If we haven't been given an rc_id value, we can't do anything + $rcid = (int) $wgRequest->getVal('rcid'); + $rc = $rcid ? RecentChange::newFromId($rcid) : null; + if ( is_null ( $rc ) ) + { + $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - # Check permissions - if( !$wgUser->isAllowed( 'patrol' ) ) { - $wgOut->permissionRequired( 'patrol' ); + if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) { + // Only new pages can be patrolled if the general patrolling is off....??? + // @fixme -- is this necessary? Shouldn't we only bother controlling the + // front end here? + $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } + + # Check permissions + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser ); - # If we haven't been given an rc_id value, we can't do anything - $rcid = $wgRequest->getVal( 'rcid' ); - if( !$rcid ) { - $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + if (count($permission_errors)>0) + { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } @@ -1539,7 +1553,10 @@ class Article { return; } - $return = SpecialPage::getTitleFor( 'Recentchanges' ); + #It would be nice to see where the user had actually come from, but for now just guess + $returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges'; + $return = Title::makeTitle( NS_SPECIAL, $returnto ); + # If it's left up to us, check that the user is allowed to patrol this edit # If the user has the "autopatrol" right, then we'll assume there are no # other conditions stopping them doing so @@ -1552,7 +1569,7 @@ class Article { # The user made this edit, and can't patrol it # Tell them so, and then back off $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) ); + $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); $wgOut->returnToMain( false, $return ); return; } @@ -1565,7 +1582,7 @@ class Article { # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); + $wgOut->addWikiMsg( 'markedaspatrolledtext' ); $wgOut->returnToMain( false, $return ); } @@ -1590,9 +1607,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); - $text = wfMsg( 'addedwatchtext', $link ); - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() ); } $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); @@ -1637,9 +1652,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); - $text = wfMsg( 'removedwatchtext', $link ); - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() ); } $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); @@ -1690,7 +1703,7 @@ class Article { global $wgUser, $wgRestrictionTypes, $wgContLang; $id = $this->mTitle->getArticleID(); - if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) { + if( array() != $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ) || wfReadOnly() || $id == 0 ) { return false; } @@ -1726,7 +1739,7 @@ class Article { $expiry_description = ''; if ( $encodedExpiry != 'infinity' ) { - $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')'; } # Prepare a null revision to be added to the history @@ -1757,10 +1770,8 @@ class Article { $comment .= "$expiry_description"; if ( $cascade ) $comment .= "$cascade_description"; - - $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); - + + $rowsAffected = false; # Update restrictions table foreach( $limit as $action => $restrictions ) { if ($restrictions != '' ) { @@ -1768,11 +1779,22 @@ class Article { array( 'pr_page' => $id, 'pr_type' => $action , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0 , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); + if($dbw->affectedRows() != 0) + $rowsAffected = true; } else { $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 'pr_type' => $action ), __METHOD__ ); + if($dbw->affectedRows() != 0) + $rowsAffected = true; } } + if(!$rowsAffected) + // No change + return true; + + # Insert a null revision + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); # Update page record $dbw->update( 'page', @@ -1788,6 +1810,8 @@ class Article { # Update the protection log $log = new LogPage( 'protect' ); + + if( $protect ) { $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); @@ -1821,35 +1845,121 @@ class Article { } return implode( ':', $bits ); } + + /** + * Auto-generates a deletion reason + * @param bool &$hasHistory Whether the page has a history + */ + public function generateReason(&$hasHistory) + { + global $wgContLang; + $dbw = wfGetDB(DB_MASTER); + // Get the last revision + $rev = Revision::newFromTitle($this->mTitle); + if(is_null($rev)) + return false; + // Get the article's contents + $contents = $rev->getText(); + $blank = false; + // If the page is blank, use the text from the previous revision, + // which can only be blank if there's a move/import/protect dummy revision involved + if($contents == '') + { + $prev = $rev->getPrevious(); + if($prev) + { + $contents = $prev->getText(); + $blank = true; + } + } + + // Find out if there was only one contributor + // Only scan the last 20 revisions + $limit = 20; + $res = $dbw->select('revision', 'rev_user_text', array('rev_page' => $this->getID()), __METHOD__, + array('LIMIT' => $limit)); + if($res === false) + // This page has no revisions, which is very weird + return false; + if($res->numRows() > 1) + $hasHistory = true; + else + $hasHistory = false; + $row = $dbw->fetchObject($res); + $onlyAuthor = $row->rev_user_text; + // Try to find a second contributor + while( $row = $dbw->fetchObject($res) ) { + if($row->rev_user_text != $onlyAuthor) { + $onlyAuthor = false; + break; + } + } + $dbw->freeResult($res); + + // Generate the summary with a '$1' placeholder + if($blank) { + // The current revision is blank and the one before is also + // blank. It's just not our lucky day + $reason = wfMsgForContent('exbeforeblank', '$1'); + } else { + if($onlyAuthor) + $reason = wfMsgForContent('excontentauthor', '$1', $onlyAuthor); + else + $reason = wfMsgForContent('excontent', '$1'); + } + + // Replace newlines with spaces to prevent uglyness + $contents = preg_replace("/[\n\r]/", ' ', $contents); + // Calculate the maximum amount of chars to get + // Max content length = max comment length - length of the comment (excl. $1) - '...' + $maxLength = 255 - (strlen($reason) - 2) - 3; + $contents = $wgContLang->truncate($contents, $maxLength, '...'); + // Remove possible unfinished links + $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); + // Now replace the '$1' placeholder + $reason = str_replace( '$1', $contents, $reason ); + return $reason; + } + /* * UI entry point for page deletion */ function delete() { global $wgUser, $wgOut, $wgRequest; + $confirm = $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); - $reason = $wgRequest->getText( 'wpReason' ); + $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); + + $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); + $this->DeleteReason = $wgRequest->getText( 'wpReason' ); + + $reason = $this->DeleteReasonList; + + if ( $reason != 'other' && $this->DeleteReason != '') { + // Entry from drop down menu + additional comment + $reason .= ': ' . $this->DeleteReason; + } elseif ( $reason == 'other' ) { + $reason = $this->DeleteReason; + } # This code desperately needs to be totally rewritten - # Check permissions - if( $wgUser->isAllowed( 'delete' ) ) { - if( $wgUser->isBlocked( !$confirm ) ) { - $wgOut->blockedPage(); - return; - } - } else { - $wgOut->permissionRequired( 'delete' ); + # Read-only check... + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); return; } + + # Check permissions + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if( wfReadOnly() ) { - $wgOut->readOnlyPage(); + if (count($permission_errors)>0) { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } - $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); + $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) ); # Better double-check that it hasn't been deleted yet! $dbw = wfGetDB( DB_MASTER ); @@ -1860,6 +1970,15 @@ class Article { return; } + # Hack for big sites + $bigHistory = $this->isBigDeletion(); + if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", + array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); + return; + } + if( $confirm ) { $this->doDelete( $reason ); if( $wgRequest->getCheck( 'wpWatch' ) ) { @@ -1870,77 +1989,47 @@ class Article { return; } - # determine whether this page has earlier revisions - # and insert a warning if it does - $maxRevisions = 20; - $authors = $this->getLastNAuthors( $maxRevisions, $latest ); + // Generate deletion reason + $hasHistory = false; + if ( !$reason ) $reason = $this->generateReason($hasHistory); - if( count( $authors ) > 1 && !$confirm ) { + // If the page has a history, insert a warning + if( $hasHistory && !$confirm ) { $skin=$wgUser->getSkin(); $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' ); - } - - # If a single user is responsible for all revisions, find out who they are - if ( count( $authors ) == $maxRevisions ) { - // Query bailed out, too many revisions to find out if they're all the same - $authorOfAll = false; - } else { - $authorOfAll = reset( $authors ); - foreach ( $authors as $author ) { - if ( $authorOfAll != $author ) { - $authorOfAll = false; - break; - } - } - } - # Fetch article text - $rev = Revision::newFromTitle( $this->mTitle ); - - if( !is_null( $rev ) ) { - # if this is a mini-text, we can paste part of it into the deletion reason - $text = $rev->getText(); - - #if this is empty, an earlier revision may contain "useful" text - $blanked = false; - if( $text == '' ) { - $prev = $rev->getPrevious(); - if( $prev ) { - $text = $prev->getText(); - $blanked = true; - } - } - - $length = strlen( $text ); - - # this should not happen, since it is not possible to store an empty, new - # page. Let's insert a standard text in case it does, though - if( $length == 0 && $reason === '' ) { - $reason = wfMsgForContent( 'exblank' ); - } - - if( $reason === '' ) { - # comment field=255, let's grep the first 150 to have some user - # space left - global $wgContLang; - $text = $wgContLang->truncate( $text, 150, '...' ); - - # let's strip out newlines - $text = preg_replace( "/[\n\r]/", '', $text ); - - if( !$blanked ) { - if( $authorOfAll === false ) { - $reason = wfMsgForContent( 'excontent', $text ); - } else { - $reason = wfMsgForContent( 'excontentauthor', $text, $authorOfAll ); - } - } else { - $reason = wfMsgForContent( 'exbeforeblank', $text ); - } + if( $bigHistory ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", + array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); } } - + return $this->confirmDelete( '', $reason ); } + + /** + * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions + */ + function isBigDeletion() { + global $wgDeleteRevisionsLimit; + if( $wgDeleteRevisionsLimit ) { + $revCount = $this->estimateRevisionCount(); + return $revCount > $wgDeleteRevisionsLimit; + } + return false; + } + + /** + * @return int approximate revision count + */ + function estimateRevisionCount() { + $dbr = wfGetDB(); + // For an exact count... + //return $dbr->selectField( 'revision', 'COUNT(*)', + // array( 'rev_page' => $this->getId() ), __METHOD__ ); + return $dbr->estimateRowCount( 'revision', '*', + array( 'rev_page' => $this->getId() ), __METHOD__ ); + } /** * Get the last N authors @@ -1990,51 +2079,59 @@ class Article { /** * Output deletion confirmation dialog + * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only. + * @param $reason string Prefilled reason */ function confirmDelete( $par, $reason ) { - global $wgOut, $wgUser; + global $wgOut, $wgUser, $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; wfDebug( "Article::confirmDelete\n" ); - $sub = htmlspecialchars( $this->mTitle->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) ); + $wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addWikiText( wfMsg( 'confirmdeletetext' ) ); - - $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par ); - - $confirm = htmlspecialchars( wfMsg( 'deletepage' ) ); - $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) ); - $token = htmlspecialchars( $wgUser->editToken() ); - $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) ); - - $wgOut->addHTML( " -<form id='deleteconfirm' method='post' action=\"{$formaction}\"> - <table border='0'> - <tr> - <td align='right'> - <label for='wpReason'>{$delcom}:</label> - </td> - <td align='left'> - <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" /> - </td> - </tr> - <tr> - <td> </td> - <td>$watch</td> - </tr> - <tr> - <td> </td> - <td> - <input type='submit' name='wpConfirmB' id='wpConfirmB' value=\"{$confirm}\" tabindex=\"3\" /> - </td> - </tr> - </table> - <input type='hidden' name='wpEditToken' value=\"{$token}\" /> -</form>\n" ); - - $wgOut->returnToMain( false ); - + $wgOut->addWikiMsg( 'confirmdeletetext' ); + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', array(), wfMsg( 'delete-legend' ) ) . + Xml::openElement( 'table' ) . + "<tr id=\"wpDeleteReasonListRow\"> + <td align='$align'>" . + Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) . + "</td> + <td>" . + Xml::listDropDown( 'wpDeleteReasonList', + wfMsgForContent( 'deletereason-dropdown' ), + wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . + "</td> + </tr> + <tr id=\"wpDeleteReasonRow\"> + <td align='$align'>" . + Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . + "</td> + <td>" . + Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td>" . + Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td>" . + Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + Xml::closeElement( 'form' ); + + $wgOut->addHTML( $form ); $this->showLogExtract( $wgOut ); } @@ -2062,15 +2159,14 @@ class Article { if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { if ( $this->doDeleteArticle( $reason ) ) { - $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); + $deleted = $this->mTitle->getPrefixedText(); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; - $text = wfMsg( 'deletedtext', $deleted, $loglink ); + $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink ); $wgOut->returnToMain( false ); wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason)); } else { @@ -2180,50 +2276,76 @@ class Article { /** * Roll back the most recent consecutive set of edits to a page * from the same user; fails if there are no eligible edits to - * roll back to, e.g. user is the sole contributor + * roll back to, e.g. user is the sole contributor. This function + * performs permissions checks on $wgUser, then calls commitRollback() + * to do the dirty work * * @param string $fromP - Name of the user whose edits to rollback. * @param string $summary - Custom summary. Set to default summary if empty. * @param string $token - Rollback token. - * @param bool $bot - If true, mark all reverted edits as bot. + * @param bool $bot - If true, mark all reverted edits as bot. * - * @param array $resultDetails contains result-specific dict of additional values - * ALREADY_ROLLED : 'current' (rev) - * SUCCESS : 'summary' (str), 'current' (rev), 'target' (rev) + * @param array $resultDetails contains result-specific array of additional values + * 'alreadyrolled' : 'current' (rev) + * success : 'summary' (str), 'current' (rev), 'target' (rev) * - * @return self::SUCCESS on succes, self::* on failure + * @return array of errors, each error formatted as + * array(messagekey, param1, param2, ...). + * On success, the array is empty. This array can also be passed to + * OutputPage::showPermissionsErrorPage(). */ public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) { - global $wgUser, $wgUseRCPatrol; + global $wgUser; $resultDetails = null; - - if( $wgUser->isAllowed( 'rollback' ) ) { - if( $wgUser->isBlocked() ) { - return self::BLOCKED; - } - } else { - return self::PERM_DENIED; - } - - if ( wfReadOnly() ) { - return self::READONLY; - } + + # Check permissions + $errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ), + $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) ); if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) - return self::BAD_TOKEN; + $errors[] = array( 'sessionfailure' ); + if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) { + $errors[] = array( 'actionthrottledtext' ); + } + # If there were errors, bail out now + if(!empty($errors)) + return $errors; + + return $this->commitRollback($fromP, $summary, $bot, $resultDetails); + } + + /** + * Backend implementation of doRollback(), please refer there for parameter + * and return value documentation + * + * NOTE: This function does NOT check ANY permissions, it just commits the + * rollback to the DB Therefore, you should only call this function direct- + * ly if you want to use custom permissions checks. If you don't, use + * doRollback() instead. + */ + public function commitRollback($fromP, $summary, $bot, &$resultDetails) { + global $wgUseRCPatrol, $wgUser; $dbw = wfGetDB( DB_MASTER ); + if( wfReadOnly() ) { + return array( array( 'readonlytext' ) ); + } + # Get the last editor $current = Revision::newFromTitle( $this->mTitle ); if( is_null( $current ) ) { # Something wrong... no page? - return self::BAD_TITLE; + return array(array('notanarticle')); } $from = str_replace( '_', ' ', $fromP ); if( $from != $current->getUserText() ) { $resultDetails = array( 'current' => $current ); - return self::ALREADY_ROLLED; + return array(array('alreadyrolled', + htmlspecialchars($this->mTitle->getPrefixedText()), + htmlspecialchars($fromP), + htmlspecialchars($current->getUserText()) + )); } # Get the last edit not by this guy @@ -2231,21 +2353,19 @@ class Article { $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp' ), - array( - 'rev_page' => $current->getPage(), + array( 'rev_page' => $current->getPage(), "rev_user <> {$user} OR rev_user_text <> {$user_text}" ), __METHOD__, - array( - 'USE INDEX' => 'page_timestamp', + array( 'USE INDEX' => 'page_timestamp', 'ORDER BY' => 'rev_timestamp DESC' ) ); if( $s === false ) { - # Something wrong - return self::ONLY_AUTHOR; + # No one else ever edited this page + return array(array('cantrollback')); } $set = array(); - if ( $bot ) { + if ( $bot && $wgUser->isAllowed('markbotedits') ) { # Mark all reverted edits as bot $set['rc_bot'] = 1; } @@ -2264,23 +2384,36 @@ class Article { ); } - # Get the edit summary + # Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id ); if( empty( $summary ) ) - $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); + { + global $wgLang; + $summary = wfMsgForContent( 'revertpage', + $target->getUserText(), $from, + $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true), + $current->getId(), $wgLang->timeanddate($current->getTimestamp()) + ); + } # Save - $flags = EDIT_UPDATE | EDIT_MINOR; - if( $bot ) + $flags = EDIT_UPDATE; + + if ($wgUser->isAllowed('minoredit')) + $flags |= EDIT_MINOR; + + if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) $flags |= EDIT_FORCE_BOT; $this->doEdit( $target->getText(), $summary, $flags ); + wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) ); + $resultDetails = array( 'summary' => $summary, 'current' => $current, 'target' => $target, ); - return self::SUCCESS; + return array(); } /** @@ -2288,8 +2421,8 @@ class Article { */ function rollback() { global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; - $details = null; + $result = $this->doRollback( $wgRequest->getVal( 'from' ), $wgRequest->getText( 'summary' ), @@ -2298,58 +2431,44 @@ class Article { $details ); - switch( $result ) { - case self::BLOCKED: - $wgOut->blockedPage(); - break; - case self::PERM_DENIED: - $wgOut->permissionRequired( 'rollback' ); - break; - case self::READONLY: - $wgOut->readOnlyPage( $this->getContent() ); - break; - case self::BAD_TOKEN: - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); - break; - case self::BAD_TITLE: - $wgOut->addHtml( wfMsg( 'notanarticle' ) ); - break; - case self::ALREADY_ROLLED: - $current = $details['current']; - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( - wfMsg( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText() ), - htmlspecialchars( $wgRequest->getVal( 'from' ) ), - htmlspecialchars( $current->getUserText() ) - ) - ); - if( $current->getComment() != '' ) { - $wgOut->addHtml( wfMsg( 'editcomment', - $wgUser->getSkin()->formatComment( $current->getComment() ) ) ); + if( in_array( array( 'blocked' ), $result ) ) { + $wgOut->blockedPage(); + return; + } + if( in_array( array( 'actionthrottledtext' ), $result ) ) { + $wgOut->rateLimited(); + return; + } + # Display permissions errors before read-only message -- there's no + # point in misleading the user into thinking the inability to rollback + # is only temporary. + if( !empty($result) && $result !== array( array('readonlytext') ) ) { + # array_diff is completely broken for arrays of arrays, sigh. Re- + # move any 'readonlytext' error manually. + $out = array(); + foreach( $result as $error ) { + if( $error != array( 'readonlytext' ) ) { + $out []= $error; } - break; - case self::ONLY_AUTHOR: - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addHtml( wfMsg( 'cantrollback' ) ); - break; - case self::SUCCESS: - $current = $details['current']; - $target = $details['target']; - $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) - . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); - $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) - . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); - $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); - $wgOut->returnToMain( false, $this->mTitle ); - break; - default: - throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" ); + } + $wgOut->showPermissionsErrorPage( $out ); + return; + } + if( $result == array( array('readonlytext') ) ) { + $wgOut->readOnlyPage(); + return; } + $current = $details['current']; + $target = $details['target']; + $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) + . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); + $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) + . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); + $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); + $wgOut->returnToMain( false, $this->mTitle ); } @@ -2375,6 +2494,29 @@ class Article { } /** + * Prepare text which is about to be saved. + * Returns a stdclass with source, pst and output members + */ + function prepareTextForEdit( $text, $revid=null ) { + if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) { + // Already prepared + return $this->mPreparedEdit; + } + global $wgParser; + $edit = (object)array(); + $edit->revid = $revid; + $edit->newText = $text; + $edit->pst = $this->preSaveTransform( $text ); + $options = new ParserOptions; + $options->setTidy( true ); + $options->enableLimitReport(); + $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid ); + $edit->oldText = $this->getContent(); + $this->mPreparedEdit = $edit; + return $edit; + } + + /** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Every 100th edit, prune the recent changes table. @@ -2388,21 +2530,28 @@ class Article { * @param $changed Whether or not the content actually changed */ function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { - global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser; + global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache; wfProfileIn( __METHOD__ ); # Parse the text - $options = new ParserOptions; - $options->setTidy(true); - $poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid ); + # Be careful not to double-PST: $text is usually already PST-ed once + if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { + wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); + $editInfo = $this->prepareTextForEdit( $text, $newid ); + } else { + wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); + $editInfo = $this->mPreparedEdit; + } # Save it to the parser cache - $parserCache =& ParserCache::singleton(); - $parserCache->save( $poutput, $this, $wgUser ); + if ( $wgEnableParserCache ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $editInfo->output, $this, $wgUser ); + } # Update the links tables - $u = new LinksUpdate( $this->mTitle, $poutput ); + $u = new LinksUpdate( $this->mTitle, $editInfo->output ); $u->doUpdate(); if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { @@ -2756,6 +2905,7 @@ class Article { $title->touchLinks(); $title->purgeSquid(); + $title->deleteTitleProtection(); } static function onArticleDelete( $title ) { @@ -2773,6 +2923,10 @@ class Article { if( $title->getNamespace() == NS_MEDIAWIKI) { $wgMessageCache->replace( $title->getDBkey(), false ); } + if( $title->getNamespace() == NS_IMAGE ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } } /** @@ -2782,9 +2936,11 @@ class Article { global $wgDeferredUpdateList, $wgUseFileCache; // Invalidate caches of articles which include this page - $update = new HTMLCacheUpdate( $title, 'templatelinks' ); - $wgDeferredUpdateList[] = $update; + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); + // Invalidate the caches of all pages which redirect here + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); + # Purge squid for this page only $title->purgeSquid(); @@ -3009,14 +3165,16 @@ class Article { * @param bool $cache */ public function outputWikiText( $text, $cache = true ) { - global $wgParser, $wgUser, $wgOut; + global $wgParser, $wgUser, $wgOut, $wgEnableParserCache; $popts = $wgOut->parserOptions(); $popts->setTidy(true); + $popts->enableLimitReport(); $parserOutput = $wgParser->parse( $text, $this->mTitle, $popts, true, true, $this->getRevIdFetched() ); $popts->setTidy(false); - if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) { + $popts->enableLimitReport( false ); + if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache =& ParserCache::singleton(); $parserCache->save( $parserOutput, $this, $wgUser ); } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 87a79438..2ad137e2 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -28,10 +28,6 @@ * accounts authenticate externally, or use it only as a fallback; also * you can transparently create internal wiki accounts the first time * someone logs in who can be authenticated externally. - * - * This interface is new, and might change a bit before 1.4.0 final is - * done... - * */ class AuthPlugin { /** @@ -211,6 +207,18 @@ class AuthPlugin { } /** + * Check if a user should authenticate locally if the global authentication fails. + * If either this or strict() returns true, local authentication is not used. + * + * @param $username String: username. + * @return bool + * @public + */ + function strictUserAuth( $username ) { + return false; + } + + /** * When creating a user account, optionally fill in preferences and such. * For instance, you might pull the email address or real name from the * external user database. diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 5e1b8156..2e2083b2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -7,6 +7,8 @@ ini_set('unserialize_callback_func', '__autoload' ); function __autoload($className) { global $wgAutoloadClasses; + # Locations of core classes + # Extension classes are specified with $wgAutoloadClasses static $localClasses = array( # Includes 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', @@ -15,6 +17,7 @@ function __autoload($className) { 'AlphabeticPager' => 'includes/Pager.php', 'Article' => 'includes/Article.php', 'AuthPlugin' => 'includes/AuthPlugin.php', + 'Autopromote' => 'includes/Autopromote.php', 'BagOStuff' => 'includes/BagOStuff.php', 'HashBagOStuff' => 'includes/BagOStuff.php', 'SqlBagOStuff' => 'includes/BagOStuff.php', @@ -55,6 +58,8 @@ function __autoload($className) { 'Diff' => 'includes/DifferenceEngine.php', 'MappedDiff' => 'includes/DifferenceEngine.php', 'DiffFormatter' => 'includes/DifferenceEngine.php', + 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php', + 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php', 'DjVuImage' => 'includes/DjVuImage.php', '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php', 'WordLevelDiff' => 'includes/DifferenceEngine.php', @@ -88,7 +93,6 @@ function __autoload($className) { 'FileStore' => 'includes/FileStore.php', 'FSException' => 'includes/FileStore.php', 'FSTransaction' => 'includes/FileStore.php', - 'HTMLForm' => 'includes/HTMLForm.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', @@ -99,7 +103,6 @@ function __autoload($className) { 'ImageGallery' => 'includes/ImageGallery.php', 'ImagePage' => 'includes/ImagePage.php', 'ImageHistoryList' => 'includes/ImagePage.php', - 'ImageRemote' => 'includes/ImageRemote.php', 'FileDeleteForm' => 'includes/FileDeleteForm.php', 'FileRevertForm' => 'includes/FileRevertForm.php', 'Job' => 'includes/JobQueue.php', @@ -134,10 +137,23 @@ function __autoload($className) { 'ReverseChronologicalPager' => 'includes/Pager.php', 'TablePager' => 'includes/Pager.php', 'Parser' => 'includes/Parser.php', + 'Parser_OldPP' => 'includes/Parser_OldPP.php', + 'Parser_DiffTest' => 'includes/Parser_DiffTest.php', + 'ParserCache' => 'includes/ParserCache.php', 'ParserOutput' => 'includes/ParserOutput.php', 'ParserOptions' => 'includes/ParserOptions.php', - 'ParserCache' => 'includes/ParserCache.php', 'PatrolLog' => 'includes/PatrolLog.php', + 'Preprocessor' => 'includes/Preprocessor.php', + 'PrefixSearch' => 'includes/PrefixSearch.php', + 'PPFrame' => 'includes/Preprocessor.php', + 'PPNode' => 'includes/Preprocessor.php', + 'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php', + 'PPFrame_DOM' => 'includes/Preprocessor_DOM.php', + 'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php', + 'PPDStack' => 'includes/Preprocessor_DOM.php', + 'PPDStackElement' => 'includes/Preprocessor_DOM.php', + 'PPNode_DOM' => 'includes/Preprocessor_DOM.php', + 'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', 'Profiler' => 'includes/Profiler.php', @@ -207,6 +223,8 @@ function __autoload($className) { 'PopularPagesPage' => 'includes/SpecialPopularpages.php', 'PreferencesForm' => 'includes/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php', + 'RandomPage' => 'includes/SpecialRandompage.php', + 'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php', 'PasswordResetForm' => 'includes/SpecialResetpass.php', 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php', 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php', @@ -225,7 +243,7 @@ function __autoload($className) { 'UploadForm' => 'includes/SpecialUpload.php', 'UploadFormMogile' => 'includes/SpecialUploadMogile.php', 'LoginForm' => 'includes/SpecialUserlogin.php', - 'UserrightsForm' => 'includes/SpecialUserrights.php', + 'UserrightsPage' => 'includes/SpecialUserrights.php', 'SpecialVersion' => 'includes/SpecialVersion.php', 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php', 'WantedPagesPage' => 'includes/SpecialWantedpages.php', @@ -240,8 +258,10 @@ function __autoload($className) { 'StringUtils' => 'includes/StringUtils.php', 'Title' => 'includes/Title.php', 'User' => 'includes/User.php', + 'UserRightsProxy' => 'includes/UserRightsProxy.php', 'MailAddress' => 'includes/UserMailer.php', 'EmailNotification' => 'includes/UserMailer.php', + 'UserMailer' => 'includes/UserMailer.php', 'WatchedItem' => 'includes/WatchedItem.php', 'WebRequest' => 'includes/WebRequest.php', 'WebResponse' => 'includes/WebResponse.php', @@ -251,6 +271,7 @@ function __autoload($className) { 'WikiErrorMsg' => 'includes/WikiError.php', 'WikiXmlError' => 'includes/WikiError.php', 'Xml' => 'includes/Xml.php', + 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', 'ZhClient' => 'includes/ZhClient.php', 'memcached' => 'includes/memcached-client.php', 'EmaillingJob' => 'includes/JobQueue.php', @@ -290,10 +311,10 @@ function __autoload($className) { # Languages 'Language' => 'languages/Language.php', - 'RandomPage' => 'includes/SpecialRandompage.php', # API 'ApiBase' => 'includes/api/ApiBase.php', + 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', @@ -302,16 +323,22 @@ function __autoload($className) { 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', + 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', + 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', 'ApiHelp' => 'includes/api/ApiHelp.php', 'ApiLogin' => 'includes/api/ApiLogin.php', + 'ApiLogout' => 'includes/api/ApiLogout.php', 'ApiMain' => 'includes/api/ApiMain.php', 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', 'ApiPageSet' => 'includes/api/ApiPageSet.php', + 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', + 'ApiParse' => 'includes/api/ApiParse.php', 'ApiQuery' => 'includes/api/ApiQuery.php', 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', + 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', @@ -327,13 +354,29 @@ function __autoload($className) { 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', + 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', + 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', + 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', 'ApiResult' => 'includes/api/ApiResult.php', + + # apiedit branch + 'ApiBlock' => 'includes/api/ApiBlock.php', + #'ApiChangeRights' => 'includes/api/ApiChangeRights.php', + # Disabled for now + 'ApiDelete' => 'includes/api/ApiDelete.php', + 'ApiMove' => 'includes/api/ApiMove.php', + 'ApiProtect' => 'includes/api/ApiProtect.php', + 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', + 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', + 'ApiRollback' => 'includes/api/ApiRollback.php', + 'ApiUnblock' => 'includes/api/ApiUnblock.php', + 'ApiUndelete' => 'includes/api/ApiUndelete.php' ); wfProfileIn( __METHOD__ ); @@ -383,4 +426,4 @@ function wfLoadAllExtensions() { require( $file ); } } -}
\ No newline at end of file +} diff --git a/includes/Autopromote.php b/includes/Autopromote.php new file mode 100644 index 00000000..b5097423 --- /dev/null +++ b/includes/Autopromote.php @@ -0,0 +1,113 @@ +<?php + +/** + * This class checks if user can get extra rights + * because of conditions specified in $wgAutopromote + */ +class Autopromote { + /** + * Get the groups for the given user based on $wgAutopromote. + * + * @param User $user The user to get the groups for + * @return array Array of groups to promote to. + */ + public static function getAutopromoteGroups( User $user ) { + global $wgAutopromote; + $promote = array(); + foreach( $wgAutopromote as $group => $cond ) { + if( self::recCheckCondition( $cond, $user ) ) + $promote[] = $group; + } + return $promote; + } + + /** + * Recursively check a condition. Conditions are in the form + * array( '&' or '|' or '^', cond1, cond2, ... ) + * where cond1, cond2, ... are themselves conditions; *OR* + * APCOND_EMAILCONFIRMED, *OR* + * array( APCOND_EMAILCONFIRMED ), *OR* + * array( APCOND_EDITCOUNT, number of edits ), *OR* + * array( APCOND_AGE, seconds since registration ), *OR* + * similar constructs defined by extensions. + * This function evaluates the former type recursively, and passes off to + * self::checkCondition for evaluation of the latter type. + * + * @param mixed $cond A condition, possibly containing other conditions + * @param User $user The user to check the conditions against + * @return bool Whether the condition is true + */ + private static function recCheckCondition( $cond, User $user ) { + $validOps = array( '&', '|', '^' ); + if( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) { + # Recursive condition + if( $cond[0] == '&' ) { + foreach( array_slice( $cond, 1 ) as $subcond ) + if( !self::recCheckCondition( $subcond, $user ) ) + return false; + return true; + } elseif( $cond[0] == '|' ) { + foreach( array_slice( $cond, 1 ) as $subcond ) + if( self::recCheckCondition( $subcond, $user ) ) + return true; + return false; + } elseif( $cond[0] == '^' ) { + $res = null; + foreach( array_slice( $cond, 1 ) as $subcond ) { + if( is_null( $res ) ) + $res = self::recCheckCondition( $subcond, $user ); + else + $res = ($res xor self::recCheckCondition( $subcond, $user )); + } + return $res; + } + } + # If we got here, the array presumably does not contain other condi- + # tions; it's not recursive. Pass it off to self::checkCondition. + if( !is_array( $cond ) ) + $cond = array( $cond ); + return self::checkCondition( $cond, $user ); + } + + /** + * As recCheckCondition, but *not* recursive. The only valid conditions + * are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/ + * APCOND_AGE. Other types will throw an exception if no extension evalu- + * ates them. + * + * @param array $cond A condition, which must not contain other conditions + * @param User $user The user to check the condition against + * @return bool Whether the condition is true for the user + */ + private static function checkCondition( $cond, User $user ) { + if( count( $cond ) < 1 ) + return false; + switch( $cond[0] ) { + case APCOND_EMAILCONFIRMED: + if( User::isValidEmailAddr( $user->getEmail() ) ) { + global $wgEmailAuthentication; + if( $wgEmailAuthentication ) { + return $user->getEmailAuthenticationTimestamp() ? true : false; + } else { + return true; + } + } + return false; + case APCOND_EDITCOUNT: + return $user->getEditCount() >= $cond[1]; + case APCOND_AGE: + $age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); + return $age >= $cond[1]; + case APCOND_INGROUPS: + $groups = array_slice( $cond, 1 ); + return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups ); + default: + $result = null; + wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) ); + if( $result === null ) { + throw new MWException( "Unrecognized condition {$cond[0]} for autopromotion!" ); + } + return $result ? true : false; + } + } +} diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index a40d020e..226abb35 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -73,6 +73,11 @@ class BagOStuff { return true; } + function keys() { + /* stub */ + return array(); + } + /* *** Emulated functions *** */ /* Better performance can likely be got with custom written versions */ function get_multi($keys) { @@ -202,6 +207,10 @@ class HashBagOStuff extends BagOStuff { unset($this->bag[$key]); return true; } + + function keys() { + return array_keys( $this->bag ); + } } /* @@ -283,6 +292,19 @@ abstract class SqlBagOStuff extends BagOStuff { return true; /* ? */ } + function keys() { + $res = $this->_query( "SELECT keyname FROM $0" ); + if(!$res) { + $this->_debug("keys: ** error: " . $this->_dberror($res) . " **"); + return array(); + } + $result = array(); + while( $row = $this->_fetchobject($res) ) { + $result[] = $row->keyname; + } + return $result; + } + function getTableName() { return $this->table; } @@ -743,6 +765,19 @@ class DBABagOStuff extends BagOStuff { wfProfileOut( __METHOD__ ); return $ret; } + + function keys() { + $reader = $this->getReader(); + $k1 = dba_firstkey( $reader ); + if( !$k1 ) { + return array(); + } + $result[] = $k1; + while( $key = dba_nextkey( $reader ) ) { + $result[] = $key; + } + return $result; + } } diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 76a388a6..6fbcd3c1 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -37,6 +37,19 @@ class CategoryPage extends Article { } } + /** + * This page should not be cached if 'from' or 'until' has been used + * @return bool + */ + function isFileCacheable() { + global $wgRequest; + + return ( ! Article::isFileCacheable() + || $wgRequest->getVal( 'from' ) + || $wgRequest->getVal( 'until' ) + ) ? false : true; + } + function openShowCategory() { # For overloading } @@ -202,7 +215,7 @@ class CategoryViewer { array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ), array( $pageCondition, 'cl_from = page_id', - 'cl_to' => $this->title->getDBKey()), + 'cl_to' => $this->title->getDBkey()), #'page_is_redirect' => 0), #+ $pageCondition, __METHOD__, @@ -410,7 +423,7 @@ class CategoryViewer { * @private */ function pagingLinks( $title, $first, $last, $limit, $query = array() ) { - global $wgUser, $wgLang; + global $wgLang; $sk = $this->getSkin(); $limitText = $wgLang->formatNum( $limit ); diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 8d0f9508..507e88fa 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -58,7 +58,7 @@ class ChangesList { // Precache various messages if( !isset( $this->message ) ) { foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. - 'blocklink history boteditletter' ) as $msg ) { + 'blocklink history boteditletter semicolon-separator' ) as $msg ) { $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); } } @@ -176,13 +176,16 @@ class ChangesList { global $wgContLang; $articlelink .= $wgContLang->getDirMark(); + wfRunHooks('ChangesListInsertArticleLink', + array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched)); + $s .= ' '.$articlelink; } function insertTimestamp(&$s, $rc) { global $wgLang; # Timestamp - $s .= '; ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $s .= $this->message['semicolon-separator'] . ' ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } /** Insert links to user page, user talk page and eventually a blocking link */ @@ -453,7 +456,7 @@ class EnhancedChangesList extends ChangesList { array_push( $users, $text ); } - $users = ' <span class="changedby">['.implode('; ',$users).']</span>'; + $users = ' <span class="changedby">[' . implode( $this->message['semicolon-separator'] . ' ', $users ) . ']</span>'; # Arrow $rci = 'RCI'.$this->rcCacheIndex; @@ -546,7 +549,7 @@ class EnhancedChangesList extends ChangesList { $r .= $link; $r .= ' ('; $r .= $rcObj->curlink; - $r .= '; '; + $r .= $this->message['semicolon-separator'] . ' '; $r .= $rcObj->lastlink; $r .= ') . . '; @@ -651,7 +654,7 @@ class EnhancedChangesList extends ChangesList { $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); # Diff - $r .= ' ('. $rcObj->difflink .'; '; + $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' '; # Hist $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . '; @@ -704,4 +707,3 @@ class EnhancedChangesList extends ChangesList { } } - diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php index a5f45016..61dbafe5 100644 --- a/includes/CoreParserFunctions.php +++ b/includes/CoreParserFunctions.php @@ -51,12 +51,20 @@ class CoreParserFunctions { static function lc( $parser, $s = '' ) { global $wgContLang; - return $wgContLang->lc( $s ); + if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) { + return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) ); + } else { + return $wgContLang->lc( $s ); + } } static function uc( $parser, $s = '' ) { global $wgContLang; - return $wgContLang->uc( $s ); + if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) { + return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) ); + } else { + return $wgContLang->uc( $s ); + } } static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); } @@ -92,9 +100,10 @@ class CoreParserFunctions { return $parser->getFunctionLang()->convertGrammar( $word, $case ); } - static function plural( $parser, $text = '', $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null ) { + static function plural( $parser, $text = '') { + $forms = array_slice( func_get_args(), 2); $text = $parser->getFunctionLang()->parseFormattedNumber( $text ); - return $parser->getFunctionLang()->convertPlural( $text, $arg0, $arg1, $arg2, $arg3, $arg4 ); + return $parser->getFunctionLang()->convertPlural( $text, $forms ); } /** @@ -190,12 +199,70 @@ class CoreParserFunctions { return wfMsgForContent( 'nosuchspecialpage' ); } } - + public static function defaultsort( $parser, $text ) { $text = trim( $text ); if( strlen( $text ) > 0 ) $parser->setDefaultSort( $text ); return ''; } + + public static function filepath( $parser, $name='', $option='' ) { + $file = wfFindFile( $name ); + if( $file ) { + $url = $file->getFullUrl(); + if( $option == 'nowiki' ) { + return "<nowiki>$url</nowiki>"; + } + return $url; + } else { + return ''; + } + } + + /** + * Parser function to extension tag adaptor + */ + public static function tagObj( $parser, $frame, $args ) { + $xpath = false; + if ( !count( $args ) ) { + return ''; + } + $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) ); + + if ( count( $args ) ) { + $inner = $frame->expand( array_shift( $args ) ); + } else { + $inner = null; + } + + $stripList = $parser->getStripList(); + if ( !in_array( $tagName, $stripList ) ) { + return '<span class="error">' . + wfMsg( 'unknown_extension_tag', $tagName ) . + '</span>'; + } + + $attributes = array(); + foreach ( $args as $arg ) { + $bits = $arg->splitArg(); + if ( strval( $bits['index'] ) === '' ) { + $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ); + $value = trim( $frame->expand( $bits['value'] ) ); + if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) { + $value = isset( $m[1] ) ? $m[1] : ''; + } + $attributes[$name] = $value; + } + } + + $params = array( + 'name' => $tagName, + 'inner' => $inner, + 'attributes' => $attributes, + 'close' => "</$tagName>", + ); + return $parser->extensionSubstitution( $params, $frame ); + } } diff --git a/includes/Database.php b/includes/Database.php index 4f8c7d5e..f8738288 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -36,6 +36,22 @@ class DBObject { }; /** + * Utility class + * @addtogroup Database + * + * This allows us to distinguish a blob from a normal string and an array of strings + */ +class Blob { + private $mData; + function __construct($data) { + $this->mData = $data; + } + function fetch() { + return $this->mData; + } +}; + +/** * Utility class. * @addtogroup Database */ @@ -729,8 +745,8 @@ class Database { global $wgUser; if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) { $userName = $wgUser->getName(); - if ( strlen( $userName ) > 15 ) { - $userName = substr( $userName, 0, 15 ) . '...'; + if ( mb_strlen( $userName ) > 15 ) { + $userName = mb_substr( $userName, 0, 15 ) . '...'; } $userName = str_replace( '/', '', $userName ); } else { @@ -743,9 +759,13 @@ class Database { # If DBO_TRX is set, start a transaction if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && - $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' - ) { - $this->begin(); + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') { + // avoid establishing transactions for SHOW and SET statements too - + // that would delay transaction initializations to once connection + // is really used by application + $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm) + if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0) + $this->begin(); } if ( $this->debug() ) { @@ -1548,7 +1568,15 @@ class Database { } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { $list .= "$value"; } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { - $list .= $field." IN (".$this->makeList($value).") "; + if( count( $value ) == 0 ) { + // Empty input... or should this throw an error? + $list .= '0'; + } elseif( count( $value ) == 1 ) { + // Special-case single values, as IN isn't terribly efficient + $list .= $field." = ".$this->addQuotes( $value[0] ); + } else { + $list .= $field." IN (".$this->makeList($value).") "; + } } elseif( is_null($value) ) { if ( $mode == LIST_AND || $mode == LIST_OR ) { $list .= "$field IS "; @@ -2011,10 +2039,11 @@ class Database { } /** - * Rollback a transaction + * Rollback a transaction. + * No-op on non-transactional databases. */ function rollback( $fname = 'Database::rollback' ) { - $this->query( 'ROLLBACK', $fname ); + $this->query( 'ROLLBACK', $fname, true ); $this->mTrxLevel = 0; } @@ -2286,6 +2315,13 @@ class Database { return $this->tableName( $matches[1] ); } + /* + * Build a concatenation list to feed into a SQL query + */ + function buildConcat( $stringList ) { + return 'CONCAT(' . implode( ',', $stringList ) . ')'; + } + } /** diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php index 32c061a0..01213715 100644 --- a/includes/DatabasePostgres.php +++ b/includes/DatabasePostgres.php @@ -16,7 +16,13 @@ class PostgresField { global $wgDBmwschema; $q = <<<END -SELECT typname, attnotnull, attlen +SELECT +CASE WHEN typname = 'int2' THEN 'smallint' +WHEN typname = 'int4' THEN 'integer' +WHEN typname = 'int8' THEN 'bigint' +WHEN typname = 'bpchar' THEN 'char' +ELSE typname END AS typname, +attnotnull, attlen FROM pg_class, pg_namespace, pg_attribute, pg_type WHERE relnamespace=pg_namespace.oid AND relkind='r' @@ -112,6 +118,12 @@ class DatabasePostgres extends Database { return true; } + function hasConstraint( $name ) { + global $wgDBmwschema; + $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'"; + return $this->numRows($res = $this->doQuery($SQL)); + } + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) { return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags ); @@ -135,7 +147,7 @@ class DatabasePostgres extends Database { $this->close(); $this->mServer = $server; - $port = $wgDBport; + $this->mPort = $port = $wgDBport; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; @@ -148,7 +160,6 @@ class DatabasePostgres extends Database { $hstring .= "port=$port "; } - error_reporting( E_ALL ); @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password"); @@ -160,87 +171,117 @@ class DatabasePostgres extends Database { } $this->mOpened = true; - ## If this is the initial connection, setup the schema stuff and possibly create the user - ## TODO: Move this out of open() - if (defined('MEDIAWIKI_INSTALL')) { - global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, - $wgDBts2schema; - - print "<li>Checking the version of Postgres..."; - $version = $this->getServerVersion(); - $PGMINVER = "8.1"; - if ($this->numeric_version < $PGMINVER) { - print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n"; - dieout("</ul>"); + + global $wgCommandLineMode; + ## If called from the command-line (e.g. importDump), only show errors + if ($wgCommandLineMode) { + $this->doQuery("SET client_min_messages = 'ERROR'"); + } + + global $wgDBmwschema, $wgDBts2schema; + if (isset( $wgDBmwschema ) && isset( $wgDBts2schema ) + && $wgDBmwschema !== 'mediawiki' + && preg_match( '/^\w+$/', $wgDBmwschema ) + && preg_match( '/^\w+$/', $wgDBts2schema ) + ) { + $safeschema = $this->quote_ident($wgDBmwschema); + $safeschema2 = $this->quote_ident($wgDBts2schema); + $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public"); + } + + return $this->mConn; + } + + + function initial_setup($password, $dbName) { + // If this is the initial connection, setup the schema stuff and possibly create the user + global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema; + + print "<li>Checking the version of Postgres..."; + $version = $this->getServerVersion(); + $PGMINVER = '8.1'; + if ($this->numeric_version < $PGMINVER) { + print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n"; + dieout("</ul>"); + } + print "version $this->numeric_version is OK.</li>\n"; + + $safeuser = $this->quote_ident($wgDBuser); + // Are we connecting as a superuser for the first time? + if ($wgDBsuperuser) { + // Are we really a superuser? Check out our rights + $SQL = "SELECT + CASE WHEN usesuper IS TRUE THEN + CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END + ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END + END AS rights + FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser); + $rows = $this->numRows($res = $this->doQuery($SQL)); + if (!$rows) { + print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n"; + dieout('</ul>'); } - print "version $this->numeric_version is OK.</li>\n"; - - $safeuser = $this->quote_ident($wgDBuser); - ## Are we connecting as a superuser for the first time? - if ($wgDBsuperuser) { - ## Are we really a superuser? Check out our rights - $SQL = "SELECT - CASE WHEN usesuper IS TRUE THEN - CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END - ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END - END AS rights - FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser); - $rows = $this->numRows($res = $this->doQuery($SQL)); - if (!$rows) { - print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n"; + $perms = pg_fetch_result($res, 0, 0); + + $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser); + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows) { + print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>"; + } + else { + if ($perms != 1 and $perms != 3) { + print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. "; + print 'Please use a different Postgres user.</li>'; dieout('</ul>'); } - $perms = pg_fetch_result($res, 0, 0); - - $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser); + print "<li>Creating user <b>$wgDBuser</b>..."; + $safepass = $this->addQuotes($wgDBpassword); + $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass"; + $this->doQuery($SQL); + print "OK</li>\n"; + } + // User now exists, check out the database + if ($dbName != $wgDBname) { + $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname); $rows = $this->numRows($this->doQuery($SQL)); if ($rows) { - print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>"; + print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>"; } else { - if ($perms != 1 and $perms != 3) { - print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. "; + if ($perms < 2) { + print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. "; print 'Please use a different Postgres user.</li>'; dieout('</ul>'); } - print "<li>Creating user <b>$wgDBuser</b>..."; - $safepass = $this->addQuotes($wgDBpassword); - $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass"; + print "<li>Creating database <b>$wgDBname</b>..."; + $safename = $this->quote_ident($wgDBname); + $SQL = "CREATE DATABASE $safename OWNER $safeuser "; $this->doQuery($SQL); print "OK</li>\n"; + // Hopefully tsearch2 and plpgsql are in template1... } - ## User now exists, check out the database - if ($dbName != $wgDBname) { - $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname); - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows) { - print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>"; - } - else { - if ($perms < 2) { - print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. "; - print 'Please use a different Postgres user.</li>'; - dieout('</ul>'); - } - print "<li>Creating database <b>$wgDBname</b>..."; - $safename = $this->quote_ident($wgDBname); - $SQL = "CREATE DATABASE $safename OWNER $safeuser "; - $this->doQuery($SQL); - print "OK</li>\n"; - ## Hopefully tsearch2 and plpgsql are in template1... - } - ## Reconnect to check out tsearch2 rights for this user - print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights..."; - @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$user password=$password"); - if ( $this->mConn == false ) { - print "<b>FAILED TO CONNECT!</b></li>"; - dieout("</ul>"); - } - print "OK</li>\n"; + // Reconnect to check out tsearch2 rights for this user + print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights..."; + + $hstring=""; + if ($this->mServer!=false && $this->mServer!="") { + $hstring="host=$this->mServer "; + } + if ($this->mPort!=false && $this->mPort!="") { + $hstring .= "port=$this->mPort "; } - ## Tsearch2 checks + @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$wgDBsuperuser password=$password"); + if ( $this->mConn == false ) { + print "<b>FAILED TO CONNECT!</b></li>"; + dieout("</ul>"); + } + print "OK</li>\n"; + } + + if ($this->numeric_version < 8.3) { + // Tsearch2 checks print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"..."; if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) { print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\"."; @@ -255,176 +296,159 @@ class DatabasePostgres extends Database { $this->doQuery($SQL); } print "OK</li>\n"; + } - - ## Setup the schema for this user if needed - $result = $this->schemaExists($wgDBmwschema); - $safeschema = $this->quote_ident($wgDBmwschema); + // Setup the schema for this user if needed + $result = $this->schemaExists($wgDBmwschema); + $safeschema = $this->quote_ident($wgDBmwschema); + if (!$result) { + print "<li>Creating schema <b>$wgDBmwschema</b> ..."; + $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser"); if (!$result) { - print "<li>Creating schema <b>$wgDBmwschema</b> ..."; - $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser"); - if (!$result) { - print "<b>FAILED</b>.</li>\n"; - dieout("</ul>"); - } - print "OK</li>\n"; + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); } - else { - print "<li>Schema already exists, explicitly granting rights...\n"; - $safeschema2 = $this->addQuotes($wgDBmwschema); - $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n". - "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n". - "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n". - "AND p.relkind IN ('r','S','v')\n"; - $SQL .= "UNION\n"; - $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n". - "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n". - "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n". - "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2"; + print "OK</li>\n"; + } + else { + print "<li>Schema already exists, explicitly granting rights...\n"; + $safeschema2 = $this->addQuotes($wgDBmwschema); + $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n". + "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n". + "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n". + "AND p.relkind IN ('r','S','v')\n"; + $SQL .= "UNION\n"; + $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n". + "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n". + "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n". + "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b>. Could not set rights for the user.</li>\n"; + dieout("</ul>"); + } + $this->doQuery("SET search_path = $safeschema"); + $rows = $this->numRows($res); + while ($rows) { + $rows--; + $this->doQuery(pg_fetch_result($res, $rows, 0)); + } + print "OK</li>"; + } + + // Install plpgsql if needed + $this->setup_plpgsql(); + + $wgDBsuperuser = ''; + return true; // Reconnect as regular user + + } // end superuser + + if (!defined('POSTGRES_SEARCHPATH')) { + + if ($this->numeric_version < 8.3) { + // Do we have the basic tsearch2 table? + print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"..."; + if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) { + print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href="; + print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>"; + print " for instructions.</li>\n"; + dieout("</ul>"); + } + print "OK</li>\n"; + + // Does this user have the rights to the tsearch2 tables? + $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0); + print "<li>Checking tsearch2 permissions..."; + // Let's check all four, just to be safe + error_reporting( 0 ); + $ts2tables = array('cfg','cfgmap','dict','parser'); + $safetsschema = $this->quote_ident($wgDBts2schema); + foreach ( $ts2tables AS $tname ) { + $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname"; $res = $this->doQuery($SQL); if (!$res) { - print "<b>FAILED</b>. Could not set rights for the user.</li>\n"; + print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ". + "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n"; dieout("</ul>"); } - $this->doQuery("SET search_path = $safeschema"); - $rows = $this->numRows($res); - while ($rows) { - $rows--; - $this->doQuery(pg_fetch_result($res, $rows, 0)); - } - print "OK</li>"; } - - $wgDBsuperuser = ''; - return true; ## Reconnect as regular user - - } ## end superuser - - if (!defined('POSTGRES_SEARCHPATH')) { - - ## Do we have the basic tsearch2 table? - print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"..."; - if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) { - print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href="; - print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>"; - print " for instructions.</li>\n"; - dieout("</ul>"); - } - print "OK</li>\n"; - - ## Does this user have the rights to the tsearch2 tables? - $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0); - print "<li>Checking tsearch2 permissions..."; - ## Let's check all four, just to be safe - error_reporting( 0 ); - $ts2tables = array('cfg','cfgmap','dict','parser'); - foreach ( $ts2tables AS $tname ) { - $SQL = "SELECT count(*) FROM $wgDBts2schema.pg_ts_$tname"; + $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'"; + $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END"; $res = $this->doQuery($SQL); + error_reporting( E_ALL ); if (!$res) { - print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ". - "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n"; + print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n"; dieout("</ul>"); } - } - $SQL = "SELECT ts_name FROM $wgDBts2schema.pg_ts_cfg WHERE locale = '$ctype'"; - $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END"; - $res = $this->doQuery($SQL); - error_reporting( E_ALL ); - if (!$res) { - print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n"; - dieout("</ul>"); - } - print "OK</li>"; + print "OK</li>"; - ## Will the current locale work? Can we force it to? - print "<li>Verifying tsearch2 locale with $ctype..."; - $rows = $this->numRows($res); - $resetlocale = 0; - if (!$rows) { - print "<b>not found</b></li>\n"; - print "<li>Attempting to set default tsearch2 locale to \"$ctype\"..."; - $resetlocale = 1; - } - else { - $tsname = pg_fetch_result($res, 0, 0); - if ($tsname != 'default') { - print "<b>not set to default ($tsname)</b>"; - print "<li>Attempting to change tsearch2 default locale to \"$ctype\"..."; + // Will the current locale work? Can we force it to? + print "<li>Verifying tsearch2 locale with $ctype..."; + $rows = $this->numRows($res); + $resetlocale = 0; + if (!$rows) { + print "<b>not found</b></li>\n"; + print "<li>Attempting to set default tsearch2 locale to \"$ctype\"..."; $resetlocale = 1; } - } - if ($resetlocale) { - $SQL = "UPDATE $wgDBts2schema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'"; - $res = $this->doQuery($SQL); - if (!$res) { - print "<b>FAILED</b>. "; - print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n"; - dieout("</ul>"); + else { + $tsname = pg_fetch_result($res, 0, 0); + if ($tsname != 'default') { + print "<b>not set to default ($tsname)</b>"; + print "<li>Attempting to change tsearch2 default locale to \"$ctype\"..."; + $resetlocale = 1; + } } - print "OK</li>"; - } - - ## Final test: try out a simple tsearch2 query - $SQL = "SELECT $wgDBts2schema.to_tsvector('default','MediaWiki tsearch2 testing')"; - $res = $this->doQuery($SQL); - if (!$res) { - print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>"; - dieout("</ul>"); - } - print "OK</li>"; - - ## Do we have plpgsql installed? - print "<li>Checking for Pl/Pgsql ..."; - $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'"; - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows < 1) { - // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it - print "not installed. Attempting to install Pl/Pgsql ..."; - $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ". - "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'"; - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows >= 1) { - $olde = error_reporting(0); - error_reporting($olde - E_WARNING); - $result = $this->doQuery("CREATE LANGUAGE plpgsql"); - error_reporting($olde); - if (!$result) { - print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + if ($resetlocale) { + $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b>. "; + print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n"; dieout("</ul>"); } + print "OK</li>"; } - else { - print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + + // Final test: try out a simple tsearch2 query + $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>"; dieout("</ul>"); } + print "OK</li>"; } - print "OK</li>\n"; + + // Install plpgsql if needed + $this->setup_plpgsql(); - ## Does the schema already exist? Who owns it? + // Does the schema already exist? Who owns it? $result = $this->schemaExists($wgDBmwschema); if (!$result) { print "<li>Creating schema <b>$wgDBmwschema</b> ..."; error_reporting( 0 ); - $result = $this->doQuery("CREATE SCHEMA $wgDBmwschema"); + $safeschema = $this->quote_ident($wgDBmwschema); + $result = $this->doQuery("CREATE SCHEMA $safeschema"); error_reporting( E_ALL ); if (!$result) { print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ". - "You can try making them the owner of the database, or try creating the schema with a ". - "different user, and then grant access to the \"$wgDBuser\" user.</li>\n"; + "You can try making them the owner of the database, or try creating the schema with a ". + "different user, and then grant access to the \"$wgDBuser\" user.</li>\n"; dieout("</ul>"); } print "OK</li>\n"; } - else if ($result != $user) { - print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$user\". Not ideal.</li>\n"; + else if ($result != $wgDBuser) { + print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.</li>\n"; } else { - print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$user\". Excellent.</li>\n"; + print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.</li>\n"; } - - ## Always return GMT time to accomodate the existing integer-based timestamp assumption - print "<li>Setting the timezone to GMT for user \"$user\" ..."; + + // Always return GMT time to accomodate the existing integer-based timestamp assumption + print "<li>Setting the timezone to GMT for user \"$wgDBuser\" ..."; $SQL = "ALTER USER $safeuser SET timezone = 'GMT'"; $result = pg_query($this->mConn, $SQL); if (!$result) { @@ -432,7 +456,7 @@ class DatabasePostgres extends Database { dieout("</ul>"); } print "OK</li>\n"; - ## Set for the rest of this session + // Set for the rest of this session $SQL = "SET timezone = 'GMT'"; $result = pg_query($this->mConn, $SQL); if (!$result) { @@ -440,7 +464,7 @@ class DatabasePostgres extends Database { dieout("</ul>"); } - print "<li>Setting the datestyle to ISO, YMD for user \"$user\" ..."; + print "<li>Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ..."; $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'"; $result = pg_query($this->mConn, $SQL); if (!$result) { @@ -448,16 +472,16 @@ class DatabasePostgres extends Database { dieout("</ul>"); } print "OK</li>\n"; - ## Set for the rest of this session + // Set for the rest of this session $SQL = "SET datestyle = 'ISO, YMD'"; $result = pg_query($this->mConn, $SQL); if (!$result) { print "<li>Failed to set datestyle</li>\n"; dieout("</ul>"); } - - ## Fix up the search paths if needed - print "<li>Setting the search path for user \"$user\" ..."; + + // Fix up the search paths if needed + print "<li>Setting the search path for user \"$wgDBuser\" ..."; $path = $this->quote_ident($wgDBmwschema); if ($wgDBts2schema !== $wgDBmwschema) $path .= ", ". $this->quote_ident($wgDBts2schema); @@ -470,7 +494,7 @@ class DatabasePostgres extends Database { dieout("</ul>"); } print "OK</li>\n"; - ## Set for the rest of this session + // Set for the rest of this session $SQL = "SET search_path = $path"; $result = pg_query($this->mConn, $SQL); if (!$result) { @@ -478,17 +502,39 @@ class DatabasePostgres extends Database { dieout("</ul>"); } define( "POSTGRES_SEARCHPATH", $path ); - }} - - global $wgCommandLineMode; - ## If called from the command-line (e.g. importDump), only show errors - if ($wgCommandLineMode) { - $this->doQuery("SET client_min_messages = 'ERROR'"); } + } - return $this->mConn; + + function setup_plpgsql() { + print "<li>Checking for Pl/Pgsql ..."; + $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'"; + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows < 1) { + // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it + print "not installed. Attempting to install Pl/Pgsql ..."; + $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ". + "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'"; + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows >= 1) { + $olde = error_reporting(0); + error_reporting($olde - E_WARNING); + $result = $this->doQuery("CREATE LANGUAGE plpgsql"); + error_reporting($olde); + if (!$result) { + print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + dieout("</ul>"); + } + } + else { + print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + dieout("</ul>"); + } + } + print "OK</li>\n"; } + /** * Closes a database connection, if it is open * Returns success, true if already closed @@ -503,6 +549,9 @@ class DatabasePostgres extends Database { } function doQuery( $sql ) { + if (function_exists('mb_convert_encoding')) { + return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') ); + } return $this->mLastResult=pg_query( $this->mConn , $sql); } @@ -760,6 +809,18 @@ class DatabasePostgres extends Database { } /** + * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. + */ + function currentSequenceValue( $seqName ) { + $safeseq = preg_replace( "/'/", "''", $seqName ); + $res = $this->query( "SELECT currval('$safeseq')" ); + $row = $this->fetchRow( $res ); + $currval = $row[0]; + $this->freeResult( $res ); + return $currval; + } + + /** * Postgres does not have a "USE INDEX" clause, so return an empty string */ function useIndexClause( $index ) { @@ -897,9 +958,9 @@ class DatabasePostgres extends Database { function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - # Ignore errors during error handling to avoid infinite recursion + // Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); - ++$this->mErrorCount; + $this->mErrorCount++; if ($ignore || $tempIgnore) { wfDebug("SQL ERROR (ignored): $error\n"); @@ -917,7 +978,7 @@ class DatabasePostgres extends Database { /** * @return string wikitext of a link to the server software's web site */ - function getSoftwareLink() { + function getSoftwareLink() { return "[http://www.postgresql.org/ PostgreSQL]"; } @@ -1074,13 +1135,14 @@ END; function setup_database() { global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; - ## Make sure that we can write to the correct schema - ## If not, Postgres will happily and silently go to the next search_path item - $ctest = "mw_test_table"; + // Make sure that we can write to the correct schema + // If not, Postgres will happily and silently go to the next search_path item + $ctest = "mediawiki_test_table"; + $safeschema = $this->quote_ident($wgDBmwschema); if ($this->tableExists($ctest, $wgDBmwschema)) { - $this->doQuery("DROP TABLE $wgDBmwschema.$ctest"); + $this->doQuery("DROP TABLE $safeschema.$ctest"); } - $SQL = "CREATE TABLE $wgDBmwschema.$ctest(a int)"; + $SQL = "CREATE TABLE $safeschema.$ctest(a int)"; $olde = error_reporting( 0 ); $res = $this->doQuery($SQL); error_reporting( $olde ); @@ -1088,19 +1150,9 @@ END; print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n"; dieout("</ul>"); } - $this->doQuery("DROP TABLE $wgDBmwschema.mw_test_table"); + $this->doQuery("DROP TABLE $safeschema.$ctest"); - dbsource( "../maintenance/postgres/tables.sql", $this); - - ## Version-specific stuff - if ($this->numeric_version == 8.1) { - $this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gist(textvector)"); - $this->doQuery("CREATE INDEX ts2_page_title ON page USING gist(titlevector)"); - } - else { - $this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector)"); - $this->doQuery("CREATE INDEX ts2_page_title ON page USING gin(titlevector)"); - } + $res = dbsource( "../maintenance/postgres/tables.sql", $this); ## Update version information $mwv = $this->addQuotes($wgVersion); @@ -1139,9 +1191,13 @@ END; } function encodeBlob( $b ) { - return pg_escape_bytea( $b ); + return new Blob ( pg_escape_bytea( $b ) ) ; } + function decodeBlob( $b ) { + if ($b instanceof Blob) { + $b = $b->fetch(); + } return pg_unescape_bytea( $b ); } @@ -1152,11 +1208,10 @@ END; function addQuotes( $s ) { if ( is_null( $s ) ) { return 'NULL'; - } else if (is_array( $s )) { ## Assume it is bytea data - return "E'$s[1]'"; + } else if ($s instanceof Blob) { + return "'".$s->fetch($s)."'"; } return "'" . pg_escape_string($s) . "'"; - // Unreachable: return "E'" . pg_escape_string($s) . "'"; } function quote_ident( $s ) { @@ -1169,6 +1224,32 @@ END; } /** + * Postgres specific version of replaceVars. + * Calls the parent version in Database.php + * + * @private + * + * @param string $com SQL string, read from a stream (usually tables.sql) + * + * @return string SQL string + */ + protected function replaceVars( $ins ) { + + $ins = parent::replaceVars( $ins ); + + if ($this->numeric_version >= 8.3) { + // Thanks for not providing backwards-compatibility, 8.3 + $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins ); + } + + if ($this->numeric_version <= 8.1) { // Our minimum version + $ins = str_replace( 'USING gin', 'USING gist', $ins ); + } + + return $ins; + } + + /** * Various select options * * @private @@ -1223,6 +1304,10 @@ END; return false; } + function buildConcat( $stringList ) { + return implode( ' || ', $stringList ); + } + } // end DatabasePostgres class diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ad682b72..376e55b1 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -13,7 +13,7 @@ * depends on it. * * Documentation is in the source and on: - * http://www.mediawiki.org/wiki/Help:Configuration_settings + * http://www.mediawiki.org/wiki/Manual:Configuration_settings * */ @@ -31,7 +31,7 @@ require_once( "$IP/includes/SiteConfiguration.php" ); $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.11.2'; +$wgVersion = '1.12.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -157,6 +157,7 @@ $wgUploadDirectory = false; /// defaults to "{$IP}/images" $wgHashedUploadDirectory = true; $wgLogo = false; /// defaults to "{$wgStylePath}/common/images/wiki.png" $wgFavicon = '/favicon.ico'; +$wgAppleTouchIcon = false; /// This one'll actually default to off. For iPhone and iPod Touch web app bookmarks $wgMathPath = false; /// defaults to "{$wgUploadPath}/math" $wgMathDirectory = false; /// defaults to "{$wgUploadDirectory}/math" $wgTmpDirectory = false; /// defaults to "{$wgUploadDirectory}/tmp" @@ -458,7 +459,12 @@ $wgHashedSharedUploadDirectory = true; * * Please specify the namespace, as in the example below. */ -$wgRepositoryBaseUrl="http://commons.wikimedia.org/wiki/Image:"; +$wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/Image:"; + +/** + * Experimental feature still under debugging. + */ +$wgFileRedirects = false; # @@ -504,6 +510,16 @@ $wgEnableEmail = true; $wgEnableUserEmail = true; /** + * Set to true to put the sending user's email in a Reply-To header + * instead of From. ($wgEmergencyContact will be used as From.) + * + * Some mailers (eg sSMTP) set the SMTP envelope sender to the From value, + * which can cause problems with SPF validation and leak recipient addressses + * when bounces are sent to the sender. + */ +$wgUserEmailUseReplyTo = false; + +/** * Minimum time, in hours, which must elapse between password reminder * emails for a given account. This is to prevent abuse by mail flooding. */ @@ -594,7 +610,21 @@ $wgSharedDB = null; # These and any other user-defined properties will be assigned to the mLBInfo member # variable of the Database object. # -# Leave at false to use the single-server variables above +# Leave at false to use the single-server variables above. If you set this +# variable, the single-server variables will generally be ignored (except +# perhaps in some command-line scripts). +# +# The first server listed in this array (with key 0) will be the master. The +# rest of the servers will be slaves. To prevent writes to your slaves due to +# accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your +# slaves in my.cnf. You can set read_only mode at runtime using: +# +# SET @@read_only=1; +# +# Since the effect of writing to a slave is so damaging and difficult to clean +# up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even +# our masters, and then set read_only=0 on masters at runtime. +# $wgDBservers = false; /** How long to wait for a slave to catch up to the master */ @@ -607,6 +637,12 @@ $wgDBerrorLog = false; $wgDBClusterTimeout = 10; /** + * Scale load balancer polling time so that under overload conditions, the database server + * receives a SHOW STATUS query at an average interval of this many microseconds + */ +$wgDBAvgStatusPoll = 2000; + +/** * wgDBminWordLen : * MySQL 3.x : used to discard words that MySQL will not return any results for * shorter values configure mysql directly. @@ -643,7 +679,7 @@ $wgDBmysql5 = false; * account. * Array numeric key => database name */ -$wgLocalDatabases = array(); +$wgLocalDatabases = array(); /** * For multi-wiki clusters with multiple master servers; if an alternate @@ -700,7 +736,7 @@ $wgCachedMessageArrays = false; # Language settings # /** Site language code, should be one of ./languages/Language(.*).php */ -$wgLanguageCode = 'en'; +$wgLanguageCode = 'en'; /** * Some languages need different word forms, usually for different cases. @@ -715,6 +751,8 @@ $wgInterwikiMagic = true; /** Hide interlanguage links from the sidebar */ $wgHideInterlanguageLinks = false; +/** List of language names or overrides for default names in Names.php */ +$wgExtraLanguageNames = array(); /** We speak UTF-8 all the time now, unless some oddities happen */ $wgInputEncoding = 'UTF-8'; @@ -792,6 +830,12 @@ $wgMsgCacheExpiry = 86400; */ $wgMaxMsgCacheEntrySize = 10000; +/** + * Set to false if you are thorough system admin who always remembers to keep + * serialized files up to date to save few mtime calls. + */ +$wgCheckSerialized = true; + # Whether to enable language variant conversion. $wgDisableLangConversion = false; @@ -864,9 +908,19 @@ $wgRedirectSources = false; $wgShowIPinHeader = true; # For non-logged in users $wgMaxNameChars = 255; # Maximum number of bytes in username -$wgMaxSigChars = 255; # Maximum number of Unicode characters in signature +$wgMaxSigChars = 255; # Maximum number of Unicode characters in signature $wgMaxArticleSize = 2048; # Maximum article size in kilobytes +$wgMaxPPNodeCount = 1000000; # A complexity limit on template expansion + +/** + * Maximum recursion depth for templates within templates. + * The current parser adds two levels to the PHP call stack for each template, + * and xdebug limits the call stack to 100 by default. So this should hopefully + * stop the parser before it hits the xdebug limit. + */ +$wgMaxTemplateDepth = 40; + $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations @@ -959,6 +1013,11 @@ $wgEnableParserCache = true; $wgEnableSidebarCache = false; /** + * Expiry time for the sidebar cache, in seconds + */ +$wgSidebarCacheExpiry = 86400; + +/** * Under which condition should a page in the main namespace be counted * as a valid article? If $wgUseCommaCount is set to true, it will be * counted if it contains at least one comma. If it is set to false @@ -1055,13 +1114,18 @@ $wgGroupPermissions['bot' ]['bot'] = true; $wgGroupPermissions['bot' ]['autoconfirmed'] = true; $wgGroupPermissions['bot' ]['nominornewtalk'] = true; $wgGroupPermissions['bot' ]['autopatrol'] = true; +$wgGroupPermissions['bot' ]['suppressredirect'] = true; +$wgGroupPermissions['bot' ]['apihighlimits'] = true; // Most extra permission abilities go to this group $wgGroupPermissions['sysop']['block'] = true; $wgGroupPermissions['sysop']['createaccount'] = true; $wgGroupPermissions['sysop']['delete'] = true; +$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs $wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text +$wgGroupPermissions['sysop']['undelete'] = true; $wgGroupPermissions['sysop']['editinterface'] = true; +$wgGroupPermissions['sysop']['editusercssjs'] = true; $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; @@ -1079,9 +1143,15 @@ $wgGroupPermissions['sysop']['autoconfirmed'] = true; $wgGroupPermissions['sysop']['upload_by_url'] = true; $wgGroupPermissions['sysop']['ipblock-exempt'] = true; $wgGroupPermissions['sysop']['blockemail'] = true; +$wgGroupPermissions['sysop']['markbotedits'] = true; +$wgGroupPermissions['sysop']['suppressredirect'] = true; +$wgGroupPermissions['sysop']['apihighlimits'] = true; +#$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; +// Permission to change users' groups assignments across wikis +#$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true; // Experimental permissions, not ready for production use //$wgGroupPermissions['sysop']['deleterevision'] = true; @@ -1095,6 +1165,19 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true; */ # $wgGroupPermissions['developer']['siteadmin'] = true; + +/** + * Implicit groups, aren't shown on Special:Listusers or somewhere else + */ +$wgImplicitGroups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ); + +/** + * These are the groups that users are allowed to add to or remove from + * their own account via Special:Userrights. + */ +$wgGroupsAddToSelf = array(); +$wgGroupsRemoveFromSelf = array(); + /** * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. @@ -1151,6 +1234,28 @@ $wgAutoConfirmCount = 0; //$wgAutoConfirmCount = 50; /** + * Automatically add a usergroup to any user who matches certain conditions. + * The format is + * array( '&' or '|' or '^', cond1, cond2, ... ) + * where cond1, cond2, ... are themselves conditions; *OR* + * APCOND_EMAILCONFIRMED, *OR* + * array( APCOND_EMAILCONFIRMED ), *OR* + * array( APCOND_EDITCOUNT, number of edits ), *OR* + * array( APCOND_AGE, seconds since registration ), *OR* + * similar constructs defined by extensions. + * + * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any + * user who has provided an e-mail address. + */ +$wgAutopromote = array( + 'autoconfirmed' => array( '&', + array( APCOND_EDITCOUNT, &$wgAutoConfirmCount ), + array( APCOND_AGE, &$wgAutoConfirmAge ), + ), + 'emailconfirmed' => APCOND_EMAILCONFIRMED, +); + +/** * These settings can be used to give finer control over who can assign which * groups at Special:Userrights. Example configuration: * @@ -1165,6 +1270,12 @@ $wgAutoConfirmCount = 0; */ $wgAddGroups = $wgRemoveGroups = array(); +/** + * Optional to restrict deletion of pages with higher revision counts + * to users with the 'bigdelete' permission. (Default given to sysops.) + */ +$wgDeleteRevisionsLimit = 0; + # Proxy scanner settings # @@ -1214,7 +1325,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '97'; +$wgStyleVersion = '116'; # Server-side caching: @@ -1333,15 +1444,24 @@ $wgInternalServer = $wgServer; $wgSquidMaxage = 18000; /** - * A list of proxy servers (ips if possible) to purge on changes don't specify - * ports here (80 is default). When mediawiki is running behind a proxy, its - * address should be listed in $wgSquidServers otherwise mediawiki won't rely - * on the X-FORWARDED-FOR header to determine the user IP address and - * all users will appear to come from the proxy IP address. Don't use domain - * names here, only IP adresses. + * Default maximum age for raw CSS/JS accesses + */ +$wgForcedRawSMaxage = 300; + +/** + * List of proxy servers to purge on changes; default port is 80. Use IP addresses. + * + * When MediaWiki is running behind a proxy, it will trust X-Forwarded-For + * headers sent/modified from these proxies when obtaining the remote IP address + * + * For a list of trusted servers which *aren't* purged, see $wgSquidServersNoPurge. */ -# $wgSquidServers = array('127.0.0.1'); $wgSquidServers = array(); + +/** + * As above, except these servers aren't purged on page changes; use to set a + * list of trusted proxies, etc. + */ $wgSquidServersNoPurge = array(); /** Maximum number of titles to purge in any one client operation */ @@ -1442,6 +1562,14 @@ $wgDebugFunctionEntry = 0; /** Lots of debugging output from SquidUpdate.php */ $wgDebugSquid = false; +/* + * Destination for wfIncrStats() data... + * 'cache' to go into the system cache, if enabled (memcached) + * 'udp' to be sent to the UDP profiler (see $wgUDPProfilerHost) + * false to disable + */ +$wgStatsMethod = 'cache'; + /** Whereas to count the number of time an article is viewed. * Does not work if pages are cached (for example with squid). */ @@ -1598,9 +1726,11 @@ $wgMediaHandlers = array( 'image/png' => 'BitmapHandler', 'image/gif' => 'BitmapHandler', 'image/x-ms-bmp' => 'BmpHandler', - 'image/svg+xml' => 'SvgHandler', - 'image/svg' => 'SvgHandler', - 'image/vnd.djvu' => 'DjVuHandler', + 'image/svg+xml' => 'SvgHandler', // official + 'image/svg' => 'SvgHandler', // compat + 'image/vnd.djvu' => 'DjVuHandler', // official + 'image/x.djvu' => 'DjVuHandler', // compat + 'image/x-djvu' => 'DjVuHandler', // compat ); @@ -1688,7 +1818,7 @@ $wgIgnoreImageErrors = false; $wgGenerateThumbnailOnParse = true; /** Obsolete, always true, kept for compatibility with extensions */ -$wgUseImageResize = true; +$wgUseImageResize = true; /** Set $wgCommandLineMode if it's not set already, to avoid notices */ @@ -1848,7 +1978,19 @@ $wgAlwaysUseTidy = false; $wgTidyBin = 'tidy'; $wgTidyConf = $IP.'/includes/tidy.conf'; $wgTidyOpts = ''; -$wgTidyInternal = function_exists( 'tidy_load_config' ); +$wgTidyInternal = extension_loaded( 'tidy' ); + +/** + * Put tidy warnings in HTML comments + * Only works for internal tidy. + */ +$wgDebugTidy = false; + +/** + * Validate the overall output using tidy and refuse + * to display the page if it's not valid. + */ +$wgValidateAllHtml = false; /** See list of skins and their symbolic names in languages/Language.php */ $wgDefaultSkin = 'monobook'; @@ -1920,7 +2062,11 @@ $wgSkinExtensionFunctions = array(); * Extension messages files * Associative array mapping extension name to the filename where messages can be found. * The file must create a variable called $messages. - * When the messages are needed, the extension should call wfLoadMessagesFile() + * When the messages are needed, the extension should call wfLoadExtensionMessages(). + * + * Example: + * $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php'; + * */ $wgExtensionMessagesFiles = array(); @@ -1958,8 +2104,9 @@ $wgSpecialPages = array(); $wgAutoloadClasses = array(); /** - * An array of extension types and inside that their names, versions, authors - * and urls, note that the version and url key can be omitted. + * An array of extension types and inside that their names, versions, authors, + * urls, descriptions and pointers to localized description msgs. Note that + * the version, url, description and descriptionmsg key can be omitted. * * <code> * $wgExtensionCredits[$type][] = array( @@ -1967,10 +2114,12 @@ $wgAutoloadClasses = array(); * 'version' => 1.9, * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', + * 'description' => 'An example extension', + * 'descriptionmsg' => 'exampleextension-desc', * ); * </code> * - * Where $type is 'specialpage', 'parserhook', or 'other'. + * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'. */ $wgExtensionCredits = array(); /* @@ -2012,6 +2161,9 @@ $wgExternalDiffEngine = false; /** Use RC Patrolling to check for vandalism */ $wgUseRCPatrol = true; +/** Use new page patrolling to check new pages on special:Newpages */ +$wgUseNPPatrol = true; + /** Set maximum number of results to return in syndication feeds (RSS, Atom) for * eg Recentchanges, Newpages. */ $wgFeedLimit = 50; @@ -2238,6 +2390,7 @@ $wgLogTypes = array( '', 'move', 'import', 'patrol', + 'merge', ); /** @@ -2256,6 +2409,7 @@ $wgLogNames = array( 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', + 'merge' => 'mergelog', ); /** @@ -2274,6 +2428,7 @@ $wgLogHeaders = array( 'move' => 'movelogpagetext', 'import' => 'importlogpagetext', 'patrol' => 'patrol-log-header', + 'merge' => 'mergelogpagetext', ); /** @@ -2293,12 +2448,13 @@ $wgLogActions = array( 'delete/restore' => 'undeletedarticle', 'delete/revision' => 'revdelete-logentry', 'upload/upload' => 'uploadedimage', - 'upload/overwrite' => 'overwroteimage', + 'upload/overwrite' => 'overwroteimage', 'upload/revert' => 'uploadedimage', 'move/move' => '1movedto2', 'move/move_redir' => '1movedto2_redir', 'import/upload' => 'import-logentry-upload', 'import/interwiki' => 'import-logentry-interwiki', + 'merge/merge' => 'pagemerge-logentry', ); /** @@ -2343,8 +2499,15 @@ $wgNoFollowLinks = true; $wgNoFollowNsExceptions = array(); /** + * Default robot policy. + * The default policy is to encourage indexing and following of links. + * It may be overridden on a per-namespace and/or per-page basis. + */ +$wgDefaultRobotPolicy = 'index,follow'; + +/** * Robot policies per namespaces. - * The default policy is 'index,follow', the array is made of namespace + * The default policy is given above, the array is made of namespace * constants as defined in includes/Defines.php * Example: * $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); @@ -2423,7 +2586,7 @@ $wgRateLimits = array( 'edit' => array( 'anon' => null, // for any and all anonymous edits (aggregate) 'user' => null, // for each logged-in user - 'newbie' => null, // for each recent account; overrides 'user' + 'newbie' => null, // for each recent (autoconfirmed) account; overrides 'user' 'ip' => null, // for each anon and recent account 'subnet' => null, // ... with final octet removed ), @@ -2748,3 +2911,29 @@ $wgDisableOutputCompression = false; */ $wgSlaveLagWarning = 10; $wgSlaveLagCritical = 30; + +/** + * Parser configuration. Associative array with the following members: + * + * class The class name + * + * The entire associative array will be passed through to the constructor as + * the first parameter. Note that only Setup.php can use this variable -- + * the configuration will change at runtime via $wgParser member functions, so + * the contents of this variable will be out-of-date. The variable can only be + * changed during LocalSettings.php, in particular, it can't be changed during + * an extension setup function. + */ +$wgParserConf = array( + 'class' => 'Parser', +); + +/** + * Hooks that are used for outputting exceptions + * Format is: + * $wgExceptionHooks[] = $funcname + * or: + * $wgExceptionHooks[] = array( $class, $funcname ) + * Hooks should return strings or false + */ +$wgExceptionHooks = array(); diff --git a/includes/Defines.php b/includes/Defines.php index c923c256..2d6aee5f 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -260,6 +260,27 @@ define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); define( 'UTF8_HEAD', false ); define( 'UTF8_TAIL', true ); - - - +# Hook support constants +define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 ); +define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 ); + +# Allowed values for Parser::$mOutputType +# Parameter to Parser::startExternalParse(). +define( 'OT_HTML', 1 ); +define( 'OT_WIKI', 2 ); +define( 'OT_PREPROCESS', 3 ); +define( 'OT_MSG' , 3 ); // b/c alias for OT_PREPROCESS + +# Flags for Parser::setFunctionHook +define( 'SFH_NO_HASH', 1 ); +define( 'SFH_OBJECT_ARGS', 2 ); + +# Flags for Parser::replaceLinkHolders +define( 'RLH_FOR_UPDATE', 1 ); + +# Autopromote conditions (must be here and not in Autopromote.php, so that +# they're loaded for DefaultSettings.php before AutoLoader.php) +define( 'APCOND_EDITCOUNT', 1 ); +define( 'APCOND_AGE', 2 ); +define( 'APCOND_EMAILCONFIRMED', 3 ); +define( 'APCOND_INGROUPS', 4 ); diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php index 99bb4798..9aa17bbb 100644 --- a/includes/DifferenceEngine.php +++ b/includes/DifferenceEngine.php @@ -38,8 +38,9 @@ class DifferenceEngine { * @param $old Integer: old ID we want to show and diff with. * @param $new String: either 'prev' or 'next'. * @param $rcid Integer: ??? FIXME (default 0) + * @param $refreshCache boolean If set, refreshes the diff cache */ - function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) { + function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) { $this->mTitle = $titleObj; wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n"); @@ -68,6 +69,7 @@ class DifferenceEngine { $this->mNewid = intval($new); } $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer + $this->mRefreshCache = $refreshCache; } function showDiffPage( $diffOnly = false ) { @@ -107,9 +109,8 @@ CONTROL; $wgOut->setArticleFlag( false ); if ( ! $this->loadRevisionData() ) { $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})"; - $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addWikitext( $mtext ); + $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); wfProfileOut( $fname ); return; } @@ -164,14 +165,15 @@ CONTROL; $rcid = $this->mRcidMarkPatrolled; } else { // Look for an unpatrolled change corresponding to this diff + $db = wfGetDB( DB_SLAVE ); $change = RecentChange::newFromConds( array( - // Add redundant timestamp condition so we can use the - // existing index - 'rc_timestamp' => $this->mNewRev->getTimestamp(), + // Add redundant user,timestamp condition so we can use the existing index + 'rc_user_text' => $this->mNewRev->getRawUserText(), + 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 'rc_this_oldid' => $this->mNewid, 'rc_last_oldid' => $this->mOldid, - 'rc_patrolled' => 0, + 'rc_patrolled' => 0 ), __METHOD__ ); @@ -217,14 +219,49 @@ CONTROL; $newminor = wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ) . ' '; } + + $rdel = ''; $ldel = ''; + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $ldel = wfMsgHtml('rev-delundel'); + } else { + $ldel = $sk->makeKnownLinkObj( $revdel, + wfMsgHtml('rev-delundel'), + 'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $this->mOldRev->getId() ) ); + // Bolden oversighted content + if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) + $ldel = "<strong>$ldel</strong>"; + } + $ldel = " <tt>(<small>$ldel</small>)</tt> "; + // We don't currently handle well changing the top revision's settings + if( $this->mNewRev->isCurrent() ) { + // If revision was hidden from sysops + $rdel = wfMsgHtml('rev-delundel'); + } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $rdel = wfMsgHtml('rev-delundel'); + } else { + $rdel = $sk->makeKnownLinkObj( $revdel, + wfMsgHtml('rev-delundel'), + 'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $this->mNewRev->getId() ) ); + // Bolden oversighted content + if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) + $rdel = "<strong>$rdel</strong>"; + } + $rdel = " <tt>(<small>$rdel</small>)</tt> "; + } - $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' . - '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" . - '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</div>" . - '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; - $newHeader = '<div id="mw-diff-ntitle1"><strong>' .$this->mNewtitle . '</strong></div>' . - '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev ) . " $rollback</div>" . - '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" . + $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' . + '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" . + '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" . + '<div id="mw-diff-otitle4">' . $prevlink .'</div>'; + $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' . + '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" . + '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" . '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>'; $this->showDiff( $oldHeader, $newHeader ); @@ -245,8 +282,10 @@ CONTROL; $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); #add deleted rev tag if needed - if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); } if( !$this->mNewRev->isCurrent() ) { @@ -258,7 +297,20 @@ CONTROL; $wgOut->setRevisionId( $this->mNewRev->getId() ); } - $wgOut->addWikiTextTidy( $this->mNewtext ); + if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) { + // Stolen from Article::view --AG 2007-10-11 + + // Give hooks a chance to customise the output + if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) { + // Wrap the whole lot in a <pre> and don't parse + $m = array(); + preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); + $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $wgOut->addHtml( htmlspecialchars( $this->mNewtext ) ); + $wgOut->addHtml( "\n</pre>\n" ); + } + } else + $wgOut->addWikiTextTidy( $this->mNewtext ); if( !$this->mNewRev->isCurrent() ) { $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); @@ -282,9 +334,8 @@ CONTROL; if ( ! $this->loadNewText() ) { $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . "{$this->mNewid})"; - $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addWikitext( $mtext ); + $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); wfProfileOut( $fname ); return; } @@ -324,10 +375,10 @@ CONTROL; * Returns false if the diff could not be generated, otherwise returns true */ function showDiff( $otitle, $ntitle ) { - global $wgOut, $wgRequest; - $diff = $this->getDiff( $otitle, $ntitle, $wgRequest->getVal( 'action' ) == 'purge' ); + global $wgOut; + $diff = $this->getDiff( $otitle, $ntitle ); if ( $diff === false ) { - $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) ); + $wgOut->addWikiMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ); return false; } else { $this->showDiffStyle(); @@ -352,11 +403,10 @@ CONTROL; * * @param Title $otitle Old title * @param Title $ntitle New title - * @param bool $skipCache Skip the diff cache for this request? * @return mixed */ - function getDiff( $otitle, $ntitle, $skipCache = false ) { - $body = $this->getDiffBody( $skipCache ); + function getDiff( $otitle, $ntitle ) { + $body = $this->getDiffBody(); if ( $body === false ) { return false; } else { @@ -368,43 +418,49 @@ CONTROL; /** * Get the diff table body, without header * - * @param bool $skipCache Skip cache for this request? * @return mixed */ - function getDiffBody( $skipCache = false ) { + function getDiffBody() { global $wgMemc; $fname = 'DifferenceEngine::getDiffBody'; wfProfileIn( $fname ); // Cacheable? $key = false; - if ( $this->mOldid && $this->mNewid && !$skipCache ) { - // Try cache + if ( $this->mOldid && $this->mNewid ) { $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid ); - $difftext = $wgMemc->get( $key ); - if ( $difftext ) { - wfIncrStats( 'diff_cache_hit' ); - $difftext = $this->localiseLineNumbers( $difftext ); - $difftext .= "\n<!-- diff cache key $key -->\n"; - wfProfileOut( $fname ); - return $difftext; - } + // Try cache + if ( !$this->mRefreshCache ) { + $difftext = $wgMemc->get( $key ); + if ( $difftext ) { + wfIncrStats( 'diff_cache_hit' ); + $difftext = $this->localiseLineNumbers( $difftext ); + $difftext .= "\n<!-- diff cache key $key -->\n"; + wfProfileOut( $fname ); + return $difftext; + } + } // don't try to load but save the result } - #loadtext is permission safe, this just clears out the diff + // Loadtext is permission safe, this just clears out the diff if ( !$this->loadText() ) { wfProfileOut( $fname ); return false; } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { - return ''; + return ''; } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - return ''; + return ''; } $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); // Save to cache for 7 days - if ( $key !== false && $difftext !== false ) { + // Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it! + if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { + wfIncrStats( 'diff_uncacheable' ); + } else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + wfIncrStats( 'diff_uncacheable' ); + } else if ( $key !== false && $difftext !== false ) { wfIncrStats( 'diff_cache_miss' ); $wgMemc->set( $key, $difftext, 7*86400 ); } else { @@ -536,15 +592,9 @@ CONTROL; /** * Add the header to a diff body */ - function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { + static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { global $wgOut; - - if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { - $otitle = '<span class="history-deleted">'.$otitle.'</span>'; - } - if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $ntitle = '<span class="history-deleted">'.$ntitle.'</span>'; - } + $header = " <table class='diff'> <col class='diff-marker' /> @@ -615,11 +665,16 @@ CONTROL; } else { $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); - $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); + $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp ); $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>" . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; } + if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>"; + } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>'; + } // Load the old revision object $this->mOldRev = false; @@ -647,12 +702,20 @@ CONTROL; $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true ); $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); - $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) - . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) ); + $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>" + . " (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; // Add an "undo" link $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); - $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) ) + $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + + if ( !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { + $this->mOldtitle = "<span class='history-deleted'>{$this->mOldPagetitle}</span>"; + } else if ( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { + $this->mOldtitle = '<span class="history-deleted">'.$this->mOldtitle.'</span>'; + } } return true; @@ -673,7 +736,6 @@ CONTROL; return false; } if ( $this->mOldRev ) { - // FIXME: permission tests $this->mOldtext = $this->mOldRev->revText(); if ( $this->mOldtext === false ) { return false; @@ -1584,7 +1646,7 @@ class DiffFormatter } function _start_block($header) { - echo $header; + echo $header . "\n"; } function _end_block() { @@ -1613,6 +1675,84 @@ class DiffFormatter } } +/** + * A formatter that outputs unified diffs + * @addtogroup DifferenceEngine + */ + +class UnifiedDiffFormatter extends DiffFormatter +{ + var $leading_context_lines = 2; + var $trailing_context_lines = 2; + + function _added($lines) { + $this->_lines($lines, '+'); + } + function _deleted($lines) { + $this->_lines($lines, '-'); + } + function _changed($orig, $closing) { + $this->_deleted($orig); + $this->_added($closing); + } + function _block_header($xbeg, $xlen, $ybeg, $ylen) { + return "@@ -$xbeg,$xlen +$ybeg,$ylen @@"; + } +} + +/** + * A pseudo-formatter that just passes along the Diff::$edits array + * @addtogroup DifferenceEngine + */ +class ArrayDiffFormatter extends DiffFormatter +{ + function format($diff) + { + $oldline = 1; + $newline = 1; + $retval = array(); + foreach($diff->edits as $edit) + switch($edit->type) + { + case 'add': + foreach($edit->closing as $l) + { + $retval[] = array( + 'action' => 'add', + 'new'=> $l, + 'newline' => $newline++ + ); + } + break; + case 'delete': + foreach($edit->orig as $l) + { + $retval[] = array( + 'action' => 'delete', + 'old' => $l, + 'oldline' => $oldline++, + ); + } + break; + case 'change': + foreach($edit->orig as $i => $l) + { + $retval[] = array( + 'action' => 'change', + 'old' => $l, + 'new' => @$edit->closing[$i], + 'oldline' => $oldline++, + 'newline' => $newline++, + ); + } + break; + case 'copy': + $oldline += count($edit->orig); + $newline += count($edit->orig); + } + return $retval; + } +} /** * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 @@ -1828,13 +1968,15 @@ class TableDiffFormatter extends DiffFormatter function _added( $lines ) { foreach ($lines as $line) { echo '<tr>' . $this->emptyLine() . - $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + $this->addedLine( '<ins class="diffchange">' . + htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n"; } } function _deleted($lines) { foreach ($lines as $line) { - echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) . + echo '<tr>' . $this->deletedLine( '<del class="diffchange">' . + htmlspecialchars ( $line ) . '</del>' ) . $this->emptyLine() . "</tr>\n"; } } diff --git a/includes/EditPage.php b/includes/EditPage.php index cceb053d..8c3a37d4 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -8,8 +8,39 @@ * The actual database and text munging is still in Article, * but it should get easier to call those from alternate * interfaces. + * + * EditPage cares about two distinct titles: + * $wgTitle is the page that forms submit to, links point to, + * redirects go to, etc. $this->mTitle (as well as $mArticle) is the + * page in the database that is actually being edited. These are + * usually the same, but they are now allowed to be different. */ class EditPage { + const AS_SUCCESS_UPDATE = 200; + const AS_SUCCESS_NEW_ARTICLE = 201; + const AS_HOOK_ERROR = 210; + const AS_FILTERING = 211; + const AS_HOOK_ERROR_EXPECTED = 212; + const AS_BLOCKED_PAGE_FOR_USER = 215; + const AS_CONTENT_TOO_BIG = 216; + const AS_USER_CANNOT_EDIT = 217; + const AS_READ_ONLY_PAGE_ANON = 218; + const AS_READ_ONLY_PAGE_LOGGED = 219; + const AS_READ_ONLY_PAGE = 220; + const AS_RATE_LIMITED = 221; + const AS_ARTICLE_WAS_DELETED = 222; + const AS_NO_CREATE_PERMISSION = 223; + const AS_BLANK_ARTICLE = 224; + const AS_CONFLICT_DETECTED = 225; + const AS_SUMMARY_NEEDED = 226; + const AS_TEXTBOX_EMPTY = 228; + const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; + const AS_OK = 230; + const AS_END = 231; + const AS_SPAM_ERROR = 232; + const AS_IMAGE_REDIRECT_ANON = 233; + const AS_IMAGE_REDIRECT_LOGGED = 234; + var $mArticle; var $mTitle; var $mMetaData = ''; @@ -42,9 +73,15 @@ class EditPage { # extensions should take care to _append_ to the present value public $editFormPageTop; // Before even the preview public $editFormTextTop; + public $editFormTextBeforeContent; public $editFormTextAfterWarn; public $editFormTextAfterTools; public $editFormTextBottom; + + /* $didSave should be set to true whenever an article was succesfully altered. */ + public $didSave = false; + + public $suppressIntro = false; /** * @todo document @@ -52,12 +89,12 @@ class EditPage { */ function EditPage( $article ) { $this->mArticle =& $article; - global $wgTitle; - $this->mTitle =& $wgTitle; + $this->mTitle = $article->getTitle(); # Placeholders for text injection by hooks (empty per default) $this->editFormPageTop = $this->editFormTextTop = + $this->editFormTextBeforeContent = $this->editFormTextAfterWarn = $this->editFormTextAfterTools = $this->editFormTextBottom = ""; @@ -65,8 +102,9 @@ class EditPage { /** * Fetch initial editing page content. + * @private */ - private function getContent( $def_text = '' ) { + function getContent( $def_text = '' ) { global $wgOut, $wgRequest, $wgParser; # Get variables from query string :P @@ -297,14 +335,13 @@ class EditPage { * the newly-edited page. */ function edit() { - global $wgOut, $wgUser, $wgRequest, $wgTitle; + global $wgOut, $wgUser, $wgRequest; - if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) return; - $fname = 'EditPage::edit'; - wfProfileIn( $fname ); - wfDebug( "$fname: enter\n" ); + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__.": enter\n" ); // this is not an article $wgOut->setArticleFlag(false); @@ -314,13 +351,28 @@ class EditPage { if( $this->live ) { $this->livePreview(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); + return; + } + + if( wfReadOnly() ) { + $wgOut->readOnlyPage( $this->getContent() ); + wfProfileOut( __METHOD__ ); return; } - $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser); - if( !$this->mTitle->exists() ) - $permErrors += $this->mTitle->getUserPermissionsErrors( 'create', $wgUser); + $permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser); + if( !$this->mTitle->exists() ) { + # We can't use array_diff here, because that considers ANY TWO + # ARRAYS TO BE EQUAL. Thanks, PHP. + $createErrors = $this->mTitle->getUserPermissionsErrors('create', $wgUser); + foreach( $createErrors as $error ) { + # in_array() actually *does* work as expected. + if( !in_array( $error, $permErrors ) ) { + $permErrors[] = $error; + } + } + } # Ignore some permissions errors. $remove = array(); @@ -341,14 +393,12 @@ class EditPage { } } } - # array_diff returns elements in $permErrors that are not in $remove. $permErrors = array_diff( $permErrors, $remove ); - if ( !empty($permErrors) ) - { - wfDebug( "$fname: User can't edit\n" ); + if ( !empty($permErrors) ) { + wfDebug( __METHOD__.": User can't edit\n" ); $wgOut->readOnlyPage( $this->getContent(), true, $permErrors ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } else { if ( $this->save ) { @@ -368,12 +418,12 @@ class EditPage { } } - wfProfileIn( "$fname-business-end" ); + wfProfileIn( __METHOD__."-business-end" ); $this->isConflict = false; // css / js subpages of user pages get a special treatment - $this->isCssJsSubpage = $wgTitle->isCssJsSubpage(); - $this->isValidCssJsSubpage = $wgTitle->isValidCssJsSubpage(); + $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); + $this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage(); /* Notice that we can't use isDeleted, because it returns true if article is ever deleted * no matter it's current state @@ -401,7 +451,7 @@ class EditPage { $this->showIntro(); if( $this->mTitle->isTalkPage() ) { - $wgOut->addWikiText( wfMsg( 'talkpagetext' ) ); + $wgOut->addWikiMsg( 'talkpagetext' ); } # Attempt submission here. This will check for edit conflicts, @@ -411,8 +461,8 @@ class EditPage { if ( 'save' == $this->formtype ) { if ( !$this->attemptSave() ) { - wfProfileOut( "$fname-business-end" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ ); return; } } @@ -422,8 +472,8 @@ class EditPage { if ( 'initial' == $this->formtype || $this->firsttime ) { if ($this->initialiseForm() === false) { $this->noSuchSectionPage(); - wfProfileOut( "$fname-business-end" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ ); return; } if( !$this->mTitle->getArticleId() ) @@ -431,8 +481,8 @@ class EditPage { } $this->showEditForm(); - wfProfileOut( "$fname-business-end" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ ); } /** @@ -587,16 +637,32 @@ class EditPage { */ private function showIntro() { global $wgOut, $wgUser; + if( $this->suppressIntro ) + return; + + # Show a warning message when someone creates/edits a user (talk) page but the user does not exists + if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { + $parts = explode( '/', $this->mTitle->getText(), 2 ); + $username = $parts[0]; + $id = User::idFromName( $username ); + $ip = User::isIP( $username ); + + if ( $id == 0 && !$ip ) { + $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', + array( 'userpage-userdoesnotexist', $username ) ); + } + } + if( !$this->showCustomIntro() && !$this->mTitle->exists() ) { if( $wgUser->isLoggedIn() ) { - $wgOut->addWikiText( wfMsg( 'newarticletext' ) ); + $wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' ); } else { - $wgOut->addWikiText( wfMsg( 'newarticletextanon' ) ); + $wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' ); } $this->showDeletionLog( $wgOut ); } } - + /** * Attempt to show a custom editing introduction, if supplied * @@ -619,11 +685,11 @@ class EditPage { } /** - * Attempt submission - * @return bool false if output is done, true if the rest of the form should be displayed + * Attempt submission (no UI) + * @return one of the constants describing the result */ - function attemptSave() { - global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut; + function internalAttemptSave( &$result, $bot = false ) { + global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut, $wgParser; global $wgMaxArticleSize; $fname = 'EditPage::attemptSave'; @@ -633,7 +699,18 @@ class EditPage { if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); - return false; + return self::AS_HOOK_ERROR; + } + + # Check image redirect + if ( $this->mTitle->getNamespace() == NS_IMAGE && + Title::newFromRedirect( $this->textbox1 ) instanceof Title && + !$wgUser->isAllowed( 'upload' ) ) { + if( $wgUser->isAnon() ) { + return self::AS_IMAGE_REDIRECT_ANON; + } else { + return self::AS_IMAGE_REDIRECT_LOGGED; + } } # Reintegrate metadata @@ -643,34 +720,33 @@ class EditPage { # Check for spam $matches = array(); if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { - $this->spamPage ( $matches[0] ); + $result['spam'] = $matches[0]; wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_SPAM_ERROR; } if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) { # Error messages or other handling should be performed by the filter function - wfProfileOut( $fname ); wfProfileOut( "$fname-checks" ); - return false; + wfProfileOut( $fname ); + return self::AS_FILTERING; } if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) { # Error messages etc. could be handled within the hook... - wfProfileOut( $fname ); wfProfileOut( "$fname-checks" ); - return false; + wfProfileOut( $fname ); + return self::AS_HOOK_ERROR; } elseif( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error - wfProfileOut( "$fname-checks " ); + wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return true; + return self::AS_HOOK_ERROR_EXPECTED; } if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { # Check block state against master, thus 'false'. - $this->blockedPage(); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_BLOCKED_PAGE_FOR_USER; } $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); if ( $this->kblength > $wgMaxArticleSize ) { @@ -678,35 +754,31 @@ class EditPage { $this->tooBig = true; wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return true; + return self::AS_CONTENT_TOO_BIG; } if ( !$wgUser->isAllowed('edit') ) { if ( $wgUser->isAnon() ) { - $this->userNotLoggedInPage(); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_READ_ONLY_PAGE_ANON; } else { - $wgOut->readOnlyPage(); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_READ_ONLY_PAGE_LOGGED; } } if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_READ_ONLY_PAGE; } if ( $wgUser->pingLimiter() ) { - $wgOut->rateLimited(); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return false; + return self::AS_RATE_LIMITED; } # If the article has been deleted while editing, don't save it without @@ -714,7 +786,7 @@ class EditPage { if ( $this->deletedSinceEdit && !$this->recreate ) { wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); - return true; + return self::AS_ARTICLE_WAS_DELETED; } wfProfileOut( "$fname-checks" ); @@ -726,24 +798,30 @@ class EditPage { // Late check for create permission, just in case *PARANOIA* if ( !$this->mTitle->userCan( 'create' ) ) { wfDebug( "$fname: no create permission\n" ); - $this->noCreatePermission(); wfProfileOut( $fname ); - return; + return self::AS_NO_CREATE_PERMISSION; } # Don't save a new article if it's blank. - if ( ( '' == $this->textbox1 ) ) { - $wgOut->redirect( $this->mTitle->getFullURL() ); + if ( '' == $this->textbox1 ) { wfProfileOut( $fname ); - return false; + return self::AS_BLANK_ARTICLE; } - $isComment=($this->section=='new'); + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError ) ) ) { + # Error messages etc. could be handled within the hook... + wfProfileOut( $fname ); + return self::AS_HOOK_ERROR; + } + + $isComment = ( $this->section == 'new' ); + $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment); + $this->minoredit, $this->watchthis, false, $isComment, $bot); wfProfileOut( $fname ); - return false; + return self::AS_SUCCESS_NEW_ARTICLE; } # Article exists. Check for edit conflict. @@ -808,18 +886,25 @@ class EditPage { if ( $this->isConflict ) { wfProfileOut( $fname ); - return true; + return self::AS_CONFLICT_DETECTED; } $oldtext = $this->mArticle->getContent(); + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError ) ) ) { + # Error messages etc. could be handled within the hook... + wfProfileOut( $fname ); + return self::AS_HOOK_ERROR; + } + # Handle the user preference to force summaries here, but not for null edits if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') && 0 != strcmp($oldtext, $text) && !Article::getRedirectAutosummary( $text )) { if( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); - return( true ); + return self::AS_SUMMARY_NEEDED; } } @@ -828,7 +913,7 @@ class EditPage { if (trim($this->summary) == '') { $this->missingSummary = true; wfProfileOut( $fname ); - return( true ); + return self::AS_SUMMARY_NEEDED; } } @@ -838,14 +923,14 @@ class EditPage { if( $this->section == 'new' ) { if ( $this->textbox1 == '' ) { $this->missingComment = true; - return true; + return self::AS_TEXTBOX_EMPTY; } if( $this->summary != '' ) { - $sectionanchor = $this->sectionAnchor( $this->summary ); + $sectionanchor = $wgParser->guessSectionNameFromWikiText( $this->summary ); # This is a new section, so create a link to the new section # in the revision summary. - $this->summary = wfMsgForContent('newsectionsummary') . - " [[{$this->mTitle->getPrefixedText()}#{$this->summary}|{$this->summary}]]"; + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); } } elseif( $this->section != '' ) { # Try to get a section anchor from the section source, redirect to edited section if header found @@ -855,7 +940,7 @@ class EditPage { # we can't deal with anchors, includes, html etc in the header for now, # headline would need to be parsed to improve this if($hasmatch and strlen($matches[2]) > 0) { - $sectionanchor = $this->sectionAnchor( $matches[2] ); + $sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] ); } } wfProfileOut( "$fname-sectionanchor" ); @@ -872,19 +957,19 @@ class EditPage { if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; wfProfileOut( $fname ); - return true; + return self::AS_MAX_ARTICLE_SIZE_EXCEEDED; } # update the article here if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, '', $sectionanchor ) ) { + $this->watchthis, $bot, $sectionanchor ) ) { wfProfileOut( $fname ); - return false; + return self::AS_SUCCESS_UPDATE; } else { $this->isConflict = true; } wfProfileOut( $fname ); - return true; + return self::AS_END; } /** @@ -897,8 +982,8 @@ class EditPage { $this->textbox1 = $this->getContent(false); if ($this->textbox1 === false) return false; - if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) - $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ); + if ( !$this->mArticle->exists() && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) + $this->textbox1 = wfMsgWeirdKey( $this->mTitle->getText() ); wfProxyCheck(); return true; } @@ -910,7 +995,7 @@ class EditPage { * near the top, for captchas and the like. */ function showEditForm( $formCallback=null ) { - global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize; + global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle; $fname = 'EditPage::showEditForm'; wfProfileIn( $fname ); @@ -929,116 +1014,123 @@ class EditPage { } if ( $this->isConflict ) { - $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); + $s = wfMsg( 'editconflict', $wgTitle->getPrefixedText() ); $wgOut->setPageTitle( $s ); - $wgOut->addWikiText( wfMsg( 'explainconflict' ) ); + $wgOut->addWikiMsg( 'explainconflict' ); $this->textbox2 = $this->textbox1; $this->textbox1 = $this->getContent(); $this->edittime = $this->mArticle->getTimestamp(); } else { - if( $this->section != '' ) { if( $this->section == 'new' ) { - $s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() ); + $s = wfMsg('editingcomment', $wgTitle->getPrefixedText() ); } else { - $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); + $s = wfMsg('editingsection', $wgTitle->getPrefixedText() ); $matches = array(); if( !$this->summary && !$this->preview && !$this->diff ) { preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); if( !empty( $matches[2] ) ) { - $this->summary = "/* ". trim($matches[2])." */ "; + global $wgParser; + $this->summary = "/* " . + $wgParser->stripSectionName(trim($matches[2])) . + " */ "; } } } } else { - $s = wfMsg( 'editing', $this->mTitle->getPrefixedText() ); + $s = wfMsg( 'editing', $wgTitle->getPrefixedText() ); } $wgOut->setPageTitle( $s ); if ( $this->missingComment ) { - $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); + $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' ); } if( $this->missingSummary && $this->section != 'new' ) { - $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); + $wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' ); } if( $this->missingSummary && $this->section == 'new' ) { - $wgOut->addWikiText( wfMsg( 'missingcommentheader' ) ); + $wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' ); } - if( !$this->hookError == '' ) { + if( $this->hookError !== '' ) { $wgOut->addWikiText( $this->hookError ); } if ( !$this->checkUnicodeCompliantBrowser() ) { - $wgOut->addWikiText( wfMsg( 'nonunicodebrowser') ); + $wgOut->addWikiMsg( 'nonunicodebrowser' ); } if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { // Let sysop know that this will make private content public if saved - if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + + if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); } + if( !$this->mArticle->mRevision->isCurrent() ) { $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); - $wgOut->addWikiText( wfMsg( 'editingold' ) ); + $wgOut->addWikiMsg( 'editingold' ); } } } if( wfReadOnly() ) { - $wgOut->addWikiText( wfMsg( 'readonlywarning' ) ); + $wgOut->addHTML( '<div id="mw-read-only-warning">'.wfMsgWikiHTML( 'readonlywarning' ).'</div>' ); } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) { - $wgOut->addWikiText( wfMsg( 'anoneditwarning' ) ); + $wgOut->addHTML( '<div id="mw-anon-edit-warning">'.wfMsgWikiHTML( 'anoneditwarning' ).'</div>' ); } else { if( $this->isCssJsSubpage && $this->formtype != 'preview' ) { # Check the skin exists if( $this->isValidCssJsSubpage ) { - $wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' ) ); + $wgOut->addWikiMsg( 'usercssjsyoucanpreview' ); } else { - $wgOut->addWikiText( wfMsg( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); + $wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() ); } } } if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { # Show a warning if editing an interface message - $wgOut->addWikiText( wfMsg( 'editinginterface' ) ); + $wgOut->addWikiMsg( 'editinginterface' ); } elseif( $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? if( $this->mTitle->isSemiProtected() ) { - $notice = wfMsg( 'semiprotectedpagewarning' ); - if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) - $notice = ''; + $noticeMsg = 'semiprotectedpagewarning'; } else { - # Then it must be protected based on static groups (regular) - $notice = wfMsg( 'protectedpagewarning' ); + # Then it must be protected based on static groups (regular) + $noticeMsg = 'protectedpagewarning'; } - $wgOut->addWikiText( $notice ); + $wgOut->addWikiMsg( $noticeMsg ); } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); + $notice = "$1\n"; if ( count($cascadeSources) > 0 ) { # Explain, and list the titles responsible - $notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n"; foreach( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } } - $wgOut->addWikiText( $notice ); + $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) ); + } + if( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) != array() ){ + $wgOut->addWikiMsg( 'titleprotectedwarning' ); } if ( $this->kblength === false ) { $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); } if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { - $wgOut->addWikiText( wfMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ) ); + $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ); } elseif( $this->kblength > 29 ) { - $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) ); + $wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ); } #need to parse the preview early so that we know which templates are used, @@ -1056,12 +1148,12 @@ class EditPage { $q = 'action=submit'; #if ( "no" == $redirect ) { $q .= "&redirect=no"; } - $action = $this->mTitle->escapeLocalURL( $q ); + $action = $wgTitle->escapeLocalURL( $q ); $summary = wfMsg('summary'); $subject = wfMsg('subject'); - $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), + $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. @@ -1069,10 +1161,14 @@ class EditPage { htmlspecialchars( wfMsg( 'newwindow' ) ); global $wgRightsText; - $copywarn = "<div id=\"editpage-copywarn\">\n" . - wfMsg( $wgRightsText ? 'copyrightwarning' : 'copyrightwarning2', + if ( $wgRightsText ) { + $copywarnMsg = array( 'copyrightwarning', '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', - $wgRightsText ) . "\n</div>"; + $wgRightsText ); + } else { + $copywarnMsg = array( 'copyrightwarning2', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); + } if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { # prepare toolbar for edit buttons @@ -1151,7 +1247,7 @@ class EditPage { $recreate = ''; if ($this->deletedSinceEdit) { if ( 'save' != $this->formtype ) { - $wgOut->addWikiText( wfMsg('deletedwhileediting')); + $wgOut->addWikiMsg('deletedwhileediting'); } else { // Hide the toolbar and edit area, use can click preview to get it back // Add an confirmation checkbox and explanation. @@ -1200,6 +1296,7 @@ END $recreate {$commentsubject} {$subjectpreview} +{$this->editFormTextBeforeContent} <textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' cols='{$cols}'{$ew} $hidden> END @@ -1208,7 +1305,7 @@ END </textarea> " ); - $wgOut->addWikiText( $copywarn ); + $wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg ); $wgOut->addHTML( $this->editFormTextAfterWarn ); $wgOut->addHTML( " {$metadata} @@ -1226,7 +1323,7 @@ END </div><!-- editOptions -->"); $wgOut->addHtml( '<div class="mw-editTools">' ); - $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); + $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); $wgOut->addHtml( '</div>' ); $wgOut->addHTML( $this->editFormTextAfterTools ); @@ -1267,14 +1364,14 @@ END $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); if ( $this->isConflict ) { - $wgOut->addWikiText( '==' . wfMsg( "yourdiff" ) . '==' ); + $wgOut->wrapWikiMsg( '==$1==', "yourdiff" ); $de = new DifferenceEngine( $this->mTitle ); $de->setText( $this->textbox2, $this->textbox1 ); $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); - $wgOut->addWikiText( '==' . wfMsg( "yourtext" ) . '==' ); - $wgOut->addHTML( "<textarea tabindex=6 id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}' wrap='virtual'>" + $wgOut->wrapWikiMsg( '==$1==', "yourtext" ); + $wgOut->addHTML( "<textarea tabindex='6' id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}'>" . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); } $wgOut->addHTML( $this->editFormTextBottom ); @@ -1333,8 +1430,7 @@ END htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) . '"></script>' . "\n" ); $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); - return "return !livePreview(" . - "getElementById('wikiPreview')," . + return "return !lpDoPreview(" . "editform.wpTextbox1.value," . '"' . $liveAction . '"' . ")"; } @@ -1382,17 +1478,12 @@ END if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { - $msg = 'token_suffix_mismatch'; + $note = wfMsg( 'token_suffix_mismatch' ); } else { - $msg = 'session_fail_preview'; + $note = wfMsg( 'session_fail_preview' ); } } else { - $msg = 'previewnote'; - } - $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . - "<div class='previewnote'>" . $wgOut->parse( wfMsg( $msg ) ) . "</div>\n"; - if ( $this->isConflict ) { - $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + $note = wfMsg( 'previewnote' ); } $parserOptions = ParserOptions::newFromUser( $wgUser ); @@ -1410,16 +1501,15 @@ END # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here if ( $this->isCssJsSubpage ) { - if(preg_match("/\\.css$/", $wgTitle->getText() ) ) { + if(preg_match("/\\.css$/", $this->mTitle->getText() ) ) { $previewtext = wfMsg('usercsspreview'); - } else if(preg_match("/\\.js$/", $wgTitle->getText() ) ) { + } else if(preg_match("/\\.js$/", $this->mTitle->getText() ) ) { $previewtext = wfMsg('userjspreview'); } $parserOptions->setTidy(true); - $parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions ); + $parserOutput = $wgParser->parse( $previewtext , $this->mTitle, $parserOptions ); $wgOut->addHTML( $parserOutput->mText ); - wfProfileOut( $fname ); - return $previewhead; + $previewHTML = ''; } else { $toparse = $this->textbox1; @@ -1431,22 +1521,38 @@ END if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ; $parserOptions->setTidy(true); + $parserOptions->enableLimitReport(); $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n", - $wgTitle, $parserOptions ); + $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->getText(); $wgOut->addParserOutputNoText( $parserOutput ); # ParserOutput might have altered the page title, so reset it - $wgOut->setPageTitle( wfMsg( 'editing', $this->mTitle->getPrefixedText() ) ); + # Also, use the title defined by DISPLAYTITLE magic word when present + if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) { + $wgOut->setPageTitle( wfMsg( 'editing', $dt ) ); + } else { + $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) ); + } foreach ( $parserOutput->getTemplates() as $ns => $template) foreach ( array_keys( $template ) as $dbk) $this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk); - wfProfileOut( $fname ); - return $previewhead . $previewHTML; + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + } } + + $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . + "<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n"; + if ( $this->isConflict ) { + $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + } + + wfProfileOut( $fname ); + return $previewhead . $previewHTML; } /** @@ -1471,7 +1577,7 @@ END $cols = $wgUser->getOption( 'cols' ); $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); $wgOut->addHtml( '<hr />' ); - $wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) ); + $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ); $wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) ); } } @@ -1480,34 +1586,18 @@ END * Produce the stock "please login to edit pages" page */ function userNotLoggedInPage() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgTitle; $skin = $wgUser->getSkin(); $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); - $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); - $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); - } - - /** - * Creates a basic error page which informs the user that - * they have to validate their email address before being - * allowed to edit. - */ - function userNotConfirmedPage() { - global $wgOut; - - $wgOut->setPageTitle( wfMsg( 'confirmedittitle' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - - $wgOut->addWikiText( wfMsg( 'confirmedittext' ) ); - $wgOut->returnToMain( false ); + $wgOut->returnToMain( false, $wgTitle ); } /** @@ -1515,14 +1605,14 @@ END * they have attempted to edit a nonexistant section. */ function noSuchSectionPage() { - global $wgOut; + global $wgOut, $wgTitle; $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) ); - $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); + $wgOut->addWikiMsg( 'nosuchsectiontext', $this->section ); + $wgOut->returnToMain( false, $wgTitle ); } /** @@ -1531,17 +1621,19 @@ END * @param $match Text which triggered one or more filters */ function spamPage( $match = false ) { - global $wgOut; + global $wgOut, $wgTitle; $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); + $wgOut->addHtml( '<div id="spamprotected">' ); + $wgOut->addWikiMsg( 'spamprotectiontext' ); if ( $match ) - $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); + $wgOut->addWikiMsg( 'spamprotectionmatch',wfEscapeWikiText( $match ) ); + $wgOut->addHtml( '</div>' ); - $wgOut->returnToMain( false ); + $wgOut->returnToMain( false, $wgTitle ); } /** @@ -1556,7 +1648,7 @@ END // This is the revision the editor started from $baseRevision = Revision::loadFromTimestamp( - $db, $this->mArticle->mTitle, $this->edittime ); + $db, $this->mTitle, $this->edittime ); if( is_null( $baseRevision ) ) { wfProfileOut( $fname ); return false; @@ -1565,7 +1657,7 @@ END // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( - $db, $this->mArticle->mTitle ); + $db, $this->mTitle ); if( is_null( $currentRevision ) ) { wfProfileOut( $fname ); return false; @@ -1606,25 +1698,22 @@ END } /** + * @deprecated use $wgParser->stripSectionName() + */ + function pseudoParseSectionAnchor( $text ) { + global $wgParser; + return $wgParser->stripSectionName( $text ); + } + + /** * Format an anchor fragment as it would appear for a given section name * @param string $text * @return string * @private */ function sectionAnchor( $text ) { - $headline = Sanitizer::decodeCharReferences( $text ); - # strip out HTML - $headline = preg_replace( '/<.*?' . '>/', '', $headline ); - $headline = trim( $headline ); - $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - return str_replace( - array_keys( $replacearray ), - array_values( $replacearray ), - $sectionanchor ); + global $wgParser; + return $wgParser->guessSectionNameFromWikiText( $text ); } /** @@ -1649,16 +1738,16 @@ END $toolarray = array( array( 'image' => 'button_bold.png', 'id' => 'mw-editbutton-bold', - 'open' => '\\\'\\\'\\\'', - 'close' => '\\\'\\\'\\\'', + 'open' => '\'\'\'', + 'close' => '\'\'\'', 'sample'=> wfMsg('bold_sample'), 'tip' => wfMsg('bold_tip'), 'key' => 'B' ), array( 'image' => 'button_italic.png', 'id' => 'mw-editbutton-italic', - 'open' => '\\\'\\\'', - 'close' => '\\\'\\\'', + 'open' => '\'\'', + 'close' => '\'\'', 'sample'=> wfMsg('italic_sample'), 'tip' => wfMsg('italic_tip'), 'key' => 'I' @@ -1681,8 +1770,8 @@ END ), array( 'image' => 'button_headline.png', 'id' => 'mw-editbutton-headline', - 'open' => "\\n== ", - 'close' => " ==\\n", + 'open' => "\n== ", + 'close' => " ==\n", 'sample'=> wfMsg('headline_sample'), 'tip' => wfMsg('headline_tip'), 'key' => 'H' @@ -1706,7 +1795,7 @@ END array( 'image' => 'button_math.png', 'id' => 'mw-editbutton-math', 'open' => "<math>", - 'close' => "<\\/math>", + 'close' => "</math>", 'sample'=> wfMsg('math_sample'), 'tip' => wfMsg('math_tip'), 'key' => 'C' @@ -1714,7 +1803,7 @@ END array( 'image' => 'button_nowiki.png', 'id' => 'mw-editbutton-nowiki', 'open' => "<nowiki>", - 'close' => "<\\/nowiki>", + 'close' => "</nowiki>", 'sample'=> wfMsg('nowiki_sample'), 'tip' => wfMsg('nowiki_tip'), 'key' => 'N' @@ -1729,7 +1818,7 @@ END ), array( 'image' => 'button_hr.png', 'id' => 'mw-editbutton-hr', - 'open' => "\\n----\\n", + 'open' => "\n----\n", 'close' => '', 'sample'=> '', 'tip' => wfMsg('hr_tip'), @@ -1740,22 +1829,22 @@ END $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; foreach($toolarray as $tool) { - - $cssId = $tool['id']; - $image=$wgStylePath.'/common/images/'.$tool['image']; - $open=$tool['open']; - $close=$tool['close']; - $sample = wfEscapeJsString( $tool['sample'] ); - - // Note that we use the tip both for the ALT tag and the TITLE tag of the image. - // Older browsers show a "speedtip" type message only for ALT. - // Ideally these should be different, realistically they - // probably don't need to be. - $tip = wfEscapeJsString( $tool['tip'] ); - - #$key = $tool["key"]; - - $toolbar.="addButton('$image','$tip','$open','$close','$sample','$cssId');\n"; + $params = array( + $image = $wgStylePath.'/common/images/'.$tool['image'], + // Note that we use the tip both for the ALT tag and the TITLE tag of the image. + // Older browsers show a "speedtip" type message only for ALT. + // Ideally these should be different, realistically they + // probably don't need to be. + $tip = $tool['tip'], + $open = $tool['open'], + $close = $tool['close'], + $sample = $tool['sample'], + $cssId = $tool['id'], + ); + + $paramList = implode( ',', + array_map( array( 'Xml', 'encodeJsVar' ), $params ) ); + $toolbar.="addButton($paramList);\n"; } $toolbar.="/*]]>*/\n</script>"; @@ -1880,7 +1969,8 @@ END 'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']', ); $buttons['diff'] = wfElement('input', $temp, ''); - + + wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) ); return $buttons; } @@ -1902,12 +1992,15 @@ END header( 'Content-type: text/xml; charset=utf-8' ); header( 'Cache-control: no-cache' ); + $previewText = $this->getPreviewText(); + #$categories = $skin->getCategoryLinks(); + $s = '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" . - Xml::openElement( 'livepreview' ) . - Xml::element( 'preview', null, $this->getPreviewText() ) . - Xml::element( 'br', array( 'style' => 'clear: both;' ) ) . - Xml::closeElement( 'livepreview' ); + Xml::tags( 'livepreview', null, + Xml::element( 'preview', null, $previewText ) + #. Xml::element( 'category', null, $categories ) + ); echo $s; } @@ -2057,7 +2150,7 @@ END function noCreatePermission() { global $wgOut; $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); - $wgOut->addWikiText( wfMsg( 'nocreatetext' ) ); + $wgOut->addWikiMsg( 'nocreatetext' ); } /** @@ -2067,7 +2160,7 @@ END * @param OutputPage $out */ private function showDeletionLog( $out ) { - $title = $this->mArticle->getTitle(); + $title = $this->mTitle; $reader = new LogReader( new FauxRequest( array( @@ -2078,13 +2171,80 @@ END ); if( $reader->hasRows() ) { $out->addHtml( '<div id="mw-recreate-deleted-warn">' ); - $out->addWikiText( wfMsg( 'recreate-deleted-warn' ) ); + $out->addWikiMsg( 'recreate-deleted-warn' ); $viewer = new LogViewer( $reader ); $viewer->showList( $out ); - $out->addHtml( '</div>' ); - } + $out->addHtml( '</div>' ); + } } - -} + /** + * Attempt submission + * @return bool false if output is done, true if the rest of the form should be displayed + */ + function attemptSave() { + global $wgUser, $wgOut, $wgTitle, $wgRequest; + + $resultDetails = false; + $value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) ); + + if( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { + $this->didSave = true; + } + + switch ($value) { + case self::AS_HOOK_ERROR_EXPECTED: + case self::AS_CONTENT_TOO_BIG: + case self::AS_ARTICLE_WAS_DELETED: + case self::AS_CONFLICT_DETECTED: + case self::AS_SUMMARY_NEEDED: + case self::AS_TEXTBOX_EMPTY: + case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: + case self::AS_END: + return true; + + case self::AS_HOOK_ERROR: + case self::AS_FILTERING: + case self::AS_SUCCESS_NEW_ARTICLE: + case self::AS_SUCCESS_UPDATE: + return false; + case self::AS_SPAM_ERROR: + $this->spamPage ( $resultDetails['spam'] ); + return false; + + case self::AS_BLOCKED_PAGE_FOR_USER: + $this->blockedPage(); + return false; + + case self::AS_IMAGE_REDIRECT_ANON: + $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + return false; + + case self::AS_READ_ONLY_PAGE_ANON: + $this->userNotLoggedInPage(); + return false; + + case self::AS_READ_ONLY_PAGE_LOGGED: + case self::AS_READ_ONLY_PAGE: + $wgOut->readOnlyPage(); + return false; + + case self::AS_RATE_LIMITED: + $wgOut->rateLimited(); + return false; + + case self::AS_NO_CREATE_PERMISSION; + $this->noCreatePermission(); + return; + + case self::AS_BLANK_ARTICLE: + $wgOut->redirect( $wgTitle->getFullURL() ); + return false; + + case self::AS_IMAGE_REDIRECT_LOGGED: + $wgOut->permissionRequired( 'upload' ); + return false; + } + } +} diff --git a/includes/Exception.php b/includes/Exception.php index 02819cc9..2fd54352 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -16,6 +16,26 @@ class MWException extends Exception return is_object( $wgLang ); } + function runHooks( $name, $args = array() ) { + global $wgExceptionHooks; + if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) + return; // Just silently ignore + if( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) + return; + $hooks = $wgExceptionHooks[ $name ]; + $callargs = array_merge( array( $this ), $args ); + + foreach( $hooks as $hook ) { + if( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { //'function' or array( 'class', hook' ) + $result = call_user_func_array( $hook, $callargs ); + } else { + $result = null; + } + if( is_string( $result ) ) + return $result; + } + } + /** Get a message from i18n */ function msg( $key, $fallback /*[, params...] */ ) { $args = array_slice( func_get_args(), 2 ); @@ -35,7 +55,8 @@ class MWException extends Exception "</p>\n"; } else { return "<p>Set <b><tt>\$wgShowExceptionDetails = true;</tt></b> " . - "in LocalSettings.php to show detailed debugging information.</p>"; + "at the bottom of LocalSettings.php to show detailed " . + "debugging information.</p>"; } } @@ -82,27 +103,29 @@ class MWException extends Exception $wgOut->enableClientCache( false ); $wgOut->redirect( '' ); $wgOut->clearHTML(); - $wgOut->addHTML( $this->getHTML() ); + if( $hookResult = $this->runHooks( get_class( $this ) ) ) { + $wgOut->addHTML( $hookResult ); + } else { + $wgOut->addHTML( $this->getHTML() ); + } $wgOut->output(); } else { + if( $hookResult = $this->runHooks( get_class( $this ) . "Raw" ) ) { + die( $hookResult ); + } echo $this->htmlHeader(); echo $this->getHTML(); echo $this->htmlFooter(); } } - /** Print the exception report using text */ - function reportText() { - echo $this->getText(); - } - /* Output a report about the exception and takes care of formatting. * It will be either HTML or plain text based on $wgCommandLineMode. */ function report() { global $wgCommandLineMode; if ( $wgCommandLineMode ) { - $this->reportText(); + fwrite( STDERR, $this->getText() ); } else { $log = $this->getLogMessage(); if ( $log ) { @@ -135,7 +158,6 @@ class MWException extends Exception function htmlFooter() { echo "</body></html>"; } - } /** @@ -199,7 +221,7 @@ function wfReportException( Exception $e ) { $e2->__toString() . "\n"; if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - echo $message; + fwrite( STDERR, $message ); } else { echo nl2br( htmlspecialchars( $message ) ). "\n"; } diff --git a/includes/Export.php b/includes/Export.php index c3ef9451..69d88fc6 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -111,7 +111,7 @@ class WikiExporter { function pageByTitle( $title ) { return $this->dumpFrom( 'page_namespace=' . $title->getNamespace() . - ' AND page_title=' . $this->db->addQuotes( $title->getDbKey() ) ); + ' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) ); } function pageByName( $name ) { diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index f3fc22e3..f5ce5b9d 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -34,6 +34,7 @@ class ExternalEdit { $name=$this->mTitle->getText(); $pos=strrpos($name,".")+1; header ( "Content-type: application/x-external-editor; charset=".$this->mCharset ); + header( "Cache-control: no-cache" ); # $type can be "Edit text", "Edit file" or "Diff text" at the moment # See the protocol specifications at [[m:Help:External editors/Tech]] for @@ -47,12 +48,7 @@ class ExternalEdit { } elseif($this->mMode=="file") { $type="Edit file"; $image = wfLocalFile( $this->mTitle ); - $img_url = $image->getURL(); - if(strpos($img_url,"://")) { - $url = $img_url; - } else { - $url = $wgServer . $img_url; - } + $url = $image->getFullURL(); $extension=substr($name, $pos); } $special=$wgLang->getNsText(NS_SPECIAL); diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index 5efc6e25..79937b85 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -1,18 +1,15 @@ <?php /** - * - * * Constructor class for data kept in external repositories * * External repositories might be populated by maintenance/async * scripts, thus partial moving of data may be possible, as well * as possibility to have any storage format (i.e. for archives) - * */ class ExternalStore { /* Fetch data from given URL */ - function fetchFromURL($url) { + static function fetchFromURL($url) { global $wgExternalStores; if (!$wgExternalStores) @@ -32,7 +29,7 @@ class ExternalStore { /** * Get an external store object of the given type */ - function &getStoreObject( $proto ) { + static function &getStoreObject( $proto ) { global $wgExternalStores; if (!$wgExternalStores) return false; @@ -55,7 +52,7 @@ class ExternalStore { * class itself as a parameter. * Returns the URL of the stored data item, or false on error */ - function insert( $url, $data ) { + static function insert( $url, $data ) { list( $proto, $params ) = explode( '://', $url, 2 ); $store =& ExternalStore::getStoreObject( $proto ); if ( $store === false ) { diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php index cff6c4d4..ef907df5 100644 --- a/includes/ExternalStoreHttp.php +++ b/includes/ExternalStoreHttp.php @@ -9,9 +9,9 @@ class ExternalStoreHttp { /* Fetch data from given URL */ function fetchFromURL($url) { - ini_set( "allow_url_fopen", true ); - $ret = file_get_contents( $url ); - ini_set( "allow_url_fopen", false ); + ini_set( "allow_url_fopen", true ); + $ret = file_get_contents( $url ); + ini_set( "allow_url_fopen", false ); return $ret; } diff --git a/includes/Feed.php b/includes/Feed.php index ed4343c3..309b29bd 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -41,6 +41,7 @@ class FeedItem { /**#@+ * @todo document + * @param $Url URL uniquely designating the item. */ function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { $this->Title = $Title; @@ -145,12 +146,13 @@ class ChannelFeed extends FeedItem { * @private */ function outXmlHeader() { - global $wgServer, $wgStylePath, $wgStyleVersion; + global $wgStylePath, $wgStyleVersion; $this->httpHeaders(); echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"; echo '<?xml-stylesheet type="text/css" href="' . - htmlspecialchars( "$wgServer$wgStylePath/common/feed.css?$wgStyleVersion" ) . '"?' . ">\n"; + htmlspecialchars( wfExpandUrl( "$wgStylePath/common/feed.css?$wgStyleVersion" ) ) . + '"?' . ">\n"; } } diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index ee165cd1..71e2c1ae 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -39,7 +39,7 @@ class FileDeleteForm { $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); return; } elseif( !$wgUser->isAllowed( 'delete' ) ) { - $wgOut->permissionError( 'delete' ); + $wgOut->permissionRequired( 'delete' ); return; } elseif( $wgUser->isBlocked() ) { $wgOut->blockedPage(); @@ -63,25 +63,37 @@ class FileDeleteForm { // Perform the deletion if appropriate if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { - $comment = $wgRequest->getText( 'wpReason' ); + $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' ); + $this->DeleteReason = $wgRequest->getText( 'wpReason' ); + $reason = $this->DeleteReasonList; + if ( $reason != 'other' && $this->DeleteReason != '') { + // Entry from drop down menu + additional comment + $reason .= ': ' . $this->DeleteReason; + } elseif ( $reason == 'other' ) { + $reason = $this->DeleteReason; + } if( $this->oldimage ) { - $status = $this->file->deleteOld( $this->oldimage, $comment ); + $status = $this->file->deleteOld( $this->oldimage, $reason ); if( $status->ok ) { // Need to do a log item $log = new LogPage( 'delete' ); - $log->addEntry( 'delete', $this->title, wfMsg( 'deletedrevision' , $this->oldimage ) ); + $logComment = wfMsgForContent( 'deletedrevision', $this->oldimage ); + if( trim( $reason ) != '' ) + $logComment .= ": {$reason}"; + $log->addEntry( 'delete', $this->title, $logComment ); } } else { - $status = $this->file->delete( $comment ); + $status = $this->file->delete( $reason ); if( $status->ok ) { // Need to delete the associated article $article = new Article( $this->title ); - $article->doDeleteArticle( $comment ); + $article->doDeleteArticle( $reason ); } } if( !$status->isGood() ) $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); if( $status->ok ) { + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) ); // Return to the main page if we just deleted all versions of the // file, otherwise go back to the description page @@ -93,27 +105,51 @@ class FileDeleteForm { $this->showForm(); $this->showLogEntries(); } - + /** * Show the confirmation form */ private function showForm() { - global $wgOut, $wgUser, $wgRequest; - - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ); - $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ); - $form .= '<fieldset><legend>' . wfMsgHtml( 'filedelete-legend' ) . '</legend>'; - $form .= $this->prepareMessage( 'filedelete-intro' ); - - $form .= '<p>' . Xml::inputLabel( wfMsg( 'filedelete-comment' ), 'wpReason', 'wpReason', - 60, $wgRequest->getText( 'wpReason' ) ) . '</p>'; - $form .= '<p>' . Xml::submitButton( wfMsg( 'filedelete-submit' ) ) . '</p>'; - $form .= '</fieldset>'; - $form .= '</form>'; - + global $wgOut, $wgUser, $wgRequest, $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ) . + $this->prepareMessage( 'filedelete-intro' ) . + Xml::openElement( 'table' ) . + "<tr> + <td align='$align'>" . + Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) . + "</td> + <td>" . + Xml::listDropDown( 'wpDeleteReasonList', + wfMsgForContent( 'filedelete-reason-dropdown' ), + wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) . + "</td> + </tr> + <tr> + <td align='$align'>" . + Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) . + "</td> + <td>" . + Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td>" . + Xml::submitButton( wfMsg( 'filedelete-submit' ), array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '3' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + $wgOut->addHtml( $form ); } - + /** * Show deletion log fragments pertaining to the current file */ @@ -142,16 +178,16 @@ class FileDeleteForm { * @return string */ private function prepareMessage( $message ) { - global $wgLang, $wgServer; + global $wgLang; if( $this->oldimage ) { + $url = $this->file->getArchiveUrl( $this->oldimage ); return wfMsgExt( - "{$message}-old", + "{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' 'parse', $this->title->getText(), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), - $wgServer . $this->file->getArchiveUrl( $this->oldimage ) - ); + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ); } else { return wfMsgExt( $message, @@ -217,4 +253,4 @@ class FileDeleteForm { return $this->oldfile->getTimestamp(); } -}
\ No newline at end of file +} diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php index 55f21fff..f335d024 100644 --- a/includes/FileRevertForm.php +++ b/includes/FileRevertForm.php @@ -28,7 +28,7 @@ class FileRevertForm { * pending authentication, confirmation, etc. */ public function execute() { - global $wgOut, $wgRequest, $wgUser, $wgLang, $wgServer; + global $wgOut, $wgRequest, $wgUser, $wgLang; $this->setHeaders(); if( wfReadOnly() ) { @@ -71,7 +71,7 @@ class FileRevertForm { $wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), - $wgServer . $this->file->getArchiveUrl( $this->oldimage ) ) ); + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ) ); $wgOut->returnToMain( false, $this->title ); } else { $wgOut->addWikiText( $status->getWikiText() ); @@ -87,14 +87,15 @@ class FileRevertForm { * Show the confirmation form */ private function showForm() { - global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang, $wgServer; + global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang; $timestamp = $this->getTimestamp(); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ); $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ); $form .= '<fieldset><legend>' . wfMsgHtml( 'filerevert-legend' ) . '</legend>'; $form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(), - $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), $wgServer . $this->file->getArchiveUrl( $this->oldimage ) ); + $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ); $form .= '<p>' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment', 60, wfMsgForContent( 'filerevert-defaultcomment', $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '</p>'; diff --git a/includes/FileStore.php b/includes/FileStore.php index 1554d66e..a547e7e4 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -162,7 +162,7 @@ class FileStore { function delete( $key ) { $destPath = $this->filePath( $key ); if( false === $destPath ) { - throw new FSExcepton( "file store does not contain file '$key'" ); + throw new FSException( "file store does not contain file '$key'" ); } else { return FileStore::deleteFile( $destPath ); } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 67cc1f39..2b9543b4 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -8,20 +8,6 @@ if ( !defined( 'MEDIAWIKI' ) ) { * Global functions used everywhere */ -/** - * Some globals and requires needed - */ - -/** Total number of articles */ -$wgNumberOfArticles = -1; # Unset - -/** Total number of views */ -$wgTotalViews = -1; - -/** Total number of edits */ -$wgTotalEdits = -1; - - require_once dirname(__FILE__) . '/LogPage.php'; require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php'; require_once dirname(__FILE__) . '/XmlFunctions.php'; @@ -112,11 +98,6 @@ function wfClone( $object ) { } /** - * Where as we got a random seed - */ -$wgRandomSeeded = false; - -/** * Seed Mersenne Twister * No-op for compatibility; only necessary in PHP < 4.2.0 */ @@ -308,11 +289,6 @@ function wfReadOnly() { * Use wfMsgForContent() instead if the message should NOT * change depending on the user preferences. * - * Note that the message may contain HTML, and is therefore - * not safe for insertion anywhere. Some functions such as - * addWikiText will do the escaping for you. Use wfMsgHtml() - * if you need an escaped message. - * * @param $key String: lookup key for the message, usually * defined in languages/Language.php * @@ -416,11 +392,10 @@ function wfMsgNoDBForContent( $key ) { * @return String: the requested message. */ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) { - $fname = 'wfMsgReal'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $message = wfMsgGetKey( $key, $useDB, $forContent, $transform ); $message = wfMsgReplaceArgs( $message, $args ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $message; } @@ -447,24 +422,12 @@ function wfMsgWeirdKey ( $key ) { function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { global $wgParser, $wgContLang, $wgMessageCache, $wgLang; - /* <Vyznev> btw, is all that code in wfMsgGetKey() that check - * if the message cache exists of not really necessary, or is - * it just paranoia? - * <TimStarling> Vyznev: it's probably not necessary - * <TimStarling> I think I wrote it in an attempt to report DB - * connection errors properly - * <TimStarling> but eventually we gave up on using the - * message cache for that and just hard-coded the strings - * <TimStarling> it may have other uses, it's not mere paranoia - */ - - if ( is_object( $wgMessageCache ) ) - $transstat = $wgMessageCache->getTransform(); - + # If $wgMessageCache isn't initialised yet, try to return something sensible. if( is_object( $wgMessageCache ) ) { - if ( ! $transform ) - $wgMessageCache->disableTransform(); $message = $wgMessageCache->get( $key, $useDB, $forContent ); + if ( $transform ) { + $message = $wgMessageCache->transform( $message ); + } } else { if( $forContent ) { $lang = &$wgContLang; @@ -476,22 +439,13 @@ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { # ISSUE: Should we try to handle "message/lang" here too? $key = str_replace( ' ' , '_' , $wgContLang->lcfirst( $key ) ); - wfSuppressWarnings(); if( is_object( $lang ) ) { $message = $lang->getMessage( $key ); } else { $message = false; } - wfRestoreWarnings(); - - if ( $transform && strstr( $message, '{{' ) !== false ) { - $message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() ); - } } - if ( is_object( $wgMessageCache ) && ! $transform ) - $wgMessageCache->setTransform( $transstat ); - return $message; } @@ -511,15 +465,13 @@ function wfMsgReplaceArgs( $message, $args ) { // Replace arguments if ( count( $args ) ) { if ( is_array( $args[0] ) ) { - foreach ( $args[0] as $key => $val ) { - $message = str_replace( '$' . $key, $val, $message ); - } - } else { - foreach( $args as $n => $param ) { - $replacementKeys['$' . ($n + 1)] = $param; - } - $message = strtr( $message, $replacementKeys ); + $args = array_values( $args[0] ); + } + $replacementKeys = array(); + foreach( $args as $n => $param ) { + $replacementKeys['$' . ($n + 1)] = $param; } + $message = strtr( $message, $replacementKeys ); } return $message; @@ -566,9 +518,12 @@ function wfMsgWikiHtml( $key ) { * @param array $options Processing rules: * <i>parse</i>: parses wikitext to html * <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy - * <i>escape</i>: filters message trough htmlspecialchars + * <i>escape</i>: filters message through htmlspecialchars + * <i>escapenoentities</i>: same, but allows entity references like through * <i>replaceafter</i>: parameters are substituted after parsing or escaping * <i>parsemag</i>: transform the message using magic phrases + * <i>content</i>: fetch message for content language instead of interface + * Behavior for conflicting options (e.g., parse+parseinline) is undefined. */ function wfMsgExt( $key, $options ) { global $wgOut, $wgParser; @@ -581,29 +536,38 @@ function wfMsgExt( $key, $options ) { $options = array($options); } - $string = wfMsgGetKey( $key, true, false, false ); + $forContent = false; + if( in_array('content', $options) ) { + $forContent = true; + } + + $string = wfMsgGetKey( $key, /*DB*/true, $forContent, /*Transform*/false ); if( !in_array('replaceafter', $options) ) { $string = wfMsgReplaceArgs( $string, $args ); } if( in_array('parse', $options) ) { - $string = $wgOut->parse( $string, true, true ); + $string = $wgOut->parse( $string, true, !$forContent ); } elseif ( in_array('parseinline', $options) ) { - $string = $wgOut->parse( $string, true, true ); + $string = $wgOut->parse( $string, true, !$forContent ); $m = array(); - if( preg_match( '/^<p>(.*)\n?<\/p>$/sU', $string, $m ) ) { + if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) { $string = $m[1]; } } elseif ( in_array('parsemag', $options) ) { global $wgMessageCache; if ( isset( $wgMessageCache ) ) { - $string = $wgMessageCache->transform( $string ); + $string = $wgMessageCache->transform( $string, !$forContent ); } } if ( in_array('escape', $options) ) { $string = htmlspecialchars ( $string ); + } elseif ( in_array( 'escapenoentities', $options ) ) { + $string = htmlspecialchars( $string ); + $string = str_replace( '&', '&', $string ); + $string = Sanitizer::normalizeCharReferences( $string ); } if( in_array('replaceafter', $options) ) { @@ -903,8 +867,8 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { */ function wfEscapeWikiText( $text ) { $text = str_replace( - array( '[', '|', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), - array( '[', '|', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), + array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), + array( '[', '|', ']', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), htmlspecialchars($text) ); return $text; } @@ -1010,6 +974,21 @@ function wfAppendQuery( $url, $query ) { } /** + * Expand a potentially local URL to a fully-qualified URL. + * Assumes $wgServer is correct. :) + * @param string $url, either fully-qualified or a local path + query + * @return string Fully-qualified URL + */ +function wfExpandUrl( $url ) { + if( substr( $url, 0, 1 ) == '/' ) { + global $wgServer; + return $wgServer . $url; + } else { + return $url; + } +} + +/** * This is obsolete, use SquidUpdate::purge() * @deprecated */ @@ -1673,13 +1652,29 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) { /** * Increment a statistics counter */ - function wfIncrStats( $key ) { - global $wgMemc; - $key = wfMemcKey( 'stats', $key ); - if ( is_null( $wgMemc->incr( $key ) ) ) { - $wgMemc->add( $key, 1 ); - } - } +function wfIncrStats( $key ) { + global $wgStatsMethod; + + if( $wgStatsMethod == 'udp' ) { + global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname; + static $socket; + if (!$socket) { + $socket=socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $statline="stats/{$wgDBname} - 1 1 1 1 1 -total\n"; + socket_sendto($socket,$statline,strlen($statline),0,$wgUDPProfilerHost,$wgUDPProfilerPort); + } + $statline="stats/{$wgDBname} - 1 1 1 1 1 {$key}\n"; + @socket_sendto($socket,$statline,strlen($statline),0,$wgUDPProfilerHost,$wgUDPProfilerPort); + } elseif( $wgStatsMethod == 'cache' ) { + global $wgMemc; + $key = wfMemcKey( 'stats', $key ); + if ( is_null( $wgMemc->incr( $key ) ) ) { + $wgMemc->add( $key, 1 ); + } + } else { + // Disabled + } +} /** * @param mixed $nr The number to format @@ -1773,6 +1768,38 @@ function wfUrlProtocols() { } /** + * Safety wrapper around ini_get() for boolean settings. + * The values returned from ini_get() are pre-normalized for settings + * set via php.ini or php_flag/php_admin_flag... but *not* + * for those set via php_value/php_admin_value. + * + * It's fairly common for people to use php_value instead of php_flag, + * which can leave you with an 'off' setting giving a false positive + * for code that just takes the ini_get() return value as a boolean. + * + * To make things extra interesting, setting via php_value accepts + * "true" and "yes" as true, but php.ini and php_flag consider them false. :) + * Unrecognized values go false... again opposite PHP's own coercion + * from string to bool. + * + * Luckily, 'properly' set settings will always come back as '0' or '1', + * so we only have to worry about them and the 'improper' settings. + * + * I frickin' hate PHP... :P + * + * @param string $setting + * @return bool + */ +function wfIniGetBool( $setting ) { + $val = ini_get( $setting ); + // 'on' and 'true' can't have whitespace around them, but '1' can. + return strtolower( $val ) == 'on' + || strtolower( $val ) == 'true' + || strtolower( $val ) == 'yes' + || preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function +} + +/** * Execute a shell command, with time and memory limits mirrored from the PHP * configuration if supported. * @param $cmd Command line, properly escaped for shell. @@ -1783,7 +1810,7 @@ function wfUrlProtocols() { function wfShellExec( $cmd, &$retval=null ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize; - if( ini_get( 'safe_mode' ) ) { + if( wfIniGetBool( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); $retval = 1; return "Unable to run external programs in safe mode."; @@ -1807,10 +1834,12 @@ function wfShellExec( $cmd, &$retval=null ) { } wfDebug( "wfShellExec: $cmd\n" ); - $output = array(); $retval = 1; // error by default? - exec( $cmd, $output, $retval ); // returns the last line of output. - return implode( "\n", $output ); + ob_start(); + passthru( $cmd, $retval ); + $output = ob_get_contents(); + ob_end_clean(); + return $output; } @@ -1901,8 +1930,18 @@ function wfRelativePath( $path, $from ) { $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); + // Trim trailing slashes -- fix for drive root + $path = rtrim( $path, DIRECTORY_SEPARATOR ); + $from = rtrim( $from, DIRECTORY_SEPARATOR ); + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); $against = explode( DIRECTORY_SEPARATOR, $from ); + + if( $pieces[0] !== $against[0] ) { + // Non-matching Windows drive letters? + // Return a full path. + return $path; + } // Trim off common prefix while( count( $pieces ) && count( $against ) @@ -1923,12 +1962,34 @@ function wfRelativePath( $path, $from ) { } /** + * array_merge() does awful things with "numeric" indexes, including + * string indexes when happen to look like integers. When we want + * to merge arrays with arbitrary string indexes, we don't want our + * arrays to be randomly corrupted just because some of them consist + * of numbers. + * + * Fuck you, PHP. Fuck you in the ear! + * + * @param array $array1, [$array2, [...]] + * @return array + */ +function wfArrayMerge( $array1/* ... */ ) { + $out = $array1; + for( $i = 1; $i < func_num_args(); $i++ ) { + foreach( func_get_arg( $i ) as $key => $value ) { + $out[$key] = $value; + } + } + return $out; +} + +/** * Make a URL index, appropriate for the el_index field of externallinks. */ function wfMakeUrlIndex( $url ) { global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php - $bits = parse_url( $url ); wfSuppressWarnings(); + $bits = parse_url( $url ); wfRestoreWarnings(); if ( !$bits ) { return false; @@ -1952,13 +2013,19 @@ function wfMakeUrlIndex( $url ) { // Reverse the labels in the hostname, convert to lower case // For emails reverse domainpart only if ( $bits['scheme'] == 'mailto' ) { - $mailparts = explode( '@', $bits['host'] ); - $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + $mailparts = explode( '@', $bits['host'], 2 ); + if ( count($mailparts) === 2 ) { + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + } else { + // No domain specified, don't mangle it + $domainpart = ''; + } $reversedHost = $domainpart . '@' . $mailparts[0]; } else { $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); } // Add an extra dot to the end + // Why? Is it in wrong place in mailto links? if ( substr( $reversedHost, -1, 1 ) !== '.' ) { $reversedHost .= '.'; } @@ -2163,11 +2230,7 @@ function wfGetPrecompiledData( $name ) { function wfGetCaller( $level = 2 ) { $backtrace = wfDebugBacktrace(); if ( isset( $backtrace[$level] ) ) { - if ( isset( $backtrace[$level]['class'] ) ) { - $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; - } else { - $caller = $backtrace[$level]['function']; - } + return wfFormatStackFrame($backtrace[$level]); } else { $caller = 'unknown'; } @@ -2176,13 +2239,14 @@ function wfGetCaller( $level = 2 ) { /** Return a string consisting all callers in stack, somewhat useful sometimes for profiling specific points */ function wfGetAllCallers() { - return implode('/', array_map( - create_function('$frame',' - return isset( $frame["class"] )? - $frame["class"]."::".$frame["function"]: - $frame["function"]; - '), - array_reverse(wfDebugBacktrace()))); + return implode('/', array_map('wfFormatStackFrame',array_reverse(wfDebugBacktrace()))); +} + +/** Return a string representation of frame */ +function wfFormatStackFrame($frame) { + return isset( $frame["class"] )? + $frame["class"]."::".$frame["function"]: + $frame["function"]; } /** @@ -2247,7 +2311,7 @@ function &wfGetDB( $db = DB_LAST, $groups = array() ) { * @param mixed $title Title object or string. May be interwiki. * @param mixed $time Requested time for an archived image, or false for the * current version. An image object will be returned which - * existed at or before the specified time. + * existed at the specified time. * @return File, or false if the file does not exist */ function wfFindFile( $title, $time = false ) { @@ -2320,4 +2384,24 @@ function wfGetNull() { return wfIsWindows() ? 'NUL' : '/dev/null'; -}
\ No newline at end of file +} + +/** + * Displays a maxlag error + * + * @param string $host Server that lags the most + * @param int $lag Maxlag (actual) + * @param int $maxLag Maxlag (requested) + */ +function wfMaxlagError( $host, $lag, $maxLag ) { + global $wgShowHostnames; + header( 'HTTP/1.1 503 Service Unavailable' ); + header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); + header( 'X-Database-Lag: ' . intval( $lag ) ); + header( 'Content-Type: text/plain' ); + if( $wgShowHostnames ) { + echo "Waiting for $host: $lag seconds lagged\n"; + } else { + echo "Waiting for a database server: $lag seconds lagged\n"; + } +} diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 260439b2..050005dd 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -25,6 +25,7 @@ class HTMLCacheUpdate { public $mTitle, $mTable, $mPrefix; public $mRowsPerJob, $mRowsPerQuery; + public $mResult; function __construct( $titleTo, $table ) { global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery; @@ -40,15 +41,14 @@ class HTMLCacheUpdate $cond = $this->getToCondition(); $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); - $resWrap = new ResultWrapper( $dbr, $res ); + $this->mResult = $res; if ( $dbr->numRows( $res ) != 0 ) { if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) { - $this->insertJobs( $resWrap ); + $this->insertJobs( $res ); } else { - $this->invalidateIDs( $resWrap ); + $this->invalidateIDs( $res ); } } - $dbr->freeResult( $res ); } function insertJobs( ResultWrapper $res ) { @@ -87,6 +87,7 @@ class HTMLCacheUpdate 'imagelinks' => 'il', 'categorylinks' => 'cl', 'templatelinks' => 'tl', + 'redirect' => 'rd', # Not needed # 'externallinks' => 'el', @@ -107,16 +108,14 @@ class HTMLCacheUpdate } function getToCondition() { + $prefix = $this->getPrefix(); switch ( $this->mTable ) { case 'pagelinks': - return array( - 'pl_namespace' => $this->mTitle->getNamespace(), - 'pl_title' => $this->mTitle->getDBkey() - ); case 'templatelinks': - return array( - 'tl_namespace' => $this->mTitle->getNamespace(), - 'tl_title' => $this->mTitle->getDBkey() + case 'redirect': + return array( + "{$prefix}_namespace" => $this->mTitle->getNamespace(), + "{$prefix}_title" => $this->mTitle->getDBkey() ); case 'imagelinks': return array( 'il_to' => $this->mTitle->getDBkey() ); @@ -218,7 +217,6 @@ class HTMLCacheUpdateJob extends Job { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ ); $update->invalidateIDs( new ResultWrapper( $dbr, $res ) ); - $dbr->freeResult( $res ); return true; } diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 64f266f6..46ecd169 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -303,7 +303,7 @@ class ImageGallery $s .= "\n\t<tr>"; } $s .= - "\n\t\t" . '<td><div class="gallerybox" style="width: '.($this->mWidths*1.25).'px;">' + "\n\t\t" . '<td><div class="gallerybox" style="width: '.($this->mWidths+35).'px;">' . $thumbhtml . "\n\t\t\t" . '<div class="gallerytext">' . "\n" . $textlink . $text . $nb diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 3cf6d0ac..573bc4d7 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -19,11 +19,14 @@ class ImagePage extends Article { /* private */ var $repo; var $mExtraDescription = false; - function __construct( $title ) { + function __construct( $title, $time = false ) { parent::__construct( $title ); - $this->img = wfFindFile( $this->mTitle ); + $this->img = wfFindFile( $this->mTitle, $time ); if ( !$this->img ) { $this->img = wfLocalFile( $this->mTitle ); + $this->current = $this->img; + } else { + $this->current = $time ? wfLocalFile( $this->mTitle ) : $this->img; } $this->repo = $this->img->repo; } @@ -66,14 +69,14 @@ class ImagePage extends Article { } else { # Just need to set the right headers $wgOut->setArticleFlag( true ); - $wgOut->setRobotpolicy( 'index,follow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $this->viewUpdates(); } # Show shared description, if needed if ( $this->mExtraDescription ) { - $fol = wfMsg( 'shareddescriptionfollows' ); + $fol = wfMsgNoTrans( 'shareddescriptionfollows' ); if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) { $wgOut->addWikiText( $fol ); } @@ -157,7 +160,7 @@ class ImagePage extends Article { } function openShowImage() { - global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang; + global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang; $full_url = $this->img->getURL(); $linkAttribs = false; @@ -176,6 +179,7 @@ class ImagePage extends Article { $maxWidth = $max[0]; $maxHeight = $max[1]; $sk = $wgUser->getSkin(); + $dirmark = $wgContLang->getDirMark(); if ( $this->img->exists() ) { # image @@ -219,7 +223,7 @@ class ImagePage extends Article { } $msgbig = wfMsgHtml( 'show-big-image' ); $msgsmall = wfMsgExt( 'show-big-image-thumb', - array( 'parseinline' ), $width, $height ); + array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) ); } else { # Image is small enough to show full size on image page $msgbig = htmlspecialchars( $this->img->getName() ); @@ -235,7 +239,7 @@ class ImagePage extends Article { } else { $anchorclose .= $msgsmall . - '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $longDesc; + '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc; } if ( $this->img->isMultipage() ) { @@ -308,10 +312,8 @@ class ImagePage extends Article { if ($showLink) { $filename = wfEscapeWikiText( $this->img->getName() ); - global $wgContLang; - $dirmark = $wgContLang->getDirMark(); if (!$this->img->isSafeFile()) { - $warning = wfMsg( 'mediawarning' ); + $warning = wfMsgNoTrans( 'mediawarning' ); $wgOut->addWikiText( <<<EOT <div class="fullMedia"> <span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark @@ -364,9 +366,8 @@ EOT } function getUploadUrl() { - global $wgServer; $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); + return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); } /** @@ -412,25 +413,23 @@ EOT $sk = $wgUser->getSkin(); - $line = $this->img->nextHistoryLine(); - - if ( $line ) { - $list = new ImageHistoryList( $sk, $this->img ); - $file = $this->repo->newFileFromRow( $line ); + if ( $this->img->exists() ) { + $list = new ImageHistoryList( $sk, $this->current ); + $file = $this->current; $dims = $file->getDimensionsString(); $s = $list->beginImageHistoryList() . - $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp), - $this->mTitle->getDBkey(), $line->img_user, - $line->img_user_text, $line->img_size, $line->img_description, + $list->imageHistoryLine( true, wfTimestamp(TS_MW, $file->getTimestamp()), + $this->mTitle->getDBkey(), $file->getUser('id'), + $file->getUser('text'), $file->getSize(), $file->getDescription(), $dims ); - while ( $line = $this->img->nextHistoryLine() ) { - $file = $this->repo->newFileFromRow( $line ); + $hist = $this->img->getHistory(); + foreach( $hist as $file ) { $dims = $file->getDimensionsString(); - $s .= $list->imageHistoryLine( false, $line->oi_timestamp, - $line->oi_archive_name, $line->oi_user, - $line->oi_user_text, $line->oi_size, $line->oi_description, + $s .= $list->imageHistoryLine( false, wfTimestamp(TS_MW, $file->getTimestamp()), + $file->getArchiveName(), $file->getUser('id'), + $file->getUser('text'), $file->getSize(), $file->getDescription(), $dims ); } @@ -563,6 +562,19 @@ class ImageHistoryList { return "</table>\n"; } + /** + * Create one row of file history + * + * @param bool $iscur is this the current file version? + * @param string $timestamp timestamp of file version + * @param string $img filename + * @param int $user ID of uploading user + * @param string $usertext username of uploading user + * @param int $size size of file version + * @param string $description description of file version + * @param string $dims dimensions of file version + * @return string a HTML formatted table row + */ public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) { global $wgUser, $wgLang, $wgContLang; $local = $this->img->isLocal(); @@ -575,28 +587,28 @@ class ImageHistoryList { $q[] = 'action=delete'; if( !$iscur ) $q[] = 'oldimage=' . urlencode( $img ); - $row .= '(' . $this->skin->makeKnownLinkObj( + $row .= $this->skin->makeKnownLinkObj( $this->title, wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), implode( '&', $q ) - ) . ')'; + ); $row .= '</td>'; } // Reversion link/current indicator $row .= '<td>'; if( $iscur ) { - $row .= '(' . wfMsgHtml( 'filehist-current' ) . ')'; + $row .= wfMsgHtml( 'filehist-current' ); } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) { $q = array(); $q[] = 'action=revert'; $q[] = 'oldimage=' . urlencode( $img ); $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) ); - $row .= '(' . $this->skin->makeKnownLinkObj( + $row .= $this->skin->makeKnownLinkObj( $this->title, wfMsgHtml( 'filehist-revert' ), implode( '&', $q ) - ) . ')'; + ); } $row .= '</td>'; diff --git a/includes/JobQueue.php b/includes/JobQueue.php index a2780bdb..5cec3106 100644 --- a/includes/JobQueue.php +++ b/includes/JobQueue.php @@ -4,8 +4,6 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point\n" ); } -require_once('UserMailer.php'); - /** * Class to both describe a background job and handle jobs. */ @@ -290,3 +288,4 @@ abstract class Job { } } + diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index 20bcd3d4..db1114c9 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -34,7 +34,7 @@ class LinkBatch { $this->data[$ns] = array(); } - $this->data[$ns][$dbkey] = 1; + $this->data[$ns][str_replace( ' ', '_', $dbkey )] = 1; } /** diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index ee668f08..ced76d75 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -51,6 +51,7 @@ class LinkFilter { * @param $prot String: protocol */ public static function makeLike( $filterEntry , $prot = 'http://' ) { + $db = wfGetDB( DB_MASTER ); if ( substr( $filterEntry, 0, 2 ) == '*.' ) { $subdomains = true; $filterEntry = substr( $filterEntry, 2 ); @@ -83,23 +84,23 @@ class LinkFilter { $mailparts = explode( '@', $host ); $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); $host = $domainpart . '@' . $mailparts[0]; - $like = "$prot$host%"; + $like = $db->escapeLike( "$prot$host" ) . "%"; } elseif ( $prot == 'mailto:' ) { // domainpart of email adress only. do not add '.' $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); - $like = "$prot$host%"; + $like = $db->escapeLike( "$prot$host" ) . "%"; } else { $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); if ( substr( $host, -1, 1 ) !== '.' ) { $host .= '.'; } - $like = "$prot$host"; + $like = $db->escapeLike( "$prot$host" ); if ( $subdomains ) { $like .= '%'; } if ( !$subdomains || $path !== '/' ) { - $like .= $path . '%'; + $like .= $db->escapeLike( $path ) . '%'; } } return $like; diff --git a/includes/Linker.php b/includes/Linker.php index 9397b800..4b092cf9 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -52,19 +52,11 @@ class Linker { } /** @todo document */ - function getInternalLinkAttributes( $link, $text, $broken = false ) { + function getInternalLinkAttributes( $link, $text, $class='' ) { $link = urldecode( $link ); $link = str_replace( '_', ' ', $link ); $link = htmlspecialchars( $link ); - - if( $broken == 'stub' ) { - $r = ' class="stub"'; - } else if ( $broken == 'yes' ) { - $r = ' class="new"'; - } else { - $r = ''; - } - + $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : ''; $r .= " title=\"{$link}\""; return $r; } @@ -72,22 +64,38 @@ class Linker { /** * @param $nt Title object. * @param $text String: FIXME - * @param $broken Boolean: FIXME, default 'false'. + * @param $class String: CSS class of the link, default ''. */ - function getInternalLinkAttributesObj( &$nt, $text, $broken = false ) { - if( $broken == 'stub' ) { - $r = ' class="stub"'; - } else if ( $broken == 'yes' ) { - $r = ' class="new"'; - } else { - $r = ''; - } - + function getInternalLinkAttributesObj( &$nt, $text, $class='' ) { + $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : ''; $r .= ' title="' . $nt->getEscapedText() . '"'; return $r; } /** + * Return the CSS colour of a known link + * + * @param mixed $s + * @param integer $threshold user defined threshold + * @return string CSS class + */ + function getLinkColour( $s, $threshold ) { + if( $s === false ) { + return ''; + } + + $colour = ''; + if ( !empty( $s->page_is_redirect ) ) { + # Page is a redirect + $colour = 'mw-redirect'; + } elseif ( $threshold > 0 && $s->page_len < $threshold && Namespace::isContent( $s->page_namespace ) ) { + # Page is a stub + $colour = 'stub'; + } + return $colour; + } + + /** * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeLinkObj for further documentation. * @@ -99,16 +107,16 @@ class Linker { * the end of the link. */ function makeLink( $title, $text = '', $query = '', $trail = '' ) { - wfProfileIn( 'Linker::makeLink' ); + wfProfileIn( __METHOD__ ); $nt = Title::newFromText( $title ); - if ($nt) { + if ( $nt instanceof Title ) { $result = $this->makeLinkObj( $nt, $text, $query, $trail ); } else { wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" ); $result = $text == "" ? $title : $text; } - wfProfileOut( 'Linker::makeLink' ); + wfProfileOut( __METHOD__ ); return $result; } @@ -125,8 +133,8 @@ class Linker { */ function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') { $nt = Title::newFromText( $title ); - if ($nt) { - return $this->makeKnownLinkObj( Title::newFromText( $title ), $text, $query, $trail, $prefix , $aprops ); + if ( $nt instanceof Title ) { + return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix , $aprops ); } else { wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" ); return $text == '' ? $title : $text; @@ -146,8 +154,8 @@ class Linker { */ function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) { $nt = Title::newFromText( $title ); - if ($nt) { - return $this->makeBrokenLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + if ( $nt instanceof Title ) { + return $this->makeBrokenLinkObj( $nt, $text, $query, $trail ); } else { wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" ); return $text == '' ? $title : $text; @@ -155,6 +163,8 @@ class Linker { } /** + * @deprecated use makeColouredLinkObj + * * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeStubLinkObj for further documentation. * @@ -167,8 +177,8 @@ class Linker { */ function makeStubLink( $title, $text = '', $query = '', $trail = '' ) { $nt = Title::newFromText( $title ); - if ($nt) { - return $this->makeStubLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + if ( $nt instanceof Title ) { + return $this->makeStubLinkObj( $nt, $text, $query, $trail ); } else { wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" ); return $text == '' ? $title : $text; @@ -191,13 +201,11 @@ class Linker { */ function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) { global $wgUser; - $fname = 'Linker::makeLinkObj'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); - # Fail gracefully - if ( ! is_object($nt) ) { - # throw new MWException(); - wfProfileOut( $fname ); + if ( !$nt instanceof Title ) { + # Fail gracefully + wfProfileOut( __METHOD__ ); return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } @@ -217,23 +225,23 @@ class Linker { } $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $t; } elseif ( $nt->isAlwaysKnown() ) { # Image links, special page links and self-links with fragements are always known. $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); } else { - wfProfileIn( $fname.'-immediate' ); + wfProfileIn( __METHOD__.'-immediate' ); - # Handles links to special pages wich do not exist in the database: + # Handles links to special pages which do not exist in the database: if( $nt->getNamespace() == NS_SPECIAL ) { - if( SpecialPage::exists( $nt->getDbKey() ) ) { + if( SpecialPage::exists( $nt->getDBkey() ) ) { $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); } else { $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); } - wfProfileOut( $fname.'-immediate' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__.'-immediate' ); + wfProfileOut( __METHOD__ ); return $retVal; } @@ -242,29 +250,23 @@ class Linker { if ( 0 == $aid ) { $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); } else { - $stub = false; + $colour = ''; if ( $nt->isContentPage() ) { + # FIXME: This is stupid, we should combine this query with + # the Title::getArticleID() query above. $threshold = $wgUser->getOption('stubthreshold'); - if ( $threshold > 0 ) { - $dbr = wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( - array( 'page' ), - array( 'page_len', - 'page_is_redirect' ), - array( 'page_id' => $aid ), $fname ) ; - $stub = ( $s !== false && !$s->page_is_redirect && - $s->page_len < $threshold ); - } - } - if ( $stub ) { - $retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); - } else { - $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); + $dbr = wfGetDB( DB_SLAVE ); + $s = $dbr->selectRow( + array( 'page' ), + array( 'page_len', 'page_is_redirect', 'page_namespace' ), + array( 'page_id' => $aid ), __METHOD__ ) ; + $colour = $this->getLinkColour( $s, $threshold ); } + $retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix ); } - wfProfileOut( $fname.'-immediate' ); + wfProfileOut( __METHOD__.'-immediate' ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $retVal; } @@ -283,13 +285,12 @@ class Linker { * @return the a-element */ function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { + wfProfileIn( __METHOD__ ); - $fname = 'Linker::makeKnownLinkObj'; - wfProfileIn( $fname ); - - if ( !is_object( $nt ) ) { - wfProfileOut( $fname ); - return $text; + if ( !$nt instanceof Title ) { + # Fail gracefully + wfProfileOut( __METHOD__ ); + return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } $u = $nt->escapeLocalURL( $query ); @@ -313,14 +314,14 @@ class Linker { list( $inside, $trail ) = Linker::splitTrail( $trail ); $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $r; } /** * Make a red link to the edit page of a given title. * - * @param $title String: The text of the title + * @param $nt Title object of the target page * @param $text String: Link text * @param $query String: Optional query part * @param $trail String: Optional trail. Alphabetic characters at the start of this string will @@ -328,15 +329,14 @@ class Linker { * the end of the link. */ function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - # Fail gracefully - if ( ! isset($nt) ) { - # throw new MWException(); + wfProfileIn( __METHOD__ ); + + if ( !$nt instanceof Title ) { + # Fail gracefully + wfProfileOut( __METHOD__ ); return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } - $fname = 'Linker::makeBrokenLinkObj'; - wfProfileIn( $fname ); - if( $nt->getNamespace() == NS_SPECIAL ) { $q = $query; } else if ( '' == $query ) { @@ -349,19 +349,21 @@ class Linker { if ( '' == $text ) { $text = htmlspecialchars( $nt->getPrefixedText() ); } - $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); + $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new' ); list( $inside, $trail ) = Linker::splitTrail( $trail ); $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $s; } /** + * @deprecated use makeColouredLinkObj + * * Make a brown link to a short article. * - * @param $title String: the text of the title + * @param $nt Title object of the target page * @param $text String: link text * @param $query String: optional query part * @param $trail String: optional trail. Alphabetic characters at the start of this string will @@ -369,7 +371,25 @@ class Linker { * the end of the link. */ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - $style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' ); + return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix ); + } + + /** + * Make a coloured link. + * + * @param $nt Title object of the target page + * @param $colour Integer: colour of the link + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. + */ + function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) { + + if($colour != ''){ + $style = $this->getInternalLinkAttributesObj( $nt, $text, $colour ); + } else $style = ''; return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style ); } @@ -388,11 +408,8 @@ class Linker { function makeSizeLinkObj( $size, $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { global $wgUser; $threshold = intval( $wgUser->getOption( 'stubthreshold' ) ); - if( $size < $threshold ) { - return $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); - } else { - return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); - } + $colour = ( $size < $threshold ) ? 'stub' : ''; + return $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix ); } /** @@ -446,6 +463,7 @@ class Linker { * @param boolean $thumb shows image as thumbnail in a frame * @param string $manualthumb image name for the manual thumbnail * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom + * @param string $time, timestamp of the file, set as false for current * @return string */ function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false, @@ -468,7 +486,7 @@ class Linker { $frameParams['valign'] = $valign; } $file = wfFindFile( $title, $time ); - return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams ); + return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams, $time ); } /** @@ -476,26 +494,27 @@ class Linker { * @param Title $title Title object * @param File $file File object, or false if it doesn't exist * - * @param array $frameParams Associative array of parameters external to the media handler. - * Boolean parameters are indicated by presence or absence, the value is arbitrary and - * will often be false. - * thumbnail If present, downscale and frame - * manualthumb Image name to use as a thumbnail, instead of automatic scaling - * framed Shows image in original size in a frame - * frameless Downscale but don't frame - * upright If present, tweak default sizes for portrait orientation - * upright_factor Fudge factor for "upright" tweak (default 0.75) - * border If present, show a border around the image - * align Horizontal alignment (left, right, center, none) - * valign Vertical alignment (baseline, sub, super, top, text-top, middle, - * bottom, text-bottom) - * alt Alternate text for image (i.e. alt attribute). Plain text. - * caption HTML for image caption. + * @param array $frameParams Associative array of parameters external to the media handler. + * Boolean parameters are indicated by presence or absence, the value is arbitrary and + * will often be false. + * thumbnail If present, downscale and frame + * manualthumb Image name to use as a thumbnail, instead of automatic scaling + * framed Shows image in original size in a frame + * frameless Downscale but don't frame + * upright If present, tweak default sizes for portrait orientation + * upright_factor Fudge factor for "upright" tweak (default 0.75) + * border If present, show a border around the image + * align Horizontal alignment (left, right, center, none) + * valign Vertical alignment (baseline, sub, super, top, text-top, middle, + * bottom, text-bottom) + * alt Alternate text for image (i.e. alt attribute). Plain text. + * caption HTML for image caption. * - * @param array $handlerParams Associative array of media handler parameters, to be passed - * to transform(). Typical keys are "width" and "page". + * @param array $handlerParams Associative array of media handler parameters, to be passed + * to transform(). Typical keys are "width" and "page". + * @param string $time, timestamp of the file, set as false for current */ - function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { + function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) { global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; if ( $file && !$file->allowInlineDisplay() ) { wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" ); @@ -556,7 +575,16 @@ class Linker { if ( $fp['align'] == '' ) { $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right'; } - return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp ).$postfix; + return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time ).$postfix; + } + + if ( $file && isset( $fp['frameless'] ) ) { + $srcWidth = $file->getWidth( $page ); + # For "frameless" option: do not present an image bigger than the source (for bitmap-style images) + # This is the same behaviour as the "thumb" option does it already. + if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) { + $hp['width'] = $srcWidth; + } } if ( $file && $hp['width'] ) { @@ -567,7 +595,7 @@ class Linker { } if ( !$thumb ) { - $s = $this->makeBrokenImageLinkObj( $title ); + $s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true ); } else { $s = $thumb->toHtml( array( 'desc-link' => true, @@ -597,7 +625,7 @@ class Linker { return $this->makeThumbLink2( $title, $file, $frameParams, $params ); } - function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { + function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) { global $wgStylePath, $wgContLang; $exists = $file && $file->exists(); @@ -654,12 +682,10 @@ class Linker { $url = $title->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); - $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; - $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; $s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">"; if( !$exists ) { - $s .= $this->makeBrokenImageLinkObj( $title ); + $s .= $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true ); $zoomicon = ''; } elseif ( !$thumb ) { $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); @@ -672,13 +698,13 @@ class Linker { if ( isset( $fp['framed'] ) ) { $zoomicon=""; } else { - $zoomicon = '<div class="magnify" style="float:'.$magnifyalign.'">'. + $zoomicon = '<div class="magnify">'. '<a href="'.$url.'" class="internal" title="'.$more.'">'. '<img src="'.$wgStylePath.'/common/images/magnify-clip.png" ' . 'width="15" height="11" alt="" /></a></div>'; } } - $s .= ' <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$fp['caption']."</div></div></div>"; + $s .= ' <div class="thumbcaption">'.$zoomicon.$fp['caption']."</div></div></div>"; return str_replace("\n", ' ', $s); } @@ -690,21 +716,27 @@ class Linker { * @param string $query Query string * @param string $trail Link trail * @param string $prefix Link prefix + * @param bool $time, a file of a certain timestamp was requested * @return string */ - public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { + public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '', $time = false ) { global $wgEnableUploads; if( $title instanceof Title ) { wfProfileIn( __METHOD__ ); - if( $wgEnableUploads ) { + $currentExists = $time ? ( wfFindFile( $title ) != false ) : false; + if( $wgEnableUploads && !$currentExists ) { $upload = SpecialPage::getTitleFor( 'Upload' ); if( $text == '' ) $text = htmlspecialchars( $title->getPrefixedText() ); + $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ); + if( $redir ) { + return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix ); + } $q = 'wpDestFile=' . $title->getPartialUrl(); if( $query != '' ) $q .= '&' . $query; list( $inside, $trail ) = self::splitTrail( $trail ); - $style = $this->getInternalLinkAttributesObj( $title, $text, 'yes' ); + $style = $this->getInternalLinkAttributesObj( $title, $text, 'new' ); wfProfileOut( __METHOD__ ); return '<a href="' . $upload->escapeLocalUrl( $q ) . '"' . $style . '>' . $prefix . $text . $inside . '</a>' . $trail; @@ -744,7 +776,7 @@ class Linker { $class = 'internal'; } else { $upload = SpecialPage::getTitleFor( 'Upload' ); - $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDbKey() ) ); + $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDBkey() ) ); $class = 'new'; } $alt = htmlspecialchars( $title->getText() ); @@ -946,8 +978,9 @@ class Linker { * add a separator where needed and format the comment itself with CSS * Called by Linker::formatComment. * - * @param $comment Comment text - * @param $title An optional title object used to links to sections + * @param string $comment Comment text + * @param object $title An optional title object used to links to sections + * @return string $comment formatted comment * * @todo Document the $local parameter. */ @@ -975,14 +1008,17 @@ class Linker { $sectionTitle = wfClone( $title ); $sectionTitle->mFragment = $section; } - $link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) ); + $link = $this->makeKnownLinkObj( $sectionTitle, wfMsgForContent( 'sectionlink' ) ); + } + $auto = $link . $auto; + if( $pre ) { + $auto = '- ' . $auto; # written summary $presep autocomment (summary /* section */) } - $sep='-'; - $auto=$link.$auto; - if($pre) { $auto = $sep.' '.$auto; } - if($post) { $auto .= ' '.$sep; } - $auto='<span class="autocomment">'.$auto.'</span>'; - $comment=$pre.$auto.$post; + if( $post ) { + $auto .= ': '; # autocomment $postsep written summary (/* section */ summary) + } + $auto = '<span class="autocomment">' . $auto . '</span>'; + $comment = $pre . $auto . $post; } return $comment; @@ -992,42 +1028,49 @@ class Linker { * Formats wiki links and media links in text; all other wiki formatting * is ignored * + * @fixme doesn't handle sub-links as in image thumb texts like the main parser * @param string $comment Text to format links in * @return string */ public function formatLinksInComment( $comment ) { + return preg_replace_callback( + '/\[\[:?(.*?)(\|(.*?))*\]\]([^[]*)/', + array( $this, 'formatLinksInCommentCallback' ), + $comment ); + } + + protected function formatLinksInCommentCallback( $match ) { global $wgContLang; $medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|'; $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):'; + + $comment = $match[0]; - $match = array(); - while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { - # Handle link renaming [[foo|text]] will show link as "text" - if( "" != $match[3] ) { - $text = $match[3]; - } else { - $text = $match[1]; - } - $submatch = array(); - if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { - # Media link; trail not supported. - $linkRegexp = '/\[\[(.*?)\]\]/'; - $thelink = $this->makeMediaLink( $submatch[1], "", $text ); + # Handle link renaming [[foo|text]] will show link as "text" + if( "" != $match[3] ) { + $text = $match[3]; + } else { + $text = $match[1]; + } + $submatch = array(); + if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { + # Media link; trail not supported. + $linkRegexp = '/\[\[(.*?)\]\]/'; + $thelink = $this->makeMediaLink( $submatch[1], "", $text ); + } else { + # Other kind of link + if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) { + $trail = $submatch[1]; } else { - # Other kind of link - if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) { - $trail = $submatch[1]; - } else { - $trail = ""; - } - $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; - if (isset($match[1][0]) && $match[1][0] == ':') - $match[1] = substr($match[1], 1); - $thelink = $this->makeLink( $match[1], $text, "", $trail ); + $trail = ""; } - $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); + $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; + if (isset($match[1][0]) && $match[1][0] == ':') + $match[1] = substr($match[1], 1); + $thelink = $this->makeLink( $match[1], $text, "", $trail ); } + $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); return $comment; } @@ -1103,7 +1146,7 @@ class Linker { /** @todo document */ function tocList($toc) { global $wgJsMimeType; - $title = wfMsgHtml('toc') ; + $title = wfMsgHtml('toc') ; return '<table id="toc" class="toc" summary="' . $title .'"><tr><td>' . '<div id="toctitle"><h2>' . $title . "</h2></div>\n" @@ -1167,9 +1210,9 @@ class Linker { // The two hooks have slightly different interfaces . . . if( $hook == 'EditSectionLink' ) { - wfRunHooks( $hook, array( &$this, $nt, $section, $hint, $url, &$result ) ); + wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $hint, $url, &$result ) ); } elseif( $hook == 'EditSectionLinkForOther' ) { - wfRunHooks( $hook, array( &$this, $nt, $section, $url, &$result ) ); + wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) ); } // For reverse compatibility, add the brackets *after* the hook is run, @@ -1334,6 +1377,8 @@ class Linker { * element (e.g., ' title="This does something [x]" accesskey="x"'). */ public function tooltipAndAccesskey($name) { + $fname="Linker::tooltipAndAccesskey"; + wfProfileIn($fname); $out = ''; $tooltip = wfMsg('tooltip-'.$name); @@ -1349,6 +1394,7 @@ class Linker { } elseif ($out) { $out .= '"'; } + wfProfileOut($fname); return $out; } @@ -1373,7 +1419,3 @@ class Linker { return $out; } } - - - - diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 9bcd9d67..a52414c3 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -73,11 +73,15 @@ class LinksUpdate { */ function doUpdate() { global $wgUseDumbLinkUpdate; + + wfRunHooks( 'LinksUpdate', array( &$this ) ); if ( $wgUseDumbLinkUpdate ) { $this->doDumbUpdate(); } else { $this->doIncrementalUpdate(); } + wfRunHooks( 'LinksUpdateComplete', array( &$this ) ); + } function doIncrementalUpdate() { @@ -595,5 +599,12 @@ class LinksUpdate { } return $arr; } + + /** + * Return the title object of the page being updated + */ + function getTitle() { + return $this->mTitle; + } } diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php index 65a6d5a6..0cdadd1e 100644 --- a/includes/LoadBalancer.php +++ b/includes/LoadBalancer.php @@ -16,12 +16,6 @@ class LoadBalancer { /* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout; /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error'; - /** - * Scale polling time so that under overload conditions, the database server - * receives a SHOW STATUS query at an average interval of this many microseconds - */ - const AVG_STATUS_POLL = 2000; - function __construct( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false ) { $this->mServers = $servers; @@ -133,7 +127,7 @@ class LoadBalancer { * Side effect: opens connections to databases */ function getReaderIndex() { - global $wgReadOnly, $wgDBClusterTimeout; + global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll; $fname = 'LoadBalancer::getReaderIndex'; wfProfileIn( $fname ); @@ -180,7 +174,7 @@ class LoadBalancer { # Too much load, back off and wait for a while. # The sleep time is scaled by the number of threads connected, # to produce a roughly constant global poll rate. - $sleepTime = self::AVG_STATUS_POLL * $status['Threads_connected']; + $sleepTime = $wgDBAvgStatusPoll * $status['Threads_connected']; # If we reach the timeout and exit the loop, don't use it $i = false; @@ -324,13 +318,13 @@ class LoadBalancer { # Query groups if ( !is_array( $groups ) ) { - $groupIndex = $this->getGroupIndex( $groups, $i ); + $groupIndex = $this->getGroupIndex( $groups ); if ( $groupIndex !== false ) { $i = $groupIndex; } } else { foreach ( $groups as $group ) { - $groupIndex = $this->getGroupIndex( $group, $i ); + $groupIndex = $this->getGroupIndex( $group ); if ( $groupIndex !== false ) { $i = $groupIndex; break; @@ -432,8 +426,7 @@ class LoadBalancer { return $db; } - function reportConnectionError( &$conn ) - { + function reportConnectionError( &$conn ) { $fname = 'LoadBalancer::reportConnectionError'; wfProfileIn( $fname ); # Prevent infinite recursion @@ -552,6 +545,17 @@ class LoadBalancer { } } } + + /* Issue COMMIT only on master, only if queries were done on connection */ + function commitMasterChanges() { + // Always 0, but who knows.. :) + $i = $this->getWriterIndex(); + if (array_key_exists($i,$this->mConnections)) { + if ($this->mConnections[$i]->lastQuery() != '') { + $this->mConnections[$i]->immediateCommit(); + } + } + } function waitTimeout( $value = NULL ) { return wfSetVar( $this->mWaitTimeout, $value ); diff --git a/includes/LogPage.php b/includes/LogPage.php index 8982b59f..7c89df76 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -116,9 +116,10 @@ class LogPage { * @static */ public static function logName( $type ) { - global $wgLogNames; + global $wgLogNames, $wgMessageCache; if( isset( $wgLogNames[$type] ) ) { + $wgMessageCache->loadAllMessages(); return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) ); } else { // Bogus log types? Perhaps an extension was removed. @@ -138,7 +139,7 @@ class LogPage { /** * @static */ - static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) { + static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) { global $wgLang, $wgContLang, $wgLogActions; $key = "$type/$action"; @@ -172,6 +173,11 @@ class LogPage { $text = $wgContLang->ucfirst( $title->getText() ); $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); break; + case 'merge': + $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); + $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); + $params[1] = $wgLang->timeanddate( $params[1] ); + break; default: $titleLink = $skin->makeLinkObj( $title ); } @@ -199,8 +205,10 @@ class LogPage { } else { array_unshift( $params, $titleLink ); if ( $key == 'block/block' ) { - if ( $translate ) { - $params[1] = $wgLang->translateBlockExpiry( $params[1] ); + if ( $skin ) { + $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>'; + } else { + $params[1] = $wgContLang->translateBlockExpiry( $params[1] ); } $params[2] = isset( $params[2] ) ? self::formatBlockFlags( $params[2] ) diff --git a/includes/MagicWord.php b/includes/MagicWord.php index f7a9400d..18c931c5 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -101,6 +101,44 @@ class MagicWord { 'numberofadmins', 'defaultsort', ); + + /* Array of caching hints for ParserCache */ + static public $mCacheTTLs = array ( + 'currentmonth' => 86400, + 'currentmonthname' => 86400, + 'currentmonthnamegen' => 86400, + 'currentmonthabbrev' => 86400, + 'currentday' => 3600, + 'currentday2' => 3600, + 'currentdayname' => 3600, + 'currentyear' => 86400, + 'currenttime' => 3600, + 'currenthour' => 3600, + 'localmonth' => 86400, + 'localmonthname' => 86400, + 'localmonthnamegen' => 86400, + 'localmonthabbrev' => 86400, + 'localday' => 3600, + 'localday2' => 3600, + 'localdayname' => 3600, + 'localyear' => 86400, + 'localtime' => 3600, + 'localhour' => 3600, + 'numberofarticles' => 3600, + 'numberoffiles' => 3600, + 'numberofedits' => 3600, + 'currentweek' => 3600, + 'currentdow' => 3600, + 'localweek' => 3600, + 'localdow' => 3600, + 'numberofusers' => 3600, + 'numberofpages' => 3600, + 'currentversion' => 86400, + 'currenttimestamp' => 3600, + 'localtimestamp' => 3600, + 'pagesinnamespace' => 3600, + 'numberofadmins' => 3600, + ); static public $mObjects = array(); @@ -122,11 +160,13 @@ class MagicWord { * @static */ static function &get( $id ) { + wfProfileIn( __METHOD__ ); if (!array_key_exists( $id, self::$mObjects ) ) { $mw = new MagicWord(); $mw->load( $id ); self::$mObjects[$id] = $mw; } + wfProfileOut( __METHOD__ ); return self::$mObjects[$id]; } @@ -148,7 +188,17 @@ class MagicWord { } return self::$mVariableIDs; } - + + /* Allow external reads of TTL array */ + static function getCacheTTL($id) { + if (array_key_exists($id,self::$mCacheTTLs)) { + return self::$mCacheTTLs[$id]; + } else { + return -1; + } + } + + # Initialises this object with an ID function load( $id ) { global $wgContLang; diff --git a/includes/Math.php b/includes/Math.php index 2771d04c..cfed9554 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -111,10 +111,17 @@ class MathRenderer { } else { $errbit = htmlspecialchars( substr($contents, 1) ); switch( $retval ) { - case 'E': $errmsg = $this->_error( 'math_lexing_error', $errbit ); - case 'S': $errmsg = $this->_error( 'math_syntax_error', $errbit ); - case 'F': $errmsg = $this->_error( 'math_unknown_function', $errbit ); - default: $errmsg = $this->_error( 'math_unknown_error', $errbit ); + case 'E': + $errmsg = $this->_error( 'math_lexing_error', $errbit ); + break; + case 'S': + $errmsg = $this->_error( 'math_syntax_error', $errbit ); + break; + case 'F': + $errmsg = $this->_error( 'math_unknown_function', $errbit ); + break; + default: + $errmsg = $this->_error( 'math_unknown_error', $errbit ); } } diff --git a/includes/MessageCache.php b/includes/MessageCache.php index 10c95a7e..ce717fa8 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -58,9 +58,8 @@ class MessageCache { * Try to load the cache from a local file */ function loadFromLocal( $hash ) { - global $wgLocalMessageCache; + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized; - $this->mCache = false; if ( $wgLocalMessageCache === false ) { return; } @@ -74,21 +73,35 @@ class MessageCache { return; } - // Check to see if the file has the hash specified - $localHash = fread( $file, 32 ); - if ( $hash == $localHash ) { - // All good, get the rest of it - $serialized = fread( $file, 10000000 ); - $this->setCache( unserialize( $serialized ) ); + if ( $wgLocalMessageCacheSerialized ) { + // Check to see if the file has the hash specified + $localHash = fread( $file, 32 ); + if ( $hash === $localHash ) { + // All good, get the rest of it + $serialized = ''; + while ( !feof( $file ) ) { + $serialized .= fread( $file, 100000 ); + } + $this->setCache( unserialize( $serialized ) ); + } + fclose( $file ); + } else { + $localHash=substr(fread($file,40),8); + fclose($file); + if ($hash!=$localHash) { + return; + } + + require("$wgLocalMessageCache/messages-" . wfWikiID()); + $this->setCache( $this->mCache); } - fclose( $file ); } /** * Save the cache to a local file */ function saveToLocal( $serialized, $hash ) { - global $wgLocalMessageCache; + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized; if ( $wgLocalMessageCache === false ) { return; @@ -111,26 +124,8 @@ class MessageCache { } function loadFromScript( $hash ) { - global $wgLocalMessageCache; - if ( $wgLocalMessageCache === false ) { - return; - } - - $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); - - wfSuppressWarnings(); - $file = fopen( $filename, 'r' ); - wfRestoreWarnings(); - if ( !$file ) { - return; - } - $localHash=substr(fread($file,40),8); - fclose($file); - if ($hash!=$localHash) { - return; - } - require("$wgLocalMessageCache/messages-" . wfWikiID()); - $this->setCache( $this->mCache); + trigger_error( 'Use of ' . __METHOD__ . ' is deprecated', E_USER_NOTICE ); + $this->loadFromLocal( $hash ); } function saveToScript($array, $hash) { @@ -201,19 +196,17 @@ class MessageCache { $this->mCache = false; # Try local cache - wfProfileIn( $fname.'-fromlocal' ); - $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); - if ( $hash ) { - if ($wgLocalMessageCacheSerialized) { + if ( $wgLocalMessageCache !== false ) { + wfProfileIn( $fname.'-fromlocal' ); + $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); + if ( $hash ) { $this->loadFromLocal( $hash ); - } else { - $this->loadFromScript( $hash ); - } - if ( $this->mCache ) { - wfDebug( "MessageCache::load(): got from local cache\n" ); + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from local cache\n" ); + } } + wfProfileOut( $fname.'-fromlocal' ); } - wfProfileOut( $fname.'-fromlocal' ); # Try memcached if ( !$this->mCache ) { @@ -358,7 +351,6 @@ class MessageCache { wfProfileIn( __METHOD__ ); $this->lock(); $this->load(); - $parserMemc->delete(wfMemcKey('sidebar')); if ( is_array( $this->mCache ) ) { if ( $text === false ) { # Article was deleted @@ -386,6 +378,7 @@ class MessageCache { } } $this->unlock(); + $parserMemc->delete(wfMemcKey('sidebar')); wfProfileOut( __METHOD__ ); } @@ -475,17 +468,20 @@ class MessageCache { } # Try the array of another language - if( $message === false && strpos( $lckey, '/' ) ) { - $message = explode( '/', $lckey ); - if ( $message[1] ) { - wfSuppressWarnings(); - $message = Language::getMessageFor( $message[0], $message[1] ); - wfRestoreWarnings(); - if ( is_null( $message ) ) { - $message = false; + $pos = strrpos( $lckey, '/' ); + if( $message === false && $pos !== false) { + $mkey = substr( $lckey, 0, $pos ); + $code = substr( $lckey, $pos+1 ); + if ( $code ) { + $validCodes = array_keys( Language::getLanguageNames() ); + if ( in_array( $code, $validCodes ) ) { + $message = Language::getMessageFor( $mkey, $code ); + if ( is_null( $message ) ) { + $message = false; + } + } else { + wfDebug( __METHOD__ . ": Invalid code $code for $mkey/$code, not trying messages array\n" ); } - } else { - $message = false; } } @@ -500,9 +496,6 @@ class MessageCache { if( $message === false ) { return '<' . htmlspecialchars($key) . '>'; } - - # Replace brace tags - $message = $this->transform( $message ); return $message; } @@ -576,7 +569,7 @@ class MessageCache { return $message; } - function transform( $message ) { + function transform( $message, $interface = false ) { global $wgParser; if ( !$this->mParser && isset( $wgParser ) ) { # Do some initialisation so that we don't have to do it twice @@ -584,9 +577,11 @@ class MessageCache { # Clone it and store it $this->mParser = clone $wgParser; } - if ( !$this->mDisableTransform && $this->mParser ) { + if ( $this->mParser ) { if( strpos( $message, '{{' ) !== false ) { - $message = $this->mParser->transformMsg( $message, $this->getParserOptions() ); + $popts = $this->getParserOptions(); + $popts->setInterfaceMessage( $interface ); + $message = $this->mParser->transformMsg( $message, $popts ); } } return $message; @@ -594,10 +589,12 @@ class MessageCache { function disable() { $this->mDisable = true; } function enable() { $this->mDisable = false; } - function disableTransform() { $this->mDisableTransform = true; } - function enableTransform() { $this->mDisableTransform = false; } - function setTransform( $x ) { $this->mDisableTransform = $x; } - function getTransform() { return $this->mDisableTransform; } + + /** @deprecated */ + function disableTransform() {} + function enableTransform() {} + function setTransform( $x ) {} + function getTransform() { return false; } /** * Add a message to the cache @@ -618,6 +615,9 @@ class MessageCache { */ function addMessages( $messages, $lang = 'en' ) { wfProfileIn( __METHOD__ ); + if ( !is_array( $messages ) ) { + throw new MWException( __METHOD__.': Invalid message array' ); + } if ( isset( $this->mExtensionMessages[$lang] ) ) { $this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang]; } else { @@ -640,7 +640,8 @@ class MessageCache { } /** - * Get the extension messages for a specific language + * Get the extension messages for a specific language. Only English, interface + * and content language are guaranteed to be loaded. * * @param string $lang The messages language, English by default */ @@ -695,9 +696,28 @@ class MessageCache { * Load messages from a given file */ function loadMessagesFile( $filename ) { - $magicWords = false; + global $wgLang, $wgContLang; + $messages = $magicWords = false; require( $filename ); - $this->addMessagesByLang( $messages ); + + /* + * Load only languages that are usually used, and merge all fallbacks, + * except English. + */ + $langs = array_unique( array( 'en', $wgContLang->getCode(), $wgLang->getCode() ) ); + foreach( $langs as $code ) { + $fbcode = $code; + $mergedMessages = array(); + do { + if ( isset($messages[$fbcode]) ) { + $mergedMessages += $messages[$fbcode]; + } + $fbcode = Language::getFallbackfor( $fbcode ); + } while( $fbcode && $fbcode !== 'en' ); + + if ( !empty($mergedMessages) ) + $this->addMessages( $mergedMessages, $code ); + } if ( $magicWords !== false ) { global $wgContLang; @@ -705,4 +725,3 @@ class MessageCache { } } } - diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 264a3595..2ca5892f 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -24,8 +24,9 @@ image/jpeg jpeg jpg jpe image/png png image/svg+xml image/svg svg image/tiff tiff tif -image/vnd.djvu djvu +image/vnd.djvu image/x.djvu image/x-djvu djvu image/x-portable-pixmap ppm +image/x-xcf xcf text/plain txt text/html html htm video/ogg ogm ogg @@ -54,6 +55,7 @@ image/png [BITMAP] image/svg+xml [DRAWING] image/tiff [BITMAP] image/vnd.djvu [BITMAP] +image/x-xcf [BITMAP] image/x-portable-pixmap [BITMAP] text/plain [TEXT] text/html [TEXT] @@ -351,10 +353,17 @@ class MimeMagic { */ function isRecognizableExtension( $extension ) { static $types = array( + // Types recognized by getimagesize() 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd', 'bmp', 'tiff', 'tif', 'jpc', 'jp2', 'jpx', 'jb2', 'swc', 'iff', 'wbmp', - 'xbm', 'djvu' + 'xbm', + + // Formats we recognize magic numbers for + 'djvu', 'ogg', 'mid', 'pdf', 'wmf', 'xcf', + + // XML formats we sure hope we recognize reliably + 'svg', ); return in_array( strtolower( $extension ), $types ); } @@ -371,8 +380,22 @@ class MimeMagic { * @return string the mime type of $file */ function guessMimeType( $file, $ext = true ) { - $mime = $this->detectMimeType( $file, $ext ); + $mime = $this->doGuessMimeType( $file, $ext ); + + if( !$mime ) { + wfDebug( __METHOD__.": internal type detection failed for $file (.$ext)...\n" ); + $mime = $this->detectMimeType( $file, $ext ); + } + + if ( isset( $this->mMimeTypeAliases[$mime] ) ) { + $mime = $this->mMimeTypeAliases[$mime]; + } + wfDebug(__METHOD__.": final mime type of $file: $mime\n"); + return $mime; + } + + function doGuessMimeType( $file, $ext = true ) { // Read a chunk of the file wfSuppressWarnings(); $f = fopen( $file, "rt" ); @@ -381,128 +404,132 @@ class MimeMagic { $head = fread( $f, 1024 ); fclose( $f ); - $sub4 = substr( $head, 0, 4 ); - if ( $sub4 == "\x01\x00\x09\x00" || $sub4 == "\xd7\xcd\xc6\x9a" ) { - // WMF kill kill kill + // Hardcode a few magic number checks... + $headers = array( + // Multimedia... + 'MThd' => 'audio/midi', + 'OggS' => 'application/ogg', + + // Image formats... // Note that WMF may have a bare header, no magic number. - // The former of the above two checks is theoretically prone to false positives - $mime = "application/x-msmetafile"; + "\x01\x00\x09\x00" => 'application/x-msmetafile', // Possibly prone to false positives? + "\xd7\xcd\xc6\x9a" => 'application/x-msmetafile', + '%PDF' => 'application/pdf', + 'gimp xcf' => 'image/x-xcf', + + // Some forbidden fruit... + 'MZ' => 'application/octet-stream', // DOS/Windows executable + "\xca\xfe\xba\xbe" => 'application/octet-stream', // Mach-O binary + "\x7fELF" => 'application/octet-stream', // ELF binary + ); + + foreach( $headers as $magic => $candidate ) { + if( strncmp( $head, $magic, strlen( $magic ) ) == 0 ) { + wfDebug( __METHOD__ . ": magic header in $file recognized as $candidate\n" ); + return $candidate; + } } - if ( strpos( $mime, "text/" ) === 0 || $mime === "application/xml" ) { - - $xml_type = NULL; - $script_type = NULL; - - /* - * look for XML formats (XHTML and SVG) - */ - if ($mime === "text/sgml" || - $mime === "text/plain" || - $mime === "text/html" || - $mime === "text/xml" || - $mime === "application/xml") { - - if ( substr( $head, 0, 5 ) == "<?xml" ) { - $xml_type = "ASCII"; - } elseif ( substr( $head, 0, 8 ) == "\xef\xbb\xbf<?xml") { - $xml_type = "UTF-8"; - } elseif ( substr( $head, 0, 10 ) == "\xfe\xff\x00<\x00?\x00x\x00m\x00l" ) { - $xml_type = "UTF-16BE"; - } elseif ( substr( $head, 0, 10 ) == "\xff\xfe<\x00?\x00x\x00m\x00l\x00") { - $xml_type = "UTF-16LE"; - } - - if ( $xml_type ) { - if ( $xml_type !== "UTF-8" && $xml_type !== "ASCII" ) { - $head = iconv( $xml_type, "ASCII//IGNORE", $head ); - } - - $match = array(); - $doctype = ""; - $tag = ""; - - if ( preg_match( '%<!DOCTYPE\s+[\w-]+\s+PUBLIC\s+["'."'".'"](.*?)["'."'".'"].*>%sim', - $head, $match ) ) { - $doctype = $match[1]; - } - if ( preg_match( '%<(\w+).*>%sim', $head, $match ) ) { - $tag = $match[1]; - } - - #print "<br>ANALYSING $file ($mime): doctype= $doctype; tag= $tag<br>"; - - if ( strpos( $doctype, "-//W3C//DTD SVG" ) === 0 ) { - $mime = "image/svg+xml"; - } elseif ( $tag === "svg" ) { - $mime = "image/svg+xml"; - } elseif ( strpos( $doctype, "-//W3C//DTD XHTML" ) === 0 ) { - $mime = "text/html"; - } elseif ( $tag === "html" ) { - $mime = "text/html"; - } - } + /* + * look for PHP + * Check for this before HTML/XML... + * Warning: this is a heuristic, and won't match a file with a lot of non-PHP before. + * It will also match text files which could be PHP. :) + */ + if( ( strpos( $head, '<?php' ) !== false ) || + ( strpos( $head, '<? ' ) !== false ) || + ( strpos( $head, "<?\n" ) !== false ) || + ( strpos( $head, "<?\t" ) !== false ) || + ( strpos( $head, "<?=" ) !== false ) || + + ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) || + ( strpos( $head, "<\x00?\x00 " ) !== false ) || + ( strpos( $head, "<\x00?\x00\n" ) !== false ) || + ( strpos( $head, "<\x00?\x00\t" ) !== false ) || + ( strpos( $head, "<\x00?\x00=" ) !== false ) ) { + + wfDebug( __METHOD__ . ": recognized $file as application/x-php\n" ); + return "application/x-php"; + } + + /* + * look for XML formats (XHTML and SVG) + */ + $xml = new XmlTypeCheck( $file ); + if( $xml->wellFormed ) { + $types = array( + 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml', + 'svg' => 'image/svg+xml', + 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml? + 'html' => 'text/html', // application/xhtml+xml? + ); + if( isset( $types[$xml->rootElement] ) ) { + $mime = $types[$xml->rootElement]; + return $mime; + } else { + /// Fixme -- this would be the place to allow additional XML type checks + return 'application/xml'; } + } - /* - * look for shell scripts - */ - if ( !$xml_type ) { - $script_type = NULL; - - # detect by shebang - if ( substr( $head, 0, 2) == "#!" ) { - $script_type = "ASCII"; - } elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) { - $script_type = "UTF-8"; - } elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) { - $script_type = "UTF-16BE"; - } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) { - $script_type= "UTF-16LE"; - } - - if ( $script_type ) { - if ( $script_type !== "UTF-8" && $script_type !== "ASCII") { - $head = iconv( $script_type, "ASCII//IGNORE", $head); - } - - $match = array(); + /* + * look for shell scripts + */ + $script_type = NULL; + + # detect by shebang + if ( substr( $head, 0, 2) == "#!" ) { + $script_type = "ASCII"; + } elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) { + $script_type = "UTF-8"; + } elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) { + $script_type = "UTF-16BE"; + } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) { + $script_type= "UTF-16LE"; + } - if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { - $mime = "application/x-{$match[2]}"; + if ( $script_type ) { + if ( $script_type !== "UTF-8" && $script_type !== "ASCII") { + // Quick and dirty fold down to ASCII! + $pack = array( 'UTF-16BE' => 'n*', 'UTF-16LE' => 'v*' ); + $chars = unpack( $pack[$script_type], substr( $head, 2 ) ); + $head = ''; + foreach( $chars as $codepoint ) { + if( $codepoint < 128 ) { + $head .= chr( $codepoint ); + } else { + $head .= '?'; } } } - /* - * look for PHP - */ - if( !$xml_type && !$script_type ) { - - if( ( strpos( $head, '<?php' ) !== false ) || - ( strpos( $head, '<? ' ) !== false ) || - ( strpos( $head, "<?\n" ) !== false ) || - ( strpos( $head, "<?\t" ) !== false ) || - ( strpos( $head, "<?=" ) !== false ) || - - ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) || - ( strpos( $head, "<\x00?\x00 " ) !== false ) || - ( strpos( $head, "<\x00?\x00\n" ) !== false ) || - ( strpos( $head, "<\x00?\x00\t" ) !== false ) || - ( strpos( $head, "<\x00?\x00=" ) !== false ) ) { + $match = array(); - $mime = "application/x-php"; - } + if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { + $mime = "application/x-{$match[2]}"; + wfDebug( __METHOD__.": shell script recognized as $mime\n" ); + return $mime; } - } - - if ( isset( $this->mMimeTypeAliases[$mime] ) ) { - $mime = $this->mMimeTypeAliases[$mime]; + + wfSuppressWarnings(); + $gis = getimagesize( $file ); + wfRestoreWarnings(); + + if( $gis && isset( $gis['mime'] ) ) { + $mime = $gis['mime']; + wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" ); + return $mime; + } else { + return false; } - wfDebug(__METHOD__.": final mime type of $file: $mime\n"); - return $mime; + // Also test DjVu + $deja = new DjVuImage( $file ); + if( $deja->isValid() ) { + wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" ); + return 'image/vnd.djvu'; + } } /** Internal mime type detection, please use guessMimeType() for application code instead. @@ -559,15 +586,6 @@ class MimeMagic { # see http://www.php.net/manual/en/ref.mime-magic.php for details. $m = mime_content_type($file); - - if ( $m == 'text/plain' ) { - // mime_content_type sometimes considers DJVU files to be text/plain. - $deja = new DjVuImage( $file ); - if( $deja->isValid() ) { - wfDebug( __METHOD__.": (re)detected $file as image/vnd.djvu\n" ); - $m = 'image/vnd.djvu'; - } - } } else { wfDebug( __METHOD__.": no magic mime detector found!\n" ); } @@ -586,66 +604,20 @@ class MimeMagic { } } - # if still not known, use getimagesize to find out the type of image - # TODO: skip things that do not have a well-known image extension? Would that be safe? - wfSuppressWarnings(); - $gis = getimagesize( $file ); - wfRestoreWarnings(); - - $notAnImage = false; - - if ( $gis && is_array($gis) && $gis[2] ) { - - switch ( $gis[2] ) { - case IMAGETYPE_GIF: $m = "image/gif"; break; - case IMAGETYPE_JPEG: $m = "image/jpeg"; break; - case IMAGETYPE_PNG: $m = "image/png"; break; - case IMAGETYPE_SWF: $m = "application/x-shockwave-flash"; break; - case IMAGETYPE_PSD: $m = "application/photoshop"; break; - case IMAGETYPE_BMP: $m = "image/bmp"; break; - case IMAGETYPE_TIFF_II: $m = "image/tiff"; break; - case IMAGETYPE_TIFF_MM: $m = "image/tiff"; break; - case IMAGETYPE_JPC: $m = "image"; break; - case IMAGETYPE_JP2: $m = "image/jpeg2000"; break; - case IMAGETYPE_JPX: $m = "image/jpeg2000"; break; - case IMAGETYPE_JB2: $m = "image"; break; - case IMAGETYPE_SWC: $m = "application/x-shockwave-flash"; break; - case IMAGETYPE_IFF: $m = "image/vnd.xiff"; break; - case IMAGETYPE_WBMP: $m = "image/vnd.wap.wbmp"; break; - case IMAGETYPE_XBM: $m = "image/x-xbitmap"; break; - } - - if ( $m ) { - wfDebug( __METHOD__.": image mime type of $file: $m\n" ); - return $m; - } - else { - $notAnImage = true; - } - } else { - // Also test DjVu - $deja = new DjVuImage( $file ); - if( $deja->isValid() ) { - wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" ); - return 'image/vnd.djvu'; - } - } - # if desired, look at extension as a fallback. if ( $ext === true ) { $i = strrpos( $file, '.' ); $ext = strtolower( $i ? substr( $file, $i + 1 ) : '' ); } if ( $ext ) { - $m = $this->guessTypesForExtension( $ext ); - - # TODO: if $notAnImage is set, do not trust the file extension if - # the results is one of the image types that should have been recognized - # by getimagesize - - if ( $m ) { - wfDebug( __METHOD__.": extension mime type of $file: $m\n" ); - return $m; + if( $this->isRecognizableExtension( $ext ) ) { + wfDebug( __METHOD__. ": refusing to guess mime type for .$ext file, we should have recognized it\n" ); + } else { + $m = $this->guessTypesForExtension( $ext ); + if ( $m ) { + wfDebug( __METHOD__.": extension mime type of $file: $m\n" ); + return $m; + } } } diff --git a/includes/Namespace.php b/includes/Namespace.php index f4df3bac..57a71282 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -41,6 +41,11 @@ if( is_array( $wgExtraNamespaces ) ) { * Users and translators should not change them * */ + +/* +WARNING: The statement below may fail on some versions of PHP: see bug 12294 +*/ + class Namespace { /** diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index d8ac12b5..107553fc 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -4,8 +4,21 @@ * Standard output handler for use with ob_start */ function wfOutputHandler( $s ) { - global $wgDisableOutputCompression; - $s = wfMangleFlashPolicy( $s ); + global $wgDisableOutputCompression, $wgValidateAllHtml; + $s = wfMangleFlashPolicy( $s ); + if ( $wgValidateAllHtml ) { + $headers = apache_response_headers(); + $isHTML = true; + foreach ( $headers as $name => $value ) { + if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false ) { + $isHTML = false; + break; + } + } + if ( $isHTML ) { + $s = wfHtmlValidationHandler( $s ); + } + } if ( !$wgDisableOutputCompression && !ini_get( 'zlib.output_compression' ) ) { if ( !defined( 'MW_NO_OUTPUT_COMPRESSION' ) ) { $s = wfGzipHandler( $s ); @@ -61,10 +74,12 @@ function wfGzipHandler( $s ) { return $s; } - $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] ); - if ( in_array( 'gzip', $tokens ) ) { - header( 'Content-Encoding: gzip' ); - $s = gzencode( $s, 3 ); + if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) { + $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] ); + if ( in_array( 'gzip', $tokens ) ) { + header( 'Content-Encoding: gzip' ); + $s = gzencode( $s, 3 ); + } } // Set vary header if it hasn't been set already @@ -78,6 +93,7 @@ function wfGzipHandler( $s ) { } if ( !$foundVary ) { header( 'Vary: Accept-Encoding' ); + header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip' ); } return $s; } @@ -98,4 +114,60 @@ function wfDoContentLength( $length ) { } } +/** + * Replace the output with an error if the HTML is not valid + */ +function wfHtmlValidationHandler( $s ) { + global $IP; + $tidy = new tidy; + $tidy->parseString( $s, "$IP/includes/tidy.conf", 'utf8' ); + if ( $tidy->getStatus() == 0 ) { + return $s; + } + + header( 'Cache-Control: no-cache' ); + + $out = <<<EOT +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> +<head> +<title>HTML validation error</title> +<style> +.highlight { background-color: #ffc } +li { white-space: pre } +</style> +</head> +<body> +<h1>HTML validation error</h1> +<ul> +EOT; + + $error = strtok( $tidy->errorBuffer, "\n" ); + $badLines = array(); + while ( $error !== false ) { + if ( preg_match( '/^line (\d+)/', $error, $m ) ) { + $lineNum = intval( $m[1] ); + $badLines[$lineNum] = true; + $out .= "<li><a href=\"#line-{$lineNum}\">" . htmlspecialchars( $error ) . "</a></li>\n"; + } + $error = strtok( "\n" ); + } + + $out .= '<pre>' . htmlspecialchars( $tidy->errorBuffer ) . '</pre>'; + $out .= '<ol>'; + $line = strtok( $s, "\n" ); + $i = 1; + while ( $line !== false ) { + if ( isset( $badLines[$i] ) ) { + $out .= "<li class=\"highlight\" id=\"line-$i\">"; + } else { + $out .= '<li>'; + } + $out .= htmlspecialchars( $line ) . '</li>'; + $line = strtok( "\n" ); + $i++; + } + $out .= '</ol></body></html>'; + return $out; +} diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 06467157..1fddeb7d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -23,12 +23,14 @@ class OutputPage { var $mIsArticleRelated; protected $mParserOptions; // lazy initialised, use parserOptions() var $mShowFeedLinks = false; + var $mFeedLinksAppendQuery = false; var $mEnableClientCache = true; var $mArticleBodyOnly = false; var $mNewSectionLink = false; var $mNoGallery = false; var $mPageTitleActionText = ''; + var $mParseWarnings = array(); /** * Constructor @@ -63,6 +65,10 @@ class OutputPage { $this->mRedirect = str_replace( "\n", '', $url ); $this->mRedirectCode = $responsecode; } + + public function getRedirect() { + return $this->mRedirect; + } /** * Set the HTTP status code to send with the output. @@ -228,6 +234,8 @@ class OutputPage { public function isPrintable() { return $this->mPrintable; } public function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } public function isSyndicated() { return $this->mShowFeedLinks; } + public function setFeedAppendQuery( $val ) { $this->mFeedLinksAppendQuery = $val; } + public function getFeedAppendQuery() { return $this->mFeedLinksAppendQuery; } public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } public function getOnloadHandler() { return $this->mOnloadHandler; } public function disable() { $this->mDoNothing = true; } @@ -351,10 +359,12 @@ class OutputPage { wfIncrStats('pcache_not_possible'); $popts = $this->parserOptions(); - $popts->setTidy($tidy); + $oldTidy = $popts->setTidy($tidy); $parserOutput = $wgParser->parse( $text, $title, $popts, $linestart, true, $this->mRevisionId ); + + $popts->setTidy( $oldTidy ); $this->addParserOutput( $parserOutput ); @@ -370,6 +380,7 @@ class OutputPage { $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); $this->addKeywords( $parserOutput ); + $this->mParseWarnings = $parserOutput->getWarnings(); if ( $parserOutput->getCacheTime() == -1 ) { $this->enableClientCache( false ); } @@ -514,16 +525,33 @@ class OutputPage { && $wgRequest->getText('uselang', false) === false; } + /** Get a complete X-Vary-Options header */ + public function getXVO() { + global $wgCookiePrefix; + return 'X-Vary-Options: ' . + # User ID cookie + "Cookie;string-contains={$wgCookiePrefix}UserID;" . + # Session cookie + 'string-contains=' . session_name() . ',' . + # Encoding checks for gzip only + 'Accept-Encoding;list-contains=gzip'; + } + public function sendCacheControl() { global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest; $fname = 'OutputPage::sendCacheControl'; + $response = $wgRequest->response(); if ($wgUseETag && $this->mETag) - $wgRequest->response()->header("ETag: $this->mETag"); + $response->header("ETag: $this->mETag"); # don't serve compressed data to clients who can't handle it # maintain different caches for logged-in users and non-logged in ones - $wgRequest->response()->header( 'Vary: Accept-Encoding, Cookie' ); + $response->header( 'Vary: Accept-Encoding, Cookie' ); + + # Add an X-Vary-Options header for Squid with Wikimedia patches + $response->header( $this->getXVO() ); + if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { if( $wgUseSquid && session_id() == '' && ! $this->isPrintable() && $this->mSquidMaxage != 0 ) @@ -535,8 +563,8 @@ class OutputPage { wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - $wgRequest->response()->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); - $wgRequest->response()->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); + $response->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); + $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { # We'll purge the proxy cache for anons explicitly, but require end user agents # to revalidate against the proxy on each visit. @@ -545,24 +573,24 @@ class OutputPage { wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); - $wgRequest->response()->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); + $response->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); } } else { # We do want clients to cache if they can, but they *must* check for updates # on revisiting the page. wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false ); - $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); - $wgRequest->response()->header( "Cache-Control: private, must-revalidate, max-age=0" ); + $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $response->header( "Cache-Control: private, must-revalidate, max-age=0" ); } - if($this->mLastModified) $wgRequest->response()->header( "Last-modified: {$this->mLastModified}" ); + if($this->mLastModified) $response->header( "Last-modified: {$this->mLastModified}" ); } else { wfDebug( "$fname: no caching **\n", false ); # In general, the absence of a last modified header should be enough to prevent # the client from using its cache. We send a few other things just to make sure. - $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' ); + $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $response->header( 'Pragma: no-cache' ); } } @@ -581,29 +609,10 @@ class OutputPage { } $fname = 'OutputPage::output'; wfProfileIn( $fname ); - $sk = $wgUser->getSkin(); - - if ( $wgUseAjax ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js?$wgStyleVersion\"></script>\n" ); - - wfRunHooks( 'AjaxAddScript', array( &$this ) ); - - if( $wgAjaxSearch ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js?$wgStyleVersion\"></script>\n" ); - $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); - } - - if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" ); - } - } if ( '' != $this->mRedirect ) { - if( substr( $this->mRedirect, 0, 4 ) != 'http' ) { - # Standards require redirect URLs to be absolute - global $wgServer; - $this->mRedirect = $wgServer . $this->mRedirect; - } + # Standards require redirect URLs to be absolute + $this->mRedirect = wfExpandUrl( $this->mRedirect ); if( $this->mRedirectCode == '301') { if( !$wgDebugRedirects ) { $wgRequest->response()->header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); @@ -680,6 +689,25 @@ class OutputPage { $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); } + $sk = $wgUser->getSkin(); + + if ( $wgUseAjax ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js?$wgStyleVersion\"></script>\n" ); + + wfRunHooks( 'AjaxAddScript', array( &$this ) ); + + if( $wgAjaxSearch && $wgUser->getBoolOption( 'ajaxsearch' ) ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js?$wgStyleVersion\"></script>\n" ); + $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); + } + + if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" ); + } + } + + + # Buffer output; final headers may depend on later processing ob_start(); @@ -758,6 +786,9 @@ class OutputPage { $name = User::whoIs( $wgUser->blockedBy() ); $reason = $wgUser->blockedFor(); + if( $reason == '' ) { + $reason = wfMsg( 'blockednoreason' ); + } $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); $ip = wfGetIP(); @@ -793,7 +824,7 @@ class OutputPage { * This could be a username, an ip range, or a single ip. */ $intended = $wgUser->mBlock->mAddress; - $this->addWikiText( wfMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ) ); + $this->addWikiMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); # Don't auto-return to special pages if( $return ) { @@ -811,9 +842,9 @@ class OutputPage { */ public function showErrorPage( $title, $msg, $params = array() ) { global $wgTitle; - - $this->mDebugtext .= 'Original title: ' . - $wgTitle->getPrefixedText() . "\n"; + if ( isset($wgTitle) ) { + $this->mDebugtext .= 'Original title: ' . $wgTitle->getPrefixedText() . "\n"; + } $this->setPageTitle( wfMsg( $title ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); $this->setRobotpolicy( 'noindex,nofollow' ); @@ -839,7 +870,7 @@ class OutputPage { global $wgTitle; $this->mDebugtext .= 'Original title: ' . - $wgTitle->getPrefixedText() . "\n"; + $wgTitle->getPrefixedText() . "\n"; $this->setPageTitle( wfMsg( 'permissionserrors' ) ); $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); $this->setRobotpolicy( 'noindex,nofollow' ); @@ -868,7 +899,7 @@ class OutputPage { $this->setArticleRelated( false ); $this->mBodytext = ''; - $this->addWikiText( wfMsg( 'versionrequiredtext', $version ) ); + $this->addWikiMsg( 'versionrequiredtext', $version ); $this->returnToMain(); } @@ -967,36 +998,46 @@ class OutputPage { /** * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors - * @return string The error-messages, formatted into a list. + * @return string The wikitext error-messages, formatted into a list. */ public function formatPermissionsErrorMessage( $errors ) { - $text = ''; + $text = wfMsgNoTrans( 'permissionserrorstext', count( $errors ) ) . "\n\n"; - if (sizeof( $errors ) > 1) { - - $text .= wfMsgExt( 'permissionserrorstext', array( 'parse' ), count( $errors ) ) . "\n"; + if (count( $errors ) > 1) { $text .= '<ul class="permissions-errors">' . "\n"; foreach( $errors as $error ) { $text .= '<li>'; - $text .= call_user_func_array( 'wfMsg', $error ); + $text .= call_user_func_array( 'wfMsgNoTrans', $error ); $text .= "</li>\n"; } $text .= '</ul>'; } else { - $text .= call_user_func_array( 'wfMsg', $errors[0]); + $text .= '<div class="permissions-errors">' . call_user_func_array( 'wfMsgNoTrans', $errors[0]) . '</div>'; } return $text; } /** - * @todo document - * @param bool $protected Is the reason the page can't be reached because it's protected? - * @param mixed $source - * @param bool $protected, page is protected? - * @param array $reason, array of arrays( msg, args ) + * Display a page stating that the Wiki is in read-only mode, + * and optionally show the source of the page that the user + * was trying to edit. Should only be called (for this + * purpose) after wfReadOnly() has returned true. + * + * For historical reasons, this function is _also_ used to + * show the error message when a user tries to edit a page + * they are not allowed to edit. (Unless it's because they're + * blocked, then we show blockedPage() instead.) In this + * case, the second parameter should be set to true and a list + * of reasons supplied as the third parameter. + * + * @todo Needs to be split into multiple functions. + * + * @param string $source Source code to show (or null). + * @param bool $protected Is this a permissions error? + * @param array $reasons List of reasons for this error, as returned by Title::getUserPermissionsErrors(). */ public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) { global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; @@ -1004,61 +1045,59 @@ class OutputPage { $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); - - if ( !empty($reasons) ) { - $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); - } else if( $protected ) { - $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - list( $cascadeSources, /* $restrictions */ ) = $wgTitle->getCascadeProtectionSources(); - - // Show an appropriate explanation depending upon the reason - // for the protection...all of these should be moved to the - // callers - if( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { - // User isn't allowed to edit the interface - $this->addWikiText( wfMsg( 'protectedinterface' ) ); - } elseif( $cascadeSources && ( $count = count( $cascadeSources ) ) > 0 ) { - // Cascading protection - $titles = ''; - foreach( $cascadeSources as $title ) - $titles .= "* [[:" . $title->getPrefixedText() . "]]\n"; - $this->addWikiText( wfMsgExt( 'cascadeprotected', 'parsemag', $count ) . "\n{$titles}" ); - } elseif( !$wgTitle->isProtected( 'edit' ) && $wgTitle->isNamespaceProtected() ) { - // Namespace protection - $ns = $wgTitle->getNamespace() == NS_MAIN - ? wfMsg( 'nstab-main' ) - : $wgTitle->getNsText(); - $this->addWikiText( wfMsg( 'namespaceprotected', $ns ) ); + // If no reason is given, just supply a default "I can't let you do + // that, Dave" message. Should only occur if called by legacy code. + if ( $protected && empty($reasons) ) { + $reasons[] = array( 'badaccess-group0' ); + } + + if ( !empty($reasons) ) { + // Permissions error + if( $source ) { + $this->setPageTitle( wfMsg( 'viewsource' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); } else { - // Standard protection - $this->addWikiText( wfMsg( 'protectedpagetext' ) ); + $this->setPageTitle( wfMsg( 'badaccess' ) ); } + $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); } else { + // Wiki is read only $this->setPageTitle( wfMsg( 'readonly' ) ); if ( $wgReadOnly ) { $reason = $wgReadOnly; } else { + // Should not happen, user should have called wfReadOnly() first $reason = file_get_contents( $wgReadOnlyFile ); } - $this->addWikiText( wfMsg( 'readonlytext', $reason ) ); + $this->addWikiMsg( 'readonlytext', $reason ); } + // Show source, if supplied if( is_string( $source ) ) { - $this->addWikiText( wfMsg( 'viewsourcetext' ) ); - $rows = $wgUser->getIntOption( 'rows' ); - $cols = $wgUser->getIntOption( 'cols' ); - $text = "\n<textarea name='wpTextbox1' id='wpTextbox1' cols='$cols' rows='$rows' readonly='readonly'>" . - htmlspecialchars( $source ) . "\n</textarea>"; + $this->addWikiMsg( 'viewsourcetext' ); + $text = wfOpenElement( 'textarea', + array( 'id' => 'wpTextbox1', + 'name' => 'wpTextbox1', + 'cols' => $wgUser->getOption( 'cols' ), + 'rows' => $wgUser->getOption( 'rows' ), + 'readonly' => 'readonly' ) ); + $text .= htmlspecialchars( $source ); + $text .= wfCloseElement( 'textarea' ); $this->addHTML( $text ); + + // Show templates used by this article + $skin = $wgUser->getSkin(); + $article = new Article( $wgTitle ); + $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); } - $article = new Article( $wgTitle ); - $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); - $this->returnToMain( false ); + # If the title doesn't exist, it's fairly pointless to print a return + # link to it. After all, you just tried editing it and couldn't, so + # what's there to do there? + if( $wgTitle->exists() ) { + $this->returnToMain( false, $wgTitle ); + } } /** @deprecated */ @@ -1275,28 +1314,87 @@ class OutputPage { } $ret .= " />\n"; } - if( $this->isSyndicated() ) { - # FIXME: centralize the mime-type and name information in Feed.php - $link = $wgRequest->escapeAppendQuery( 'feed=rss' ); - $ret .= "<link rel='alternate' type='application/rss+xml' title='RSS 2.0' href='$link' />\n"; - $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); - $ret .= "<link rel='alternate' type='application/atom+xml' title='Atom 1.0' href='$link' />\n"; + + foreach( $this->getSyndicationLinks() as $format => $link ) { + # Use the page name for the title (accessed through $wgTitle since + # there's no other way). In principle, this could lead to issues + # with having the same name for different feeds corresponding to + # the same page, but we can't avoid that at this low a level. + global $wgTitle; + + $ret .= $this->feedLink( + $format, + $link, + wfMsg( "page-{$format}-feed", $wgTitle->getPrefixedText() ) ); # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep) } + # Recent changes feed should appear on every page + # Put it after the per-page feed to avoid changing existing behavior. + # It's still available, probably via a menu in your browser. + global $wgSitename; + $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); + $ret .= $this->feedLink( + 'rss', + $rctitle->getFullURL( 'feed=rss' ), + wfMsg( 'site-rss-feed', $wgSitename ) ); + $ret .= $this->feedLink( + 'atom', + $rctitle->getFullURL( 'feed=atom' ), + wfMsg( 'site-atom-feed', $wgSitename ) ); + return $ret; } + + /** + * Return URLs for each supported syndication format for this page. + * @return array associating format keys with URLs + */ + public function getSyndicationLinks() { + global $wgTitle, $wgFeedClasses; + $links = array(); + + if( $this->isSyndicated() ) { + if( is_string( $this->getFeedAppendQuery() ) ) { + $appendQuery = "&" . $this->getFeedAppendQuery(); + } else { + $appendQuery = ""; + } + + foreach( $wgFeedClasses as $format => $class ) { + $links[$format] = $wgTitle->getLocalUrl( "feed=$format{$appendQuery}" ); + } + } + return $links; + } + + /** + * Generate a <link rel/> for an RSS feed. + */ + private function feedLink( $type, $url, $text ) { + return Xml::element( 'link', array( + 'rel' => 'alternate', + 'type' => "application/$type+xml", + 'title' => $text, + 'href' => $url ) ) . "\n"; + } /** * Turn off regular page output and return an error reponse * for when rate limiting has triggered. - * @todo i18n */ public function rateLimited() { - global $wgOut; - $wgOut->disable(); - wfHttpError( 500, 'Internal Server Error', - 'Sorry, the server has encountered an internal error. ' . - 'Please wait a moment and hit "refresh" to submit the request again.' ); + global $wgOut, $wgTitle; + + $this->setPageTitle(wfMsg('actionthrottled')); + $this->setRobotPolicy( 'noindex,follow' ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + $this->clearHTML(); + $this->setStatusCode(503); + $this->addWikiMsg( 'actionthrottledtext' ); + + $this->returnToMain( false, $wgTitle ); } /** @@ -1327,5 +1425,72 @@ class OutputPage { $this->addHtml( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" ); } } - + + /** + * Add a wikitext-formatted message to the output. + * This is equivalent to: + * + * $wgOut->addWikiText( wfMsgNoTrans( ... ) ) + */ + public function addWikiMsg( /*...*/ ) { + $args = func_get_args(); + $name = array_shift( $args ); + $this->addWikiMsgArray( $name, $args ); + } + + /** + * Add a wikitext-formatted message to the output. + * Like addWikiMsg() except the parameters are taken as an array + * instead of a variable argument list. + * + * $options is passed through to wfMsgExt(), see that function for details. + */ + public function addWikiMsgArray( $name, $args, $options = array() ) { + $options[] = 'parse'; + $text = wfMsgExt( $name, $options, $args ); + $this->addHTML( $text ); + } + + /** + * This function takes a number of message/argument specifications, wraps them in + * some overall structure, and then parses the result and adds it to the output. + * + * In the $wrap, $1 is replaced with the first message, $2 with the second, and so + * on. The subsequent arguments may either be strings, in which case they are the + * message names, or an arrays, in which case the first element is the message name, + * and subsequent elements are the parameters to that message. + * + * The special named parameter 'options' in a message specification array is passed + * through to the $options parameter of wfMsgExt(). + * + * For example: + * + * $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'some-error' ); + * + * Is equivalent to: + * + * $wgOut->addWikiText( '<div class="error">' . wfMsgNoTrans( 'some-error' ) . '</div>' ); + */ + public function wrapWikiMsg( $wrap /*, ...*/ ) { + $msgSpecs = func_get_args(); + array_shift( $msgSpecs ); + $msgSpecs = array_values( $msgSpecs ); + $s = $wrap; + foreach ( $msgSpecs as $n => $spec ) { + $options = array(); + if ( is_array( $spec ) ) { + $args = $spec; + $name = array_shift( $args ); + if ( isset( $args['options'] ) ) { + $options = $args['options']; + unset( $args['options'] ); + } + } else { + $args = array(); + $name = $spec; + } + $s = str_replace( '$' . ($n+1), wfMsgExt( $name, $options, $args ), $s ); + } + $this->addHTML( $this->parse( $s ) ); + } } diff --git a/includes/PageHistory.php b/includes/PageHistory.php index d84c3515..0c44682e 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -61,18 +61,17 @@ class PageHistory { /* * Setup page variables. */ - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setPageTitle( wfMsg( 'history-title', $this->mTitle->getPrefixedText() ) ); $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setSyndicated( true ); + $wgOut->setFeedAppendQuery( 'action=history' ); $logPage = SpecialPage::getTitleFor( 'Log' ); $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); - - $subtitle = wfMsgHtml( 'revhistory' ) . '<br />' . $logLink; - $wgOut->setSubtitle( $subtitle ); + $wgOut->setSubtitle( $logLink ); $feedType = $wgRequest->getVal( 'feed' ); if( $feedType ) { @@ -84,12 +83,11 @@ class PageHistory { * Fail if article doesn't exist. */ if( !$this->mTitle->exists() ) { - $wgOut->addWikiText( wfMsg( 'nohistory' ) ); + $wgOut->addWikiMsg( 'nohistory' ); wfProfileOut( $fname ); return; } - /* * "go=first" means to jump to the last (earliest) history page. * This is deprecated, it no longer appears in the user interface @@ -99,7 +97,7 @@ class PageHistory { $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); return; } - + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); /** @@ -117,7 +115,11 @@ class PageHistory { wfProfileOut( $fname ); } - /** @todo document */ + /** + * Creates begin of history list with a submit button + * + * @return string HTML output + */ function beginHistoryList() { global $wgTitle; $this->lastdate = ''; @@ -143,7 +145,11 @@ class PageHistory { return $s; } - /** @todo document */ + /** + * Creates end of history list with a submit button + * + * @return string HTML output + */ function endHistoryList() { $s = '</ul>'; $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); @@ -151,18 +157,25 @@ class PageHistory { return $s; } - /** @todo document */ + /** + * Creates a submit button + * + * @param array $bits optional CSS ID + * @return string HTML output for the submit button + */ function submitButton( $bits = array() ) { - return ( $this->linesonpage > 0 ) - ? wfElement( 'input', array_merge( $bits, - array( + # Disable submit button if history has 1 revision only + if ( $this->linesonpage > 1 ) { + return Xml::submitButton( wfMsg( 'compareselectedversions' ), + $bits + array( 'class' => 'historysubmit', - 'type' => 'submit', 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', - 'value' => wfMsg( 'compareselectedversions' ), - ) ) ) - : ''; + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + return ''; + } } /** @@ -222,11 +235,11 @@ class PageHistory { $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - if (!is_null($size = $rev->getSize())) { - if ($size == 0) - $stxt = wfMsgHtml('historyempty'); + if ( !is_null( $size = $rev->getSize() ) ) { + if ( $size == 0 ) + $stxt = wfMsgHtml( 'historyempty' ); else - $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); + $stxt = wfMsgExt( 'historysize', array( 'parsemag' ), $wgLang->formatNum( $size ) ); $s .= " <span class=\"history-size\">$stxt</span>"; } @@ -249,18 +262,22 @@ class PageHistory { $tools = array(); if ( !is_null( $next ) && is_object( $next ) ) { - if( $wgUser->isAllowed( 'rollback' ) && $latest ) { + if( !$this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) + && !$this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ) + && $latest ) { $tools[] = '<span class="mw-rollback-link">' . $this->mSkin->buildRollbackLink( $rev ) . '</span>'; } - $undolink = $this->mSkin->makeKnownLinkObj( - $this->mTitle, - wfMsgHtml( 'editundo' ), - 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() - ); - $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; + if( $this->mTitle->quickUserCan( 'edit' ) ) { + $undolink = $this->mSkin->makeKnownLinkObj( + $this->mTitle, + wfMsgHtml( 'editundo' ), + 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() + ); + $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; + } } if( $tools ) { @@ -329,14 +346,19 @@ class PageHistory { } } - /** @todo document */ + /** + * Create radio buttons for page history + * + * @param object $rev Revision + * @param bool $firstInList Is this version the first one? + * @param int $counter A counter of what row number we're at, counted from the top row = 1. + * @return string HTML output for the radio buttons + */ function diffButtons( $rev, $firstInList, $counter ) { if( $this->linesonpage > 1) { $radio = array( 'type' => 'radio', 'value' => $rev->getId(), -# do we really need to flood this on every item? -# 'title' => wfMsgHtml( 'selectolderversionfordiff' ) ); if( !$rev->userCan( Revision::DELETED_TEXT ) ) { @@ -345,7 +367,7 @@ class PageHistory { /** @todo: move title texts to javascript */ if ( $firstInList ) { - $first = wfElement( 'input', array_merge( + $first = Xml::element( 'input', array_merge( $radio, array( 'style' => 'visibility:hidden', @@ -357,13 +379,13 @@ class PageHistory { } else { $checkmark = array(); } - $first = wfElement( 'input', array_merge( + $first = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'oldid' ) ) ); $checkmark = array(); } - $second = wfElement( 'input', array_merge( + $second = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'diff' ) ) ); @@ -464,7 +486,7 @@ class PageHistory { global $wgFeedClasses; if( !isset( $wgFeedClasses[$type] ) ) { global $wgOut; - $wgOut->addWikiText( wfMsg( 'feed-invalid' ) ); + $wgOut->addWikiMsg( 'feed-invalid' ); return; } @@ -607,6 +629,3 @@ class PageHistoryPager extends ReverseChronologicalPager { return $s; } } - - - diff --git a/includes/Pager.php b/includes/Pager.php index 70d0873c..ed7086b4 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -422,21 +422,21 @@ abstract class AlphabeticPager extends IndexPager { */ function getNavigationBar() { global $wgLang; - + $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('page_first'), /* Introduced the message */ + 'prev' => wfMsgHtml( 'prevn', $wgLang->formatNum( $this->mLimit ) ), + 'next' => wfMsgHtml( 'nextn', $wgLang->formatNum($this->mLimit ) ), + 'first' => wfMsgHtml( 'page_first' ), /* Introduced the message */ 'last' => wfMsgHtml( 'page_last' ) /* Introduced the message */ ); - + $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; - + } } @@ -457,17 +457,18 @@ abstract class ReverseChronologicalPager extends IndexPager { if ( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } + $nicenumber = $wgLang->formatNum( $this->mLimit ); $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('histlast'), + 'prev' => wfMsgExt( 'pager-newer-n', array( 'parsemag' ), $nicenumber ), + 'next' => wfMsgExt( 'pager-older-n', array( 'parsemag' ), $nicenumber ), + 'first' => wfMsgHtml( 'histlast' ), 'last' => wfMsgHtml( 'histfirst' ) ); $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; @@ -712,4 +713,3 @@ abstract class TablePager extends IndexPager { */ abstract function getFieldNames(); } - diff --git a/includes/Parser.php b/includes/Parser.php index 32e7f2a8..41eabe4f 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -7,55 +7,6 @@ * @addtogroup Parser */ -/** - * Update this version number when the ParserOutput format - * changes in an incompatible way, so the parser cache - * can automatically discard old data. - */ -define( 'MW_PARSER_VERSION', '1.6.2' ); - -define( 'RLH_FOR_UPDATE', 1 ); - -# Allowed values for $mOutputType -define( 'OT_HTML', 1 ); -define( 'OT_WIKI', 2 ); -define( 'OT_MSG' , 3 ); -define( 'OT_PREPROCESS', 4 ); - -# Flags for setFunctionHook -define( 'SFH_NO_HASH', 1 ); - -# string parameter for extractTags which will cause it -# to strip HTML comments in addition to regular -# <XML>-style tags. This should not be anything we -# may want to use in wikisyntax -define( 'STRIP_COMMENTS', 'HTMLCommentStrip' ); - -# Constants needed for external link processing -define( 'HTTP_PROTOCOLS', 'http:\/\/|https:\/\/' ); -# Everything except bracket, space, or control characters -define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' ); -# Including space, but excluding newlines -define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' ); -define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' ); -define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' ); -define( 'EXT_LINK_BRACKETED', '/\[(\b(' . wfUrlProtocols() . ')'. - EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' ); -define( 'EXT_IMAGE_REGEX', - '/^('.HTTP_PROTOCOLS.')'. # Protocol - '('.EXT_LINK_URL_CLASS.'+)\\/'. # Hostname and path - '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename -); - -// State constants for the definition list colon extraction -define( 'MW_COLON_STATE_TEXT', 0 ); -define( 'MW_COLON_STATE_TAG', 1 ); -define( 'MW_COLON_STATE_TAGSTART', 2 ); -define( 'MW_COLON_STATE_CLOSETAG', 3 ); -define( 'MW_COLON_STATE_TAGSLASH', 4 ); -define( 'MW_COLON_STATE_COMMENT', 5 ); -define( 'MW_COLON_STATE_COMMENTDASH', 6 ); -define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); /** * PHP Parser - Processes wiki markup (which uses a more user-friendly @@ -64,15 +15,17 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * (which in turn the browser understands, and can display). * * <pre> - * There are four main entry points into the Parser class: + * There are five main entry points into the Parser class: * parse() * produces HTML output * preSaveTransform(). * produces altered wiki markup. - * transformMsg() - * performs brace substitution on MediaWiki messages * preprocess() * removes HTML comments and expands templates + * cleanSig() + * Cleans a signature before saving it to preferences + * extractSections() + * Extracts sections from an article for section editing * * Globals used: * objects: $wgLang, $wgContLang @@ -92,23 +45,60 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); */ class Parser { - const VERSION = MW_PARSER_VERSION; + /** + * Update this version number when the ParserOutput format + * changes in an incompatible way, so the parser cache + * can automatically discard old data. + */ + const VERSION = '1.6.4'; + + # Flags for Parser::setFunctionHook + # Also available as global constants from Defines.php + const SFH_NO_HASH = 1; + const SFH_OBJECT_ARGS = 2; + + # Constants needed for external link processing + # Everything except bracket, space, or control characters + const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]'; + const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+) + \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx'; + + // State constants for the definition list colon extraction + const COLON_STATE_TEXT = 0; + const COLON_STATE_TAG = 1; + const COLON_STATE_TAGSTART = 2; + const COLON_STATE_CLOSETAG = 3; + const COLON_STATE_TAGSLASH = 4; + const COLON_STATE_COMMENT = 5; + const COLON_STATE_COMMENTDASH = 6; + const COLON_STATE_COMMENTDASHDASH = 7; + + // Flags for preprocessToDom + const PTD_FOR_INCLUSION = 1; + + // Allowed values for $this->mOutputType + // Parameter to startExternalParse(). + const OT_HTML = 1; + const OT_WIKI = 2; + const OT_PREPROCESS = 3; + const OT_MSG = 3; + /**#@+ * @private */ # Persistent: var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables, - $mImageParams, $mImageParamsMagicArray; - + $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerSuffix, $mMarkerIndex, + $mExtLinkBracketedRegex, $mPreprocessor, $mDefaultStripList, $mVarCache, $mConf; + + # Cleared with clearState(): var $mOutput, $mAutonumber, $mDTopen, $mStripState; var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; - var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; - var $mIncludeSizes, $mDefaultSort; - var $mTemplates, // cache of already loaded templates, avoids - // multiple SQL queries for the same string - $mTemplatePath; // stores an unsorted hash of all the templates already loaded - // in this path. Used for loop detection. + var $mInterwikiLinkHolders, $mLinkHolders; + var $mIncludeSizes, $mPPNodeCount, $mDefaultSort; + var $mTplExpandCache; // empty-frame expansion cache + var $mTplRedirCache, $mTplDomCache, $mHeadings; # Temporary # These are variables reset at least once per parse regardless of $clearState @@ -127,11 +117,23 @@ class Parser * * @public */ - function Parser() { + function __construct( $conf = array() ) { + $this->mConf = $conf; $this->mTagHooks = array(); $this->mTransparentTagHooks = array(); $this->mFunctionHooks = array(); $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); + $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' ); + $this->mMarkerSuffix = "-QINU\x7f"; + $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'. + '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S'; + $this->mVarCache = array(); + if ( isset( $conf['preprocessorClass'] ) ) { + $this->mPreprocessorClass = $conf['preprocessorClass']; + } else { + $this->mPreprocessorClass = 'Preprocessor_DOM'; + } + $this->mMarkerIndex = 0; $this->mFirstCall = true; } @@ -142,38 +144,46 @@ class Parser if ( !$this->mFirstCall ) { return; } + $this->mFirstCall = false; wfProfileIn( __METHOD__ ); global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; - + $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); - - $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); - $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); - $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); - $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); - $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); - $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); - $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); - $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); - $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); - $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); - $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); - $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); + + # Syntax for arguments (see self::setFunctionHook): + # "name for lookup in localized magic words array", + # function callback, + # optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...} + # instead of {{#int:...}}) + $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); + $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); + $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); + $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); - $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); - $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); - $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); - $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); - $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); - $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); + $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); + $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); + $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); + $this->setFunctionHook( 'filepath', array( 'CoreParserFunctions', 'filepath' ), SFH_NO_HASH ); + $this->setFunctionHook( 'tag', array( 'CoreParserFunctions', 'tagObj' ), SFH_OBJECT_ARGS ); if ( $wgAllowDisplayTitle ) { $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); @@ -183,7 +193,8 @@ class Parser } $this->initialiseVariables(); - $this->mFirstCall = false; + + wfRunHooks( 'ParserFirstCallInit', array( &$this ) ); wfProfileOut( __METHOD__ ); } @@ -203,7 +214,7 @@ class Parser $this->mDTopen = false; $this->mIncludeCount = array(); $this->mStripState = new StripState; - $this->mArgStack = array(); + $this->mArgStack = false; $this->mInPre = false; $this->mInterwikiLinkHolders = array( 'texts' => array(), @@ -224,21 +235,32 @@ class Parser * Using it at the front also gives us a little extra robustness * since it shouldn't match when butted up against identifier-like * string constructs. + * + * Must not consist of all title characters, or else it will change + * the behaviour of <nowiki> in a link. */ - $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString(); + #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString(); + # Changed to \x7f to allow XML double-parsing -- TS + $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString(); + # Clear these on every parse, bug 4549 - $this->mTemplates = array(); - $this->mTemplatePath = array(); + $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array(); $this->mShowToc = true; $this->mForceTocPosition = false; $this->mIncludeSizes = array( - 'pre-expand' => 0, 'post-expand' => 0, - 'arg' => 0 + 'arg' => 0, ); + $this->mPPNodeCount = 0; $this->mDefaultSort = false; + $this->mHeadings = array(); + + # Fix cloning + if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) { + $this->mPreprocessor = null; + } wfRunHooks( 'ParserClearState', array( &$this ) ); wfProfileOut( __METHOD__ ); @@ -248,19 +270,43 @@ class Parser $this->mOutputType = $ot; // Shortcut alias $this->ot = array( - 'html' => $ot == OT_HTML, - 'wiki' => $ot == OT_WIKI, - 'msg' => $ot == OT_MSG, - 'pre' => $ot == OT_PREPROCESS, + 'html' => $ot == self::OT_HTML, + 'wiki' => $ot == self::OT_WIKI, + 'pre' => $ot == self::OT_PREPROCESS, ); } /** + * Set the context title + */ + function setTitle( $t ) { + if ( !$t || $t instanceof FakeTitle ) { + $t = Title::newFromText( 'NO TITLE' ); + } + if ( strval( $t->getFragment() ) !== '' ) { + # Strip the fragment to avoid various odd effects + $this->mTitle = clone $t; + $this->mTitle->setFragment( '' ); + } else { + $this->mTitle = $t; + } + } + + /** * Accessor for mUniqPrefix. * * @public */ function uniqPrefix() { + if( !isset( $this->mUniqPrefix ) ) { + // @fixme this is probably *horribly wrong* + // LanguageConverter seems to want $wgParser's uniqPrefix, however + // if this is called for a parser cache hit, the parser may not + // have ever been initialized in the first place. + // Not really sure what the heck is supposed to be going on here. + return ''; + //throw new MWException( "Accessing uninitialized mUniqPrefix" ); + } return $this->mUniqPrefix; } @@ -292,16 +338,16 @@ class Parser } $this->mOptions = $options; - $this->mTitle =& $title; + $this->setTitle( $title ); $oldRevisionId = $this->mRevisionId; $oldRevisionTimestamp = $this->mRevisionTimestamp; if( $revid !== null ) { $this->mRevisionId = $revid; $this->mRevisionTimestamp = null; } - $this->setOutputType( OT_HTML ); + $this->setOutputType( self::OT_HTML ); wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); + # No more strip! wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); $text = $this->mStripState->unstripGeneral( $text ); @@ -334,17 +380,17 @@ class Parser //!JF Move to its own function $uniq_prefix = $this->mUniqPrefix; - $matches = array(); + $matches = array(); $elements = array_keys( $this->mTransparentTagHooks ); - $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); - - foreach( $matches as $marker => $data ) { - list( $element, $content, $params, $tag ) = $data; - $tagName = strtolower( $element ); - if( isset( $this->mTransparentTagHooks[$tagName] ) ) { - $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], - array( $content, $params, $this ) ); - } else { + $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + $tagName = strtolower( $element ); + if( isset( $this->mTransparentTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { $output = $tag; } $this->mStripState->general->setPair( $marker, $output ); @@ -386,14 +432,15 @@ class Parser wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); # Information on include size limits, for the benefit of users who try to skirt them - if ( max( $this->mIncludeSizes ) > 1000 ) { + if ( $this->mOptions->getEnableLimitReport() ) { $max = $this->mOptions->getMaxIncludeSize(); - $text .= "<!-- \n" . - "Pre-expand include size: {$this->mIncludeSizes['pre-expand']} bytes\n" . - "Post-expand include size: {$this->mIncludeSizes['post-expand']} bytes\n" . - "Template argument size: {$this->mIncludeSizes['arg']} bytes\n" . - "Maximum: $max bytes\n" . - "-->\n"; + $limitReport = + "NewPP limit report\n" . + "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" . + "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" . + "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n"; + wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) ); + $text .= "\n<!-- \n$limitReport-->\n"; } $this->mOutput->setText( $text ); $this->mRevisionId = $oldRevisionId; @@ -411,7 +458,6 @@ class Parser function recursiveTagParse( $text ) { wfProfileIn( __METHOD__ ); wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); wfProfileOut( __METHOD__ ); @@ -425,18 +471,14 @@ class Parser function preprocess( $text, $title, $options, $revid = null ) { wfProfileIn( __METHOD__ ); $this->clearState(); - $this->setOutputType( OT_PREPROCESS ); + $this->setOutputType( self::OT_PREPROCESS ); $this->mOptions = $options; - $this->mTitle = $title; + $this->setTitle( $title ); if( $revid !== null ) { $this->mRevisionId = $revid; } wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); - if ( $this->mOptions->getRemoveComments() ) { - $text = Sanitizer::removeHTMLcomments( $text ); - } $text = $this->replaceVariables( $text ); $text = $this->mStripState->unstripBoth( $text ); wfProfileOut( __METHOD__ ); @@ -462,8 +504,19 @@ class Parser } /** + * Get a preprocessor object + */ + function getPreprocessor() { + if ( !isset( $this->mPreprocessor ) ) { + $class = $this->mPreprocessorClass; + $this->mPreprocessor = new $class( $this ); + } + return $this->mPreprocessor; + } + + /** * Replaces all occurrences of HTML-style comments and the given tags - * in the text with a random marker and returns teh next text. The output + * in the text with a random marker and returns the next text. The output * parameter $matches will be an associative array filled with data in * the form: * 'UNIQ-xxxxx' => array( @@ -507,7 +560,7 @@ class Parser $inside = $p[4]; } - $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07"; + $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . $this->mMarkerSuffix; $stripped .= $marker; if ( $close === '/>' ) { @@ -542,125 +595,24 @@ class Parser } /** - * Strips and renders nowiki, pre, math, hiero - * If $render is set, performs necessary rendering operations on plugins - * Returns the text, and fills an array with data needed in unstrip() - * - * @param StripState $state - * - * @param bool $stripcomments when set, HTML comments <!-- like this --> - * will be stripped in addition to other tags. This is important - * for section editing, where these comments cause confusion when - * counting the sections in the wikisource - * - * @param array dontstrip contains tags which should not be stripped; - * used to prevent stipping of <gallery> when saving (fixes bug 2700) - * - * @private + * Get a list of strippable XML-like elements */ - function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { - global $wgContLang; - wfProfileIn( __METHOD__ ); - $render = ($this->mOutputType == OT_HTML); - - $uniq_prefix = $this->mUniqPrefix; - $commentState = new ReplacementArray; - $nowikiItems = array(); - $generalItems = array(); - - $elements = array_merge( - array( 'nowiki', 'gallery' ), - array_keys( $this->mTagHooks ) ); + function getStripList() { global $wgRawHtml; + $elements = $this->mStripList; if( $wgRawHtml ) { $elements[] = 'html'; } if( $this->mOptions->getUseTeX() ) { $elements[] = 'math'; } + return $elements; + } - # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700) - foreach ( $elements AS $k => $v ) { - if ( !in_array ( $v , $dontstrip ) ) continue; - unset ( $elements[$k] ); - } - - $matches = array(); - $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); - - foreach( $matches as $marker => $data ) { - list( $element, $content, $params, $tag ) = $data; - if( $render ) { - $tagName = strtolower( $element ); - wfProfileIn( __METHOD__."-render-$tagName" ); - switch( $tagName ) { - case '!--': - // Comment - if( substr( $tag, -3 ) == '-->' ) { - $output = $tag; - } else { - // Unclosed comment in input. - // Close it so later stripping can remove it - $output = "$tag-->"; - } - break; - case 'html': - if( $wgRawHtml ) { - $output = $content; - break; - } - // Shouldn't happen otherwise. :) - case 'nowiki': - $output = Xml::escapeTagsOnly( $content ); - break; - case 'math': - $output = $wgContLang->armourMath( - MathRenderer::renderMath( $content, $params ) ); - break; - case 'gallery': - $output = $this->renderImageGallery( $content, $params ); - break; - default: - if( isset( $this->mTagHooks[$tagName] ) ) { - $output = call_user_func_array( $this->mTagHooks[$tagName], - array( $content, $params, $this ) ); - } else { - throw new MWException( "Invalid call hook $element" ); - } - } - wfProfileOut( __METHOD__."-render-$tagName" ); - } else { - // Just stripping tags; keep the source - $output = $tag; - } - - // Unstrip the output, to support recursive strip() calls - $output = $state->unstripBoth( $output ); - - if( !$stripcomments && $element == '!--' ) { - $commentState->setPair( $marker, $output ); - } elseif ( $element == 'html' || $element == 'nowiki' ) { - $nowikiItems[$marker] = $output; - } else { - $generalItems[$marker] = $output; - } - } - # Add the new items to the state - # We do this after the loop instead of during it to avoid slowing - # down the recursive unstrip - $state->nowiki->mergeArray( $nowikiItems ); - $state->general->mergeArray( $generalItems ); - - # Unstrip comments unless explicitly told otherwise. - # (The comments are always stripped prior to this point, so as to - # not invoke any extension tags / parser hooks contained within - # a comment.) - if ( !$stripcomments ) { - // Put them all back and forget them - $text = $commentState->replace( $text ); - } - - wfProfileOut( __METHOD__ ); + /** + * @deprecated use replaceVariables + */ + function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { return $text; } @@ -699,9 +651,10 @@ class Parser * * @private */ - function insertStripItem( $text, &$state ) { - $rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString(); - $state->general->setPair( $rnd, $text ); + function insertStripItem( $text ) { + $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-{$this->mMarkerSuffix}"; + $this->mMarkerIndex++; + $this->mStripState->general->setPair( $rnd, $text ); return $rnd; } @@ -785,8 +738,7 @@ class Parser /** * Use the HTML tidy PECL extension to use the tidy library in-process, - * saving the overhead of spawning a new process. Currently written to - * the PHP 4.3.x version of the extension, may not work on PHP 5. + * saving the overhead of spawning a new process. * * 'pear install tidy' should be able to compile the extension module. * @@ -794,21 +746,26 @@ class Parser * @static */ function internalTidy( $text ) { - global $wgTidyConf; + global $wgTidyConf, $IP, $wgDebugTidy; $fname = 'Parser::internalTidy'; wfProfileIn( $fname ); - tidy_load_config( $wgTidyConf ); - tidy_set_encoding( 'utf8' ); - tidy_parse_string( $text ); - tidy_clean_repair(); - if( tidy_get_status() == 2 ) { + $tidy = new tidy; + $tidy->parseString( $text, $wgTidyConf, 'utf8' ); + $tidy->cleanRepair(); + if( $tidy->getStatus() == 2 ) { // 2 is magic number for fatal error // http://www.php.net/manual/en/function.tidy-get-status.php $cleansource = null; } else { - $cleansource = tidy_get_output(); + $cleansource = tidy_get_output( $tidy ); + } + if ( $wgDebugTidy && $tidy->getStatus() > 0 ) { + $cleansource .= "<!--\nTidy reports:\n" . + str_replace( '-->', '-->', $tidy->errorBuffer ) . + "\n-->"; } + wfProfileOut( $fname ); return $cleansource; } @@ -1007,12 +964,11 @@ class Parser /** * Helper function for parse() that transforms wiki markup into - * HTML. Only called for $mOutputType == OT_HTML. + * HTML. Only called for $mOutputType == self::OT_HTML. * * @private */ function internalParse( $text ) { - $args = array(); $isMain = true; $fname = 'Parser::internalParse'; wfProfileIn( $fname ); @@ -1023,14 +979,8 @@ class Parser return $text ; } - # Remove <noinclude> tags and <includeonly> sections - $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) ); - $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') ); - $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text ); - - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) ); - - $text = $this->replaceVariables( $text, $args ); + $text = $this->replaceVariables( $text ); + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) ); wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); // Tables need to come after variable replacement for things to work @@ -1069,7 +1019,7 @@ class Parser * * @private */ - function &doMagicLinks( &$text ) { + function doMagicLinks( $text ) { wfProfileIn( __METHOD__ ); $text = preg_replace_callback( '!(?: # Start cases @@ -1133,8 +1083,8 @@ class Parser wfProfileIn( $fname ); for ( $i = 6; $i >= 1; --$i ) { $h = str_repeat( '=', $i ); - $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m", - "<h{$i}>\\1</h{$i}>\\2", $text ); + $text = preg_replace( "/^$h(.+)$h\\s*$/m", + "<h$i>\\1</h$i>", $text ); } wfProfileOut( $fname ); return $text; @@ -1160,9 +1110,8 @@ class Parser /** * Helper function for doAllQuotes() - * @private */ - function doQuotes( $text ) { + public function doQuotes( $text ) { $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); if ( count( $arr ) == 1 ) return $text; @@ -1339,7 +1288,7 @@ class Parser $sk = $this->mOptions->getSkin(); - $bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); $s = $this->replaceFreeExternalLinks( array_shift( $bits ) ); @@ -1433,7 +1382,7 @@ class Parser $remainder = $bits[$i++]; $m = array(); - if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { + if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { # Found some characters after the protocol that look promising $url = $protocol . $m[1]; $trail = $m[2]; @@ -1443,7 +1392,7 @@ class Parser if(strlen($trail) == 0 && isset($bits[$i]) && preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && - preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) + preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) { # add protocol, arg $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link @@ -1540,7 +1489,7 @@ class Parser $text = false; if ( $this->mOptions->getAllowExternalImages() || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { - if ( preg_match( EXT_IMAGE_REGEX, $url ) ) { + if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { # Image found $text = $sk->makeExternalImage( htmlspecialchars( $url ) ); } @@ -1578,11 +1527,15 @@ class Parser # Match cases where there is no "]]", which might still be images static $e1_img = FALSE; if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; } - # Match the end of a line for a word that's not followed by whitespace, - # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched - $e2 = wfMsgForContent( 'linkprefix' ); $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); + $e2 = null; + if ( $useLinkPrefixExtension ) { + # Match the end of a line for a word that's not followed by whitespace, + # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched + $e2 = wfMsgForContent( 'linkprefix' ); + } + if( is_null( $this->mTitle ) ) { throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); } @@ -2283,7 +2236,7 @@ class Parser } // Ugly state machine to walk through avoiding tags. - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; $stack = 0; $len = strlen( $str ); for( $i = 0; $i < $len; $i++ ) { @@ -2291,11 +2244,11 @@ class Parser switch( $state ) { // (Using the number is a performance hack for common cases) - case 0: // MW_COLON_STATE_TEXT: + case 0: // self::COLON_STATE_TEXT: switch( $c ) { case "<": // Could be either a <start> tag or an </end> tag - $state = MW_COLON_STATE_TAGSTART; + $state = self::COLON_STATE_TAGSTART; break; case ":": if( $stack == 0 ) { @@ -2332,41 +2285,41 @@ class Parser } // Skip ahead to next tag start $i = $lt; - $state = MW_COLON_STATE_TAGSTART; + $state = self::COLON_STATE_TAGSTART; } break; - case 1: // MW_COLON_STATE_TAG: + case 1: // self::COLON_STATE_TAG: // In a <tag> switch( $c ) { case ">": $stack++; - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; break; case "/": // Slash may be followed by >? - $state = MW_COLON_STATE_TAGSLASH; + $state = self::COLON_STATE_TAGSLASH; break; default: // ignore } break; - case 2: // MW_COLON_STATE_TAGSTART: + case 2: // self::COLON_STATE_TAGSTART: switch( $c ) { case "/": - $state = MW_COLON_STATE_CLOSETAG; + $state = self::COLON_STATE_CLOSETAG; break; case "!": - $state = MW_COLON_STATE_COMMENT; + $state = self::COLON_STATE_COMMENT; break; case ">": // Illegal early close? This shouldn't happen D: - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; break; default: - $state = MW_COLON_STATE_TAG; + $state = self::COLON_STATE_TAG; } break; - case 3: // MW_COLON_STATE_CLOSETAG: + case 3: // self::COLON_STATE_CLOSETAG: // In a </tag> if( $c == ">" ) { $stack--; @@ -2375,35 +2328,35 @@ class Parser wfProfileOut( $fname ); return false; } - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; } break; - case MW_COLON_STATE_TAGSLASH: + case self::COLON_STATE_TAGSLASH: if( $c == ">" ) { // Yes, a self-closed tag <blah/> - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; } else { // Probably we're jumping the gun, and this is an attribute - $state = MW_COLON_STATE_TAG; + $state = self::COLON_STATE_TAG; } break; - case 5: // MW_COLON_STATE_COMMENT: + case 5: // self::COLON_STATE_COMMENT: if( $c == "-" ) { - $state = MW_COLON_STATE_COMMENTDASH; + $state = self::COLON_STATE_COMMENTDASH; } break; - case MW_COLON_STATE_COMMENTDASH: + case self::COLON_STATE_COMMENTDASH: if( $c == "-" ) { - $state = MW_COLON_STATE_COMMENTDASHDASH; + $state = self::COLON_STATE_COMMENTDASHDASH; } else { - $state = MW_COLON_STATE_COMMENT; + $state = self::COLON_STATE_COMMENT; } break; - case MW_COLON_STATE_COMMENTDASHDASH: + case self::COLON_STATE_COMMENTDASHDASH: if( $c == ">" ) { - $state = MW_COLON_STATE_TEXT; + $state = self::COLON_STATE_TEXT; } else { - $state = MW_COLON_STATE_COMMENT; + $state = self::COLON_STATE_COMMENT; } break; default: @@ -2430,14 +2383,13 @@ class Parser * Some of these require message or data lookups and can be * expensive to check many times. */ - static $varCache = array(); - if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) { - if ( isset( $varCache[$index] ) ) { - return $varCache[$index]; + if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) { + if ( isset( $this->mVarCache[$index] ) ) { + return $this->mVarCache[$index]; } } - $ts = time(); + $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() ); wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); # Use the time zone @@ -2464,29 +2416,29 @@ class Parser switch ( $index ) { case 'currentmonth': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); case 'currentmonthname': - return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); case 'currentmonthnamegen': - return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); case 'currentmonthabbrev': - return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); case 'currentday': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); case 'currentday2': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); case 'localmonth': - return $varCache[$index] = $wgContLang->formatNum( $localMonth ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth ); case 'localmonthname': - return $varCache[$index] = $wgContLang->getMonthName( $localMonthName ); + return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName ); case 'localmonthnamegen': - return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); + return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); case 'localmonthabbrev': - return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); + return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); case 'localday': - return $varCache[$index] = $wgContLang->formatNum( $localDay ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay ); case 'localday2': - return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 ); case 'pagename': return wfEscapeWikiText( $this->mTitle->getText() ); case 'pagenamee': @@ -2524,16 +2476,40 @@ class Parser $subjPage = $this->mTitle->getSubjectPage(); return $subjPage->getPrefixedUrl(); case 'revisionid': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" ); return $this->mRevisionId; case 'revisionday': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. This is for null edits. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" ); return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); case 'revisionday2': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. This is for null edits. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" ); return substr( $this->getRevisionTimestamp(), 6, 2 ); case 'revisionmonth': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. This is for null edits. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" ); return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); case 'revisionyear': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. This is for null edits. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" ); return substr( $this->getRevisionTimestamp(), 0, 4 ); case 'revisiontimestamp': + // Let the edit saving system know we should parse the page + // *after* a revision ID has been assigned. This is for null edits. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" ); return $this->getRevisionTimestamp(); case 'namespace': return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); @@ -2548,51 +2524,51 @@ class Parser case 'subjectspacee': return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); case 'currentdayname': - return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); + return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); case 'currentyear': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true ); case 'currenttime': - return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); + return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); case 'currenthour': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true ); case 'currentweek': // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to // int to remove the padding - return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); case 'currentdow': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); + return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); case 'localdayname': - return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); + return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); case 'localyear': - return $varCache[$index] = $wgContLang->formatNum( $localYear, true ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true ); case 'localtime': - return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false ); + return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false ); case 'localhour': - return $varCache[$index] = $wgContLang->formatNum( $localHour, true ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true ); case 'localweek': // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to // int to remove the padding - return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek ); + return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek ); case 'localdow': - return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); + return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); case 'numberofarticles': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); case 'numberoffiles': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() ); case 'numberofusers': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() ); case 'numberofpages': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); case 'numberofadmins': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); case 'numberofedits': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); case 'currenttimestamp': - return $varCache[$index] = wfTimestampNow(); + return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts ); case 'localtimestamp': - return $varCache[$index] = $localTimestamp; + return $this->mVarCache[$index] = $localTimestamp; case 'currentversion': - return $varCache[$index] = SpecialVersion::getVersion(); + return $this->mVarCache[$index] = SpecialVersion::getVersion(); case 'sitename': return $wgSitename; case 'server': @@ -2608,7 +2584,7 @@ class Parser return $wgContLanguageCode; default: $ret = null; - if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) ) + if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) ) return $ret; else return null; @@ -2625,187 +2601,51 @@ class Parser wfProfileIn( $fname ); $variableIDs = MagicWord::getVariableIDs(); - $this->mVariables = array(); - foreach ( $variableIDs as $id ) { - $mw =& MagicWord::get( $id ); - $mw->addToArray( $this->mVariables, $id ); - } + $this->mVariables = new MagicWordArray( $variableIDs ); wfProfileOut( $fname ); } /** - * parse any parentheses in format ((title|part|part)) - * and call callbacks to get a replacement text for any found piece + * Preprocess some wikitext and return the document tree. + * This is the ghost of replace_variables(). * * @param string $text The text to parse - * @param array $callbacks rules in form: - * '{' => array( # opening parentheses - * 'end' => '}', # closing parentheses - * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found - * 3 => callback # replacement callback to call if {{{..}}} is found - * ) - * ) - * 'min' => 2, # Minimum parenthesis count in cb - * 'max' => 3, # Maximum parenthesis count in cb + * @param integer flags Bitwise combination of: + * self::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * included. Default is to assume a direct page view. + * + * The generated DOM tree must depend only on the input text and the flags. + * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. + * + * Any flag added to the $flags parameter here, or any other parameter liable to cause a + * change in the DOM tree for a given text, must be passed through the section identifier + * in the section edit link and thus back to extractSections(). + * + * The output of this function is currently only cached in process memory, but a persistent + * cache may be implemented at a later date which takes further advantage of these strict + * dependency requirements. + * * @private */ - function replace_callback ($text, $callbacks) { - wfProfileIn( __METHOD__ ); - $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet - $lastOpeningBrace = -1; # last not closed parentheses - - $validOpeningBraces = implode( '', array_keys( $callbacks ) ); - - $i = 0; - while ( $i < strlen( $text ) ) { - # Find next opening brace, closing brace or pipe - if ( $lastOpeningBrace == -1 ) { - $currentClosing = ''; - $search = $validOpeningBraces; - } else { - $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd']; - $search = $validOpeningBraces . '|' . $currentClosing; - } - $rule = null; - $i += strcspn( $text, $search, $i ); - if ( $i < strlen( $text ) ) { - if ( $text[$i] == '|' ) { - $found = 'pipe'; - } elseif ( $text[$i] == $currentClosing ) { - $found = 'close'; - } elseif ( isset( $callbacks[$text[$i]] ) ) { - $found = 'open'; - $rule = $callbacks[$text[$i]]; - } else { - # Some versions of PHP have a strcspn which stops on null characters - # Ignore and continue - ++$i; - continue; - } - } else { - # All done - break; - } - - if ( $found == 'open' ) { - # found opening brace, let's add it to parentheses stack - $piece = array('brace' => $text[$i], - 'braceEnd' => $rule['end'], - 'title' => '', - 'parts' => null); - - # count opening brace characters - $piece['count'] = strspn( $text, $piece['brace'], $i ); - $piece['startAt'] = $piece['partStart'] = $i + $piece['count']; - $i += $piece['count']; - - # we need to add to stack only if opening brace count is enough for one of the rules - if ( $piece['count'] >= $rule['min'] ) { - $lastOpeningBrace ++; - $openingBraceStack[$lastOpeningBrace] = $piece; - } - } elseif ( $found == 'close' ) { - # lets check if it is enough characters for closing brace - $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; - $count = strspn( $text, $text[$i], $i, $maxCount ); - - # check for maximum matching characters (if there are 5 closing - # characters, we will probably need only 3 - depending on the rules) - $matchingCount = 0; - $matchingCallback = null; - $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; - if ( $count > $cbType['max'] ) { - # The specified maximum exists in the callback array, unless the caller - # has made an error - $matchingCount = $cbType['max']; - } else { - # Count is less than the maximum - # Skip any gaps in the callback array to find the true largest match - # Need to use array_key_exists not isset because the callback can be null - $matchingCount = $count; - while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) { - --$matchingCount; - } - } - - if ($matchingCount <= 0) { - $i += $count; - continue; - } - $matchingCallback = $cbType['cb'][$matchingCount]; - - # let's set a title or last part (if '|' was found) - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } - - $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; - $pieceEnd = $i + $matchingCount; - - if( is_callable( $matchingCallback ) ) { - $cbArgs = array ( - 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), - 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), - 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], - 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), - ); - # finally we can call a user callback and replace piece of text - $replaceWith = call_user_func( $matchingCallback, $cbArgs ); - $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); - $i = $pieceStart + strlen($replaceWith); - } else { - # null value for callback means that parentheses should be parsed, but not replaced - $i += $matchingCount; - } + function preprocessToDom ( $text, $flags = 0 ) { + $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags ); + return $dom; + } - # reset last opening parentheses, but keep it in case there are unused characters - $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], - 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], - 'count' => $openingBraceStack[$lastOpeningBrace]['count'], - 'title' => '', - 'parts' => null, - 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); - $openingBraceStack[$lastOpeningBrace--] = null; - - if ($matchingCount < $piece['count']) { - $piece['count'] -= $matchingCount; - $piece['startAt'] -= $matchingCount; - $piece['partStart'] = $piece['startAt']; - # do we still qualify for any callback with remaining count? - $currentCbList = $callbacks[$piece['brace']]['cb']; - while ( $piece['count'] ) { - if ( array_key_exists( $piece['count'], $currentCbList ) ) { - $lastOpeningBrace++; - $openingBraceStack[$lastOpeningBrace] = $piece; - break; - } - --$piece['count']; - } - } - } elseif ( $found == 'pipe' ) { - # lets set a title if it is a first separator, or next part otherwise - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - $openingBraceStack[$lastOpeningBrace]['parts'] = array(); - } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } - $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; - } + /* + * Return a three-element array: leading whitespace, string contents, trailing whitespace + */ + public static function splitWhitespace( $s ) { + $ltrimmed = ltrim( $s ); + $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) ); + $trimmed = rtrim( $ltrimmed ); + $diff = strlen( $ltrimmed ) - strlen( $trimmed ); + if ( $diff > 0 ) { + $w2 = substr( $ltrimmed, -$diff ); + } else { + $w2 = ''; } - - wfProfileOut( __METHOD__ ); - return $text; + return array( $w1, $trimmed, $w2 ); } /** @@ -2814,94 +2654,38 @@ class Parser * taking care to avoid infinite loops. * * Note that the substitution depends on value of $mOutputType: - * OT_WIKI: only {{subst:}} templates - * OT_MSG: only magic variables - * OT_HTML: all templates and magic variables + * self::OT_WIKI: only {{subst:}} templates + * self::OT_PREPROCESS: templates but not extension tags + * self::OT_HTML: all templates and extension tags * * @param string $tex The text to transform - * @param array $args Key-value pairs representing template parameters to substitute + * @param PPFrame $frame Object describing the arguments passed to the template * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion * @private */ - function replaceVariables( $text, $args = array(), $argsOnly = false ) { + function replaceVariables( $text, $frame = false, $argsOnly = false ) { # Prevent too big inclusions if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { return $text; } - $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; + $fname = __METHOD__; wfProfileIn( $fname ); - # This function is called recursively. To keep track of arguments we need a stack: - array_push( $this->mArgStack, $args ); - - $braceCallbacks = array(); - if ( !$argsOnly ) { - $braceCallbacks[2] = array( &$this, 'braceSubstitution' ); - } - if ( $this->mOutputType != OT_MSG ) { - $braceCallbacks[3] = array( &$this, 'argSubstitution' ); - } - if ( $braceCallbacks ) { - $callbacks = array( - '{' => array( - 'end' => '}', - 'cb' => $braceCallbacks, - 'min' => $argsOnly ? 3 : 2, - 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, - ), - '[' => array( - 'end' => ']', - 'cb' => array(2=>null), - 'min' => 2, - 'max' => 2, - ) - ); - $text = $this->replace_callback ($text, $callbacks); - - array_pop( $this->mArgStack ); + if ( $frame === false ) { + $frame = $this->getPreprocessor()->newFrame(); + } elseif ( !( $frame instanceof PPFrame ) ) { + throw new MWException( __METHOD__ . ' called using the old argument format' ); } - wfProfileOut( $fname ); - return $text; - } - /** - * Replace magic variables - * @private - */ - function variableSubstitution( $matches ) { - global $wgContLang; - $fname = 'Parser::variableSubstitution'; - $varname = $wgContLang->lc($matches[1]); - wfProfileIn( $fname ); - $skip = false; - if ( $this->mOutputType == OT_WIKI ) { - # Do only magic variables prefixed by SUBST - $mwSubst =& MagicWord::get( 'subst' ); - if (!$mwSubst->matchStartAndRemove( $varname )) - $skip = true; - # Note that if we don't substitute the variable below, - # we don't remove the {{subst:}} magic word, in case - # it is a template rather than a magic variable. - } - if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) { - $id = $this->mVariables[$varname]; - # Now check if we did really match, case sensitive or not - $mw =& MagicWord::get( $id ); - if ($mw->match($matches[1])) { - $text = $this->getVariableValue( $id ); - $this->mOutput->mContainsOldMagic = true; - } else { - $text = $matches[0]; - } - } else { - $text = $matches[0]; - } + $dom = $this->preprocessToDom( $text ); + $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0; + $text = $frame->expand( $dom, $flags ); + wfProfileOut( $fname ); return $text; } - /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. static function createAssocArgs( $args ) { $assocArgs = array(); @@ -2930,50 +2714,40 @@ class Parser * replacing any variables or templates within the template. * * @param array $piece The parts of the template - * $piece['text']: matched text * $piece['title']: the title, i.e. the part before the | * $piece['parts']: the parameter array + * $piece['lineStart']: whether the brace was at the start of a line + * @param PPFrame The current frame, contains template arguments * @return string the text of the template * @private */ - function braceSubstitution( $piece ) { + function braceSubstitution( $piece, $frame ) { global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; - $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; + $fname = __METHOD__; wfProfileIn( $fname ); wfProfileIn( __METHOD__.'-setup' ); # Flags $found = false; # $text has been filled $nowiki = false; # wiki markup in $text should be escaped - $noparse = false; # Unsafe HTML tags should not be stripped, etc. - $noargs = false; # Don't replace triple-brace arguments in $text - $replaceHeadings = false; # Make the edit section links go to the template not the article - $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. $isHTML = false; # $text is HTML, armour it against wikitext transformation $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered + $isChildObj = false; # $text is a DOM node needing expansion in a child frame + $isLocalObj = false; # $text is a DOM node needing expansion in the current frame # Title object, where $text came from $title = NULL; - $linestart = ''; - - - # $part1 is the bit before the first |, and must contain only title characters - # $args is a list of arguments, starting from index 0, not including $part1 + # $part1 is the bit before the first |, and must contain only title characters. + # Various prefixes will be stripped from it later. + $titleWithSpaces = $frame->expand( $piece['title'] ); + $part1 = trim( $titleWithSpaces ); + $titleText = false; - $titleText = $part1 = $piece['title']; - # If the third subpattern matched anything, it will start with | - - if (null == $piece['parts']) { - $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title'])); - if ($replaceWith != $piece['text']) { - $text = $replaceWith; - $found = true; - $noparse = true; - $noargs = true; - } - } + # Original title text preserved for various purposes + $originalTitle = $part1; + # $args is a list of argument nodes, starting from index 0, not including $part1 $args = (null == $piece['parts']) ? array() : $piece['parts']; wfProfileOut( __METHOD__.'-setup' ); @@ -2986,10 +2760,20 @@ class Parser # 1) Found SUBST but not in the PST phase # 2) Didn't find SUBST and in the PST phase # In either case, return without further processing - $text = $piece['text']; + $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); + $isLocalObj = true; + $found = true; + } + } + + # Variables + if ( !$found && $args->getLength() == 0 ) { + $id = $this->mVariables->matchStartToEnd( $part1 ); + if ( $id !== false ) { + $text = $this->getVariableValue( $id ); + if (MagicWord::getCacheTTL($id)>-1) + $this->mOutput->mContainsOldMagic = true; $found = true; - $noparse = true; - $noargs = true; } } @@ -3013,9 +2797,6 @@ class Parser } wfProfileOut( __METHOD__.'-modifiers' ); - //save path level before recursing into functions & templates. - $lastPathLevel = $this->mTemplatePath; - # Parser functions if ( !$found ) { wfProfileIn( __METHOD__ . '-pfunc' ); @@ -3036,288 +2817,278 @@ class Parser } } if ( $function ) { - $funcArgs = array_map( 'trim', $args ); - $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs ); - $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs ); - $found = true; + list( $callback, $flags ) = $this->mFunctionHooks[$function]; + $initialArgs = array( &$this ); + $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) ); + if ( $flags & SFH_OBJECT_ARGS ) { + # Add a frame parameter, and pass the arguments as an array + $allArgs = $initialArgs; + $allArgs[] = $frame; + for ( $i = 0; $i < $args->getLength(); $i++ ) { + $funcArgs[] = $args->item( $i ); + } + $allArgs[] = $funcArgs; + } else { + # Convert arguments to plain text + for ( $i = 0; $i < $args->getLength(); $i++ ) { + $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) ); + } + $allArgs = array_merge( $initialArgs, $funcArgs ); + } - // The text is usually already parsed, doesn't need triple-brace tags expanded, etc. - //$noargs = true; - //$noparse = true; + # Workaround for PHP bug 35229 and similar + if ( !is_callable( $callback ) ) { + throw new MWException( "Tag hook for $name is not callable\n" ); + } + $result = call_user_func_array( $callback, $allArgs ); + $found = true; if ( is_array( $result ) ) { if ( isset( $result[0] ) ) { - $text = $linestart . $result[0]; + $text = $result[0]; unset( $result[0] ); } // Extract flags into the local scope - // This allows callers to set flags such as nowiki, noparse, found, etc. + // This allows callers to set flags such as nowiki, found, etc. extract( $result ); } else { - $text = $linestart . $result; + $text = $result; } } } wfProfileOut( __METHOD__ . '-pfunc' ); } - # Template table test - - # Did we encounter this template already? If yes, it is in the cache - # and we need to check for loops. - if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) { - $found = true; - - # Infinite loop test - if ( isset( $this->mTemplatePath[$part1] ) ) { - $noparse = true; - $noargs = true; - $found = true; - $text = $linestart . - "[[$part1]]<!-- WARNING: template loop detected -->"; - wfDebug( __METHOD__.": template loop broken at '$part1'\n" ); - } else { - # set $text to cached message. - $text = $linestart . $this->mTemplates[$piece['title']]; - #treat title for cached page the same as others - $ns = NS_TEMPLATE; - $subpage = ''; - $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); - if ($subpage !== '') { - $ns = $this->mTitle->getNamespace(); - } - $title = Title::newFromText( $part1, $ns ); - //used by include size checking - $titleText = $title->getPrefixedText(); - //used by edit section links - $replaceHeadings = true; - - } - } - - # Load from database + # Finish mangling title and then check for loops. + # Set $title to a Title object and $titleText to the PDBK if ( !$found ) { - wfProfileIn( __METHOD__ . '-loadtpl' ); $ns = NS_TEMPLATE; - # declaring $subpage directly in the function call - # does not work correctly with references and breaks - # {{/subpage}}-style inclusions + # Split the title into page and subpage $subpage = ''; $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); if ($subpage !== '') { $ns = $this->mTitle->getNamespace(); } $title = Title::newFromText( $part1, $ns ); - - - if ( !is_null( $title ) ) { + if ( $title ) { $titleText = $title->getPrefixedText(); # Check for language variants if the template is not found if($wgContLang->hasVariants() && $title->getArticleID() == 0){ $wgContLang->findVariantLink($part1, $title); } + # Do infinite loop check + if ( !$frame->loopCheck( $title ) ) { + $found = true; + $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>"; + wfDebug( __METHOD__.": template loop broken at '$titleText'\n" ); + } + # Do recursion depth check + $limit = $this->mOptions->getMaxTemplateDepth(); + if ( $frame->depth >= $limit ) { + $found = true; + $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>"; + } + } + } - if ( !$title->isExternal() ) { - if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { - $text = SpecialPage::capturePath( $title ); - if ( is_string( $text ) ) { - $found = true; - $noparse = true; - $noargs = true; - $isHTML = true; - $this->disableCache(); - } - } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { - $found = false; //access denied - wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); - } else { - list($articleContent,$title) = $this->fetchTemplateAndtitle( $title ); - if ( $articleContent !== false ) { - $found = true; - $text = $articleContent; - $replaceHeadings = true; - } - } - - # If the title is valid but undisplayable, make a link to it - if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { - $text = "[[:$titleText]]"; + # Load from database + if ( !$found && $title ) { + wfProfileIn( __METHOD__ . '-loadtpl' ); + if ( !$title->isExternal() ) { + if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { + $text = SpecialPage::capturePath( $title ); + if ( is_string( $text ) ) { $found = true; - } - } elseif ( $title->isTrans() ) { - // Interwiki transclusion - if ( $this->ot['html'] && !$forceRawInterwiki ) { - $text = $this->interwikiTransclude( $title, 'render' ); $isHTML = true; - $noparse = true; - } else { - $text = $this->interwikiTransclude( $title, 'raw' ); - $replaceHeadings = true; + $this->disableCache(); + } + } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { + $found = false; //access denied + wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); + } else { + list( $text, $title ) = $this->getTemplateDom( $title ); + if ( $text !== false ) { + $found = true; + $isChildObj = true; } - $found = true; } - # Template cache array insertion - # Use the original $piece['title'] not the mangled $part1, so that - # modifiers such as RAW: produce separate cache entries - if( $found ) { - if( $isHTML ) { - // A special page; don't store it in the template cache. - } else { - $this->mTemplates[$piece['title']] = $text; - } - $text = $linestart . $text; + # If the title is valid but undisplayable, make a link to it + if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = "[[:$titleText]]"; + $found = true; + } + } elseif ( $title->isTrans() ) { + // Interwiki transclusion + if ( $this->ot['html'] && !$forceRawInterwiki ) { + $text = $this->interwikiTransclude( $title, 'render' ); + $isHTML = true; + } else { + $text = $this->interwikiTransclude( $title, 'raw' ); + // Preprocess it like a template + $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); + $isChildObj = true; } + $found = true; } wfProfileOut( __METHOD__ . '-loadtpl' ); } - if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) { - # Error, oversize inclusion - $text = $linestart . - "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->"; - $noparse = true; - $noargs = true; + # If we haven't found text to substitute by now, we're done + # Recover the source wikitext and return it + if ( !$found ) { + $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); + wfProfileOut( $fname ); + return array( 'object' => $text ); } - # Recursive parsing, escaping and link table handling - # Only for HTML output - if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) { - $text = wfEscapeWikiText( $text ); - } elseif ( !$this->ot['msg'] && $found ) { - if ( $noargs ) { - $assocArgs = array(); - } else { - # Clean up argument array - $assocArgs = self::createAssocArgs($args); - # Add a new element to the templace recursion path - $this->mTemplatePath[$part1] = 1; - } - - if ( !$noparse ) { - # If there are any <onlyinclude> tags, only include them - if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) { - $replacer = new OnlyIncludeReplacer; - StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>', - array( &$replacer, 'replace' ), $text ); - $text = $replacer->output; - } - # Remove <noinclude> sections and <includeonly> tags - $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text ); - $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) ); - - if( $this->ot['html'] || $this->ot['pre'] ) { - # Strip <nowiki>, <pre>, etc. - $text = $this->strip( $text, $this->mStripState ); - if ( $this->ot['html'] ) { - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs ); - } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) { - $text = Sanitizer::removeHTMLcomments( $text ); - } - } - $text = $this->replaceVariables( $text, $assocArgs ); - - # If the template begins with a table or block-level - # element, it should be treated as beginning a new line. - if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{ - $text = "\n" . $text; + # Expand DOM-style return values in a child frame + if ( $isChildObj ) { + # Clean up argument array + $newFrame = $frame->newChild( $args, $title ); + + if ( $nowiki ) { + $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG ); + } elseif ( $titleText !== false && $newFrame->isEmpty() ) { + # Expansion is eligible for the empty-frame cache + if ( isset( $this->mTplExpandCache[$titleText] ) ) { + $text = $this->mTplExpandCache[$titleText]; + } else { + $text = $newFrame->expand( $text ); + $this->mTplExpandCache[$titleText] = $text; } - } elseif ( !$noargs ) { - # $noparse and !$noargs - # Just replace the arguments, not any double-brace items - # This is used for rendered interwiki transclusion - $text = $this->replaceVariables( $text, $assocArgs, true ); + } else { + # Uncached expansion + $text = $newFrame->expand( $text ); } } - # Prune lower levels off the recursion check path - $this->mTemplatePath = $lastPathLevel; + if ( $isLocalObj && $nowiki ) { + $text = $frame->expand( $text, PPFrame::RECOVER_ORIG ); + $isLocalObj = false; + } - if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { + # Replace raw HTML by a placeholder + # Add a blank line preceding, to prevent it from mucking up + # immediately preceding headings + if ( $isHTML ) { + $text = "\n\n" . $this->insertStripItem( $text ); + } + # Escape nowiki-style return values + elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = wfEscapeWikiText( $text ); + } + # Bug 529: if the template begins with a table or block-level + # element, it should be treated as beginning a new line. + # This behaviour is somewhat controversial. + elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{ + $text = "\n" . $text; + } + + if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { # Error, oversize inclusion - $text = $linestart . - "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->"; - $noparse = true; - $noargs = true; + $text = "[[$originalTitle]]" . + $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' ); } - if ( !$found ) { - wfProfileOut( $fname ); - return $piece['text']; + if ( $isLocalObj ) { + $ret = array( 'object' => $text ); } else { - wfProfileIn( __METHOD__ . '-placeholders' ); - if ( $isHTML ) { - # Replace raw HTML by a placeholder - # Add a blank line preceding, to prevent it from mucking up - # immediately preceding headings - $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState ); - } else { - # replace ==section headers== - # XXX this needs to go away once we have a better parser. - if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) { - if( !is_null( $title ) ) - $encodedname = base64_encode($title->getPrefixedDBkey()); - else - $encodedname = base64_encode(""); - $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1, - PREG_SPLIT_DELIM_CAPTURE); - $text = ''; - $nsec = $headingOffset; - - for( $i = 0; $i < count($m); $i += 2 ) { - $text .= $m[$i]; - if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue; - $hl = $m[$i + 1]; - if( strstr($hl, "<!--MWTEMPLATESECTION") ) { - $text .= $hl; - continue; - } - $m2 = array(); - preg_match('/^(={1,6})(.*?)(={1,6})\s*?$/m', $hl, $m2); - $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION=" - . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3]; + $ret = array( 'text' => $text ); + } - $nsec++; - } - } - } - wfProfileOut( __METHOD__ . '-placeholders' ); + wfProfileOut( $fname ); + return $ret; + } + + /** + * Get the semi-parsed DOM representation of a template with a given title, + * and its redirect destination title. Cached. + */ + function getTemplateDom( $title ) { + $cacheTitle = $title; + $titleText = $title->getPrefixedDBkey(); + + if ( isset( $this->mTplRedirCache[$titleText] ) ) { + list( $ns, $dbk ) = $this->mTplRedirCache[$titleText]; + $title = Title::makeTitle( $ns, $dbk ); + $titleText = $title->getPrefixedDBkey(); + } + if ( isset( $this->mTplDomCache[$titleText] ) ) { + return array( $this->mTplDomCache[$titleText], $title ); } - # Prune lower levels off the recursion check path - $this->mTemplatePath = $lastPathLevel; + // Cache miss, go to the database + list( $text, $title ) = $this->fetchTemplateAndTitle( $title ); - if ( !$found ) { - wfProfileOut( $fname ); - return $piece['text']; - } else { - wfProfileOut( $fname ); - return $text; + if ( $text === false ) { + $this->mTplDomCache[$titleText] = false; + return array( false, $title ); + } + + $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); + $this->mTplDomCache[ $titleText ] = $dom; + + if (! $title->equals($cacheTitle)) { + $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] = + array( $title->getNamespace(),$cdb = $title->getDBkey() ); } + + return array( $dom, $title ); } /** * Fetch the unparsed text of a template and register a reference to it. */ - function fetchTemplateAndtitle( $title ) { + function fetchTemplateAndTitle( $title ) { + $templateCb = $this->mOptions->getTemplateCallback(); + $stuff = call_user_func( $templateCb, $title ); + $text = $stuff['text']; + $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; + if ( isset( $stuff['deps'] ) ) { + foreach ( $stuff['deps'] as $dep ) { + $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); + } + } + return array($text,$finalTitle); + } + + function fetchTemplate( $title ) { + $rv = $this->fetchTemplateAndTitle($title); + return $rv[0]; + } + + /** + * Static function to get a template + * Can be overridden via ParserOptions::setTemplateCallback(). + */ + static function statelessFetchTemplate( $title ) { $text = $skip = false; $finalTitle = $title; + $deps = array(); + // Loop to fetch the article, with up to 1 redirect for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { # Give extensions a chance to select the revision instead $id = false; // Assume current - wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( &$this, &$title, &$skip, &$id ) ); + wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( false, &$title, &$skip, &$id ) ); if( $skip ) { $text = false; - $this->mOutput->addTemplate( $title, $title->getArticleID(), null ); + $deps[] = array( + 'title' => $title, + 'page_id' => $title->getArticleID(), + 'rev_id' => null ); break; } $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title ); $rev_id = $rev ? $rev->getId() : 0; - - $this->mOutput->addTemplate( $title, $title->getArticleID(), $rev_id ); - + + $deps[] = array( + 'title' => $title, + 'page_id' => $title->getArticleID(), + 'rev_id' => $rev_id ); + if( $rev ) { $text = $rev->getText(); } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { @@ -3338,12 +3109,10 @@ class Parser $finalTitle = $title; $title = Title::newFromRedirect( $text ); } - return array($text,$finalTitle); - } - - function fetchTemplate( $title ) { - $rv = $this->fetchTemplateAndtitle($title); - return $rv[0]; + return array( + 'text' => $text, + 'finalTitle' => $finalTitle, + 'deps' => $deps ); } /** @@ -3392,23 +3161,128 @@ class Parser * Triple brace replacement -- used for template arguments * @private */ - function argSubstitution( $matches ) { - $arg = trim( $matches['title'] ); - $text = $matches['text']; - $inputArgs = end( $this->mArgStack ); + function argSubstitution( $piece, $frame ) { + wfProfileIn( __METHOD__ ); - if ( array_key_exists( $arg, $inputArgs ) ) { - $text = $inputArgs[$arg]; - } else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) && - null != $matches['parts'] && count($matches['parts']) > 0) { - $text = $matches['parts'][0]; + $error = false; + $parts = $piece['parts']; + $nameWithSpaces = $frame->expand( $piece['title'] ); + $argName = trim( $nameWithSpaces ); + $object = false; + $text = $frame->getArgument( $argName ); + if ( $text === false && $parts->getLength() > 0 + && ( + $this->ot['html'] + || $this->ot['pre'] + || ( $this->ot['wiki'] && $frame->isTemplate() ) + ) + ) { + # No match in frame, use the supplied default + $object = $parts->item( 0 )->getChildren(); } if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { - $text = $matches['text'] . - '<!-- WARNING: argument omitted, expansion size too large -->'; + $error = '<!-- WARNING: argument omitted, expansion size too large -->'; } - return $text; + if ( $text === false && $object === false ) { + # No match anywhere + $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts ); + } + if ( $error !== false ) { + $text .= $error; + } + if ( $object !== false ) { + $ret = array( 'object' => $object ); + } else { + $ret = array( 'text' => $text ); + } + + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * Return the text to be used for a given extension tag. + * This is the ghost of strip(). + * + * @param array $params Associative array of parameters: + * name PPNode for the tag name + * attr PPNode for unparsed text where tag attributes are thought to be + * attributes Optional associative array of parsed attributes + * inner Contents of extension element + * noClose Original text did not have a close tag + * @param PPFrame $frame + */ + function extensionSubstitution( $params, $frame ) { + global $wgRawHtml, $wgContLang; + + $name = $frame->expand( $params['name'] ); + $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] ); + $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] ); + + $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . $this->mMarkerSuffix; + + if ( $this->ot['html'] ) { + $name = strtolower( $name ); + + $attributes = Sanitizer::decodeTagAttributes( $attrText ); + if ( isset( $params['attributes'] ) ) { + $attributes = $attributes + $params['attributes']; + } + switch ( $name ) { + case 'html': + if( $wgRawHtml ) { + $output = $content; + break; + } else { + throw new MWException( '<html> extension tag encountered unexpectedly' ); + } + case 'nowiki': + $output = Xml::escapeTagsOnly( $content ); + break; + case 'math': + $output = $wgContLang->armourMath( + MathRenderer::renderMath( $content, $attributes ) ); + break; + case 'gallery': + $output = $this->renderImageGallery( $content, $attributes ); + break; + default: + if( isset( $this->mTagHooks[$name] ) ) { + # Workaround for PHP bug 35229 and similar + if ( !is_callable( $this->mTagHooks[$name] ) ) { + throw new MWException( "Tag hook for $name is not callable\n" ); + } + $output = call_user_func_array( $this->mTagHooks[$name], + array( $content, $attributes, $this ) ); + } else { + throw new MWException( "Invalid call hook $name" ); + } + } + } else { + if ( is_null( $attrText ) ) { + $attrText = ''; + } + if ( isset( $params['attributes'] ) ) { + foreach ( $params['attributes'] as $attrName => $attrValue ) { + $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' . + htmlspecialchars( $attrValue ) . '"'; + } + } + if ( $content === null ) { + $output = "<$name$attrText/>"; + } else { + $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] ); + $output = "<$name$attrText>$content$close"; + } + } + + if ( $name == 'html' || $name == 'nowiki' ) { + $this->mStripState->nowiki->setPair( $marker, $output ); + } else { + $this->mStripState->general->setPair( $marker, $output ); + } + return $marker; } /** @@ -3419,7 +3293,7 @@ class Parser * @return boolean False if this inclusion would take it over the maximum, true otherwise */ function incrementIncludeSize( $type, $size ) { - if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { + if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) { return false; } else { $this->mIncludeSizes[$type] += $size; @@ -3471,7 +3345,7 @@ class Parser /** * This function accomplishes several tasks: * 1) Auto-number headings if that option is enabled - * 2) Add an [edit] link to sections for logged in users who have enabled the option + * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page * 3) Add a Table of contents on the top for users who have enabled the option * 4) Auto-anchor headings * @@ -3527,7 +3401,6 @@ class Parser # headline counter $headlineCount = 0; - $sectionCount = 0; # headlineCount excluding template sections $numVisible = 0; # Ugh .. the TOC should have neat indentation levels which can be @@ -3542,18 +3415,21 @@ class Parser $prevlevel = 0; $toclevel = 0; $prevtoclevel = 0; + $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-{$this->mMarkerSuffix}"; + $baseTitleText = $this->mTitle->getPrefixedDBkey(); + $tocraw = array(); foreach( $matches[3] as $headline ) { - $istemplate = 0; - $templatetitle = ''; - $templatesection = 0; + $isTemplate = false; + $titleText = false; + $sectionIndex = false; $numbering = ''; - $mat = array(); - if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) { - $istemplate = 1; - $templatetitle = base64_decode($mat[1]); - $templatesection = 1 + (int)base64_decode($mat[2]); - $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline); + $markerMatches = array(); + if (preg_match("/^$markerRegex/", $headline, $markerMatches)) { + $serial = $markerMatches[1]; + list( $titleText, $sectionIndex ) = $this->mHeadings[$serial]; + $isTemplate = ($titleText != $baseTitleText); + $headline = preg_replace("/^$markerRegex/", "", $headline); } if( $toclevel ) { @@ -3626,41 +3502,41 @@ class Parser } } - # The canonized header is a version of the header text safe to use for links + # The safe header is a version of the header text safe to use for links # Avoid insertion of weird stuff like <math> by expanding the relevant sections - $canonized_headline = $this->mStripState->unstripBoth( $headline ); + $safeHeadline = $this->mStripState->unstripBoth( $headline ); # Remove link placeholders by the link text. # <!--LINK number--> # turns into # link text with suffix - $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e', + $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e', "\$this->mLinkHolders['texts'][\$1]", - $canonized_headline ); - $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e', + $safeHeadline ); + $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e', "\$this->mInterwikiLinkHolders['texts'][\$1]", - $canonized_headline ); + $safeHeadline ); # Strip out HTML (other than plain <sup> and <sub>: bug 8393) $tocline = preg_replace( array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ), array( '', '<$1>'), - $canonized_headline + $safeHeadline ); $tocline = trim( $tocline ); # For the anchor, strip out HTML-y stuff period - $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline ); - $canonized_headline = trim( $canonized_headline ); + $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline ); + $safeHeadline = trim( $safeHeadline ); # Save headline for section edit hint before it's escaped - $headline_hint = $canonized_headline; - $canonized_headline = Sanitizer::escapeId( $canonized_headline ); - $refers[$headlineCount] = $canonized_headline; + $headlineHint = $safeHeadline; + $safeHeadline = Sanitizer::escapeId( $safeHeadline ); + $refers[$headlineCount] = $safeHeadline; # count how many in assoc. array so we can track dupes in anchors - isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1; - $refcount[$headlineCount]=$refers[$canonized_headline]; + isset( $refers[$safeHeadline] ) ? $refers[$safeHeadline]++ : $refers[$safeHeadline] = 1; + $refcount[$headlineCount] = $refers[$safeHeadline]; # Don't number the heading if it is the only one (looks silly) if( $doNumberHeadings && count( $matches[3] ) > 1) { @@ -3669,29 +3545,33 @@ class Parser } # Create the anchor for linking from the TOC to the section - $anchor = $canonized_headline; + $anchor = $safeHeadline; if($refcount[$headlineCount] > 1 ) { $anchor .= '_' . $refcount[$headlineCount]; } if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); + $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering ); } # give headline the correct <h#> tag - if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) { - if( $istemplate ) - $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection); - else - $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); + if( $showEditLink && $sectionIndex !== false ) { + if( $isTemplate ) { + # Put a T flag in the section identifier, to indicate to extractSections() + # that sections inside <includeonly> should be counted. + $editlink = $sk->editSectionLinkForOther($titleText, "T-$sectionIndex"); + } else { + $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint); + } } else { $editlink = ''; } $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); $headlineCount++; - if( !$istemplate ) - $sectionCount++; } + $this->mOutput->setSections( $tocraw ); + # Never ever show TOC if no headers if( $numVisible < 1 ) { $enoughToc = false; @@ -3750,21 +3630,19 @@ class Parser */ function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) { $this->mOptions = $options; - $this->mTitle =& $title; - $this->setOutputType( OT_WIKI ); + $this->setTitle( $title ); + $this->setOutputType( self::OT_WIKI ); if ( $clearState ) { $this->clearState(); } - $stripState = new StripState; $pairs = array( "\r\n" => "\n", ); $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); - $text = $this->strip( $text, $stripState, true, array( 'gallery' ) ); - $text = $this->pstPass2( $text, $stripState, $user ); - $text = $stripState->unstripBoth( $text ); + $text = $this->pstPass2( $text, $user ); + $text = $this->mStripState->unstripBoth( $text ); return $text; } @@ -3772,31 +3650,32 @@ class Parser * Pre-save transform helper function * @private */ - function pstPass2( $text, &$stripState, $user ) { + function pstPass2( $text, $user ) { global $wgContLang, $wgLocaltimezone; /* Note: This is the timestamp saved as hardcoded wikitext to * the database, we use $wgContLang here in order to give * everyone the same signature and use the default one rather * than the one selected in each user's preferences. + * + * (see also bug 12815) */ + $ts = $this->mOptions->getTimestamp(); + $tz = 'UTC'; if ( isset( $wgLocaltimezone ) ) { + $unixts = wfTimestamp( TS_UNIX, $ts ); $oldtz = getenv( 'TZ' ); putenv( 'TZ='.$wgLocaltimezone ); - } - $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) . - ' (' . date( 'T' ) . ')'; - if ( isset( $wgLocaltimezone ) ) { + $ts = date( 'YmdHis', $unixts ); + $tz = date( 'T', $unixts ); # might vary on DST changeover! putenv( 'TZ='.$oldtz ); } + $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)"; # Variable replacement # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags $text = $this->replaceVariables( $text ); - # Strip out <nowiki> etc. added via replaceVariables - $text = $this->strip( $text, $stripState, false, array( 'gallery' ) ); - # Signatures $sigText = $this->getUserSig( $user ); $text = strtr( $text, array( @@ -3870,8 +3749,13 @@ class Parser $nickname = $this->cleanSigInSig( $nickname ); # If we're still here, make it a link to the user page - $userpage = $user->getUserPage(); - return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' ); + $userText = wfEscapeWikiText( $username ); + $nickText = wfEscapeWikiText( $nickname ); + if ( $user->isAnon() ) { + return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText ); + } else { + return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText ); + } } /** @@ -3895,18 +3779,30 @@ class Parser * @return string Signature text */ function cleanSig( $text, $parsing = false ) { - global $wgTitle; - $this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG ); + if ( !$parsing ) { + global $wgTitle; + $this->clearState(); + $this->setTitle( $wgTitle ); + $this->mOptions = new ParserOptions; + $this->setOutputType = self::OT_PREPROCESS; + } + # FIXME: regex doesn't respect extension tags or nowiki + # => Move this logic to braceSubstitution() $substWord = MagicWord::get( 'subst' ); $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); $substText = '{{' . $substWord->getSynonym( 0 ); $text = preg_replace( $substRegex, $substText, $text ); $text = $this->cleanSigInSig( $text ); - $text = $this->replaceVariables( $text ); + $dom = $this->preprocessToDom( $text ); + $frame = $this->getPreprocessor()->newFrame(); + $text = $frame->expand( $dom ); + + if ( !$parsing ) { + $text = $this->mStripState->unstripBoth( $text ); + } - $this->clearState(); return $text; } @@ -3926,7 +3822,7 @@ class Parser * @public */ function startExternalParse( &$title, $options, $outputType, $clearState = true ) { - $this->mTitle =& $title; + $this->setTitle( $title ); $this->mOptions = $options; $this->setOutputType( $outputType ); if ( $clearState ) { @@ -3935,11 +3831,11 @@ class Parser } /** - * Transform a MediaWiki message by replacing magic variables. + * Wrapper for preprocess() * - * @param string $text the text to transform + * @param string $text the text to preprocess * @param ParserOptions $options options - * @return string the text with variables substituted + * @return string * @public */ function transformMsg( $text, $options ) { @@ -3955,16 +3851,7 @@ class Parser $executing = true; wfProfileIn($fname); - - if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) { - $this->mTitle = $wgTitle; - } else { - $this->mTitle = Title::newFromText('msg'); - } - $this->mOptions = $options; - $this->setOutputType( OT_MSG ); - $this->clearState(); - $text = $this->replaceVariables( $text ); + $text = $this->preprocess( $text, $wgTitle, $options ); $executing = false; wfProfileOut($fname); @@ -3990,6 +3877,7 @@ class Parser $tag = strtolower( $tag ); $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; $this->mTagHooks[$tag] = $callback; + $this->mStripList[] = $tag; return $oldVal; } @@ -4003,6 +3891,14 @@ class Parser } /** + * Remove all tag hooks + */ + function clearTagHooks() { + $this->mTagHooks = array(); + $this->mStripList = $this->mDefaultStripList; + } + + /** * Create a function, e.g. {{sum:1|2|3}} * The callback function should have the form: * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } @@ -4013,8 +3909,6 @@ class Parser * found The text returned is valid, stop processing the template. This * is on by default. * nowiki Wiki markup in the return value should be escaped - * noparse Unsafe HTML tags should not be stripped, etc. - * noargs Don't replace triple-brace arguments in the return value * isHTML The returned text is HTML, armour it against wikitext transformation * * @public @@ -4027,8 +3921,8 @@ class Parser * @return The old callback function for this name, if any */ function setFunctionHook( $id, $callback, $flags = 0 ) { - $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null; - $this->mFunctionHooks[$id] = $callback; + $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null; + $this->mFunctionHooks[$id] = array( $callback, $flags ); # Add to function cache $mw = MagicWord::get( $id ); @@ -4068,10 +3962,7 @@ class Parser /** * Replace <!--LINK--> link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() - * Returns an array of links found, indexed by PDBK: - * 0 - broken - * 1 - normal link - * 2 - stub + * Returns an array of link CSS classes, indexed by PDBK. * $options is a bit field, RLH_FOR_UPDATE to select for update */ function replaceLinkHolders( &$text, $options = 0 ) { @@ -4083,6 +3974,7 @@ class Parser $pdbks = array(); $colours = array(); + $linkcolour_ids = array(); $sk = $this->mOptions->getSkin(); $linkCache =& LinkCache::singleton(); @@ -4111,21 +4003,21 @@ class Parser # Check if it's a static known link, e.g. interwiki if ( $title->isAlwaysKnown() ) { - $colours[$pdbk] = 1; + $colours[$pdbk] = ''; } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { - $colours[$pdbk] = 1; + $colours[$pdbk] = ''; $this->mOutput->addLink( $title, $id ); } elseif ( $linkCache->isBadLink( $pdbk ) ) { - $colours[$pdbk] = 0; + $colours[$pdbk] = 'new'; } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) { - $colours[$pdbk] = 0; + $colours[$pdbk] = 'new'; } else { # Not in the link cache, add it to the query if ( !isset( $current ) ) { $current = $ns; - $query = "SELECT page_id, page_namespace, page_title"; + $query = "SELECT page_id, page_namespace, page_title, page_is_redirect"; if ( $threshold > 0 ) { - $query .= ', page_len, page_is_redirect'; + $query .= ', page_len'; } $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; } elseif ( $current != $ns ) { @@ -4148,20 +4040,17 @@ class Parser # Fetch data and form into an associative array # non-existent = broken - # 1 = known - # 2 = stub while ( $s = $dbr->fetchObject($res) ) { $title = Title::makeTitle( $s->page_namespace, $s->page_title ); $pdbk = $title->getPrefixedDBkey(); $linkCache->addGoodLinkObj( $s->page_id, $title ); $this->mOutput->addLink( $title, $s->page_id ); - - $colours[$pdbk] = ( $threshold == 0 || ( - $s->page_len >= $threshold || # always true if $threshold <= 0 - $s->page_is_redirect || - !Namespace::isContent( $s->page_namespace ) ) - ? 1 : 2 ); + $colours[$pdbk] = $sk->getLinkColour( $s, $threshold ); + //add id to the extension todolist + $linkcolour_ids[$s->page_id] = $pdbk; } + //pass an array of page_ids to an extension + wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); } wfProfileOut( $fname.'-check' ); @@ -4217,9 +4106,9 @@ class Parser // construct query $titleClause = $linkBatch->constructSet('page', $dbr); - $variantQuery = "SELECT page_id, page_namespace, page_title"; + $variantQuery = "SELECT page_id, page_namespace, page_title, page_is_redirect"; if ( $threshold > 0 ) { - $variantQuery .= ', page_len, page_is_redirect'; + $variantQuery .= ', page_len'; } $variantQuery .= " FROM $page WHERE $titleClause"; @@ -4257,18 +4146,10 @@ class Parser // set pdbk and colour $pdbks[$key] = $varPdbk; - if ( $threshold > 0 ) { - $size = $s->page_len; - if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { - $colours[$varPdbk] = 1; - } else { - $colours[$varPdbk] = 2; - } - } - else { - $colours[$varPdbk] = 1; - } + $colours[$varPdbk] = $sk->getLinkColour( $s, $threshold ); + $linkcolour_ids[$s->page_id] = $pdbk; } + wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); } // check if the object is a variant of a category @@ -4301,19 +4182,15 @@ class Parser $pdbk = $pdbks[$key]; $searchkey = "<!--LINK $key-->"; $title = $this->mLinkHolders['titles'][$key]; - if ( empty( $colours[$pdbk] ) ) { + if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) { $linkCache->addBadLinkObj( $title ); - $colours[$pdbk] = 0; + $colours[$pdbk] = 'new'; $this->mOutput->addLink( $title, 0 ); $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); - } elseif ( $colours[$pdbk] == 1 ) { - $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title, - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } elseif ( $colours[$pdbk] == 2 ) { - $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title, + } else { + $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk], $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } @@ -4466,13 +4343,7 @@ class Parser $label = ''; } - $pout = $this->parse( $label, - $this->mTitle, - $this->mOptions, - false, // Strip whitespace...? - false // Don't clear state! - ); - $html = $pout->getText(); + $html = $this->recursiveTagParse( trim( $label ) ); $ig->add( $nt, $html ); @@ -4647,12 +4518,12 @@ class Parser * Callback from the Sanitizer for expanding items found in HTML attribute * values, so they can be safely tested and escaped. * @param string $text - * @param array $args + * @param PPFrame $frame * @return string * @private */ - function attributeStripCallback( &$text, $args ) { - $text = $this->replaceVariables( $text, $args ); + function attributeStripCallback( &$text, $frame = false ) { + $text = $this->replaceVariables( $text, $frame ); $text = $this->mStripState->unstripBoth( $text ); return $text; } @@ -4680,123 +4551,112 @@ class Parser * * External callers should use the getSection and replaceSection methods. * - * @param $text Page wikitext - * @param $section Numbered section. 0 pulls the text before the first - * heading; other numbers will pull the given section - * along with its lower-level subsections. - * @param $mode One of "get" or "replace" - * @param $newtext Replacement text for section data. + * @param string $text Page wikitext + * @param string $section A section identifier string of the form: + * <flag1> - <flag2> - ... - <section number> + * + * Currently the only recognised flag is "T", which means the target section number + * was derived during a template inclusion parse, in other words this is a template + * section edit link. If no flags are given, it was an ordinary section edit link. + * This flag is required to avoid a section numbering mismatch when a section is + * enclosed by <includeonly> (bug 6563). + * + * The section number 0 pulls the text before the first heading; other numbers will + * pull the given section along with its lower-level subsections. If the section is + * not found, $mode=get will return $newtext, and $mode=replace will return $text. + * + * @param string $mode One of "get" or "replace" + * @param string $newText Replacement text for section data. * @return string for "get", the extracted section text. * for "replace", the whole page with the section replaced. */ - private function extractSections( $text, $section, $mode, $newtext='' ) { - # I.... _hope_ this is right. - # Otherwise, sometimes we don't have things initialized properly. + private function extractSections( $text, $section, $mode, $newText='' ) { + global $wgTitle; $this->clearState(); - - # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML - # comments to be stripped as well) - $stripState = new StripState; - - $oldOutputType = $this->mOutputType; - $oldOptions = $this->mOptions; - $this->mOptions = new ParserOptions(); - $this->setOutputType( OT_WIKI ); - - $striptext = $this->strip( $text, $stripState, true ); - - $this->setOutputType( $oldOutputType ); - $this->mOptions = $oldOptions; - - # now that we can be sure that no pseudo-sections are in the source, - # split it up by section - $uniq = preg_quote( $this->uniqPrefix(), '/' ); - $comment = "(?:$uniq-!--.*?QINU\x07)"; - $secs = preg_split( - "/ - ( - ^ - (?:$comment|<\/?noinclude>)* # Initial comments will be stripped - (=+) # Should this be limited to 6? - .+? # Section title... - \\2 # Ending = count must match start - (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok - $ - | - <h([1-6])\b.*?> - .*? - <\/h\\3\s*> - ) - /mix", - $striptext, -1, - PREG_SPLIT_DELIM_CAPTURE); - - if( $mode == "get" ) { - if( $section == 0 ) { - // "Section 0" returns the content before any other section. - $rv = $secs[0]; - } else { - //track missing section, will replace if found. - $rv = $newtext; - } - } elseif( $mode == "replace" ) { - if( $section == 0 ) { - $rv = $newtext . "\n\n"; - $remainder = true; - } else { - $rv = $secs[0]; - $remainder = false; + $this->setTitle( $wgTitle ); // not generally used but removes an ugly failure mode + $this->mOptions = new ParserOptions; + $this->setOutputType( self::OT_WIKI ); + $outText = ''; + $frame = $this->getPreprocessor()->newFrame(); + + // Process section extraction flags + $flags = 0; + $sectionParts = explode( '-', $section ); + $sectionIndex = array_pop( $sectionParts ); + foreach ( $sectionParts as $part ) { + if ( $part == 'T' ) { + $flags |= self::PTD_FOR_INCLUSION; } } - $count = 0; - $sectionLevel = 0; - for( $index = 1; $index < count( $secs ); ) { - $headerLine = $secs[$index++]; - if( $secs[$index] ) { - // A wiki header - $headerLevel = strlen( $secs[$index++] ); - } else { - // An HTML header - $index++; - $headerLevel = intval( $secs[$index++] ); - } - $content = $secs[$index++]; - - $count++; - if( $mode == "get" ) { - if( $count == $section ) { - $rv = $headerLine . $content; - $sectionLevel = $headerLevel; - } elseif( $count > $section ) { - if( $sectionLevel && $headerLevel > $sectionLevel ) { - $rv .= $headerLine . $content; - } else { - // Broke out to a higher-level section + // Preprocess the text + $root = $this->preprocessToDom( $text, $flags ); + + // <h> nodes indicate section breaks + // They can only occur at the top level, so we can find them by iterating the root's children + $node = $root->getFirstChild(); + + // Find the target section + if ( $sectionIndex == 0 ) { + // Section zero doesn't nest, level=big + $targetLevel = 1000; + } else { + while ( $node ) { + if ( $node->getName() == 'h' ) { + $bits = $node->splitHeading(); + if ( $bits['i'] == $sectionIndex ) { + $targetLevel = $bits['level']; break; } } - } elseif( $mode == "replace" ) { - if( $count < $section ) { - $rv .= $headerLine . $content; - } elseif( $count == $section ) { - $rv .= $newtext . "\n\n"; - $sectionLevel = $headerLevel; - } elseif( $count > $section ) { - if( $headerLevel <= $sectionLevel ) { - // Passed the section's sub-parts. - $remainder = true; - } - if( $remainder ) { - $rv .= $headerLine . $content; - } + if ( $mode == 'replace' ) { + $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); + } + $node = $node->getNextSibling(); + } + } + + if ( !$node ) { + // Not found + if ( $mode == 'get' ) { + return $newText; + } else { + return $text; + } + } + + // Find the end of the section, including nested sections + do { + if ( $node->getName() == 'h' ) { + $bits = $node->splitHeading(); + $curLevel = $bits['level']; + if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) { + break; } } + if ( $mode == 'get' ) { + $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); + } + $node = $node->getNextSibling(); + } while ( $node ); + + // Write out the remainder (in replace mode only) + if ( $mode == 'replace' ) { + // Output the replacement text + // Add two newlines on -- trailing whitespace in $newText is conventionally + // stripped by the editor, so we need both newlines to restore the paragraph gap + $outText .= $newText . "\n\n"; + while ( $node ) { + $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); + $node = $node->getNextSibling(); + } } - if (is_string($rv)) - # reinsert stripped tags - $rv = trim( $stripState->unstripBoth( $rv ) ); - return $rv; + if ( is_string( $outText ) ) { + // Re-insert stripped tags + $outText = trim( $this->mStripState->unstripBoth( $outText ) ); + } + + return $outText; } /** @@ -4806,9 +4666,9 @@ class Parser * * If a section contains subsections, these are also returned. * - * @param $text String: text to look in - * @param $section Integer: section number - * @param $deftext: default to return if section is not found + * @param string $text text to look in + * @param string $section section identifier + * @param string $deftext default to return if section is not found * @return string text of the requested section */ public function getSection( $text, $section, $deftext='' ) { @@ -4874,21 +4734,120 @@ class Parser : $this->mTitle->getPrefixedText(); } } -} -/** - * @todo document, briefly. - * @addtogroup Parser - */ -class OnlyIncludeReplacer { - var $output = ''; + /** + * Try to guess the section anchor name based on a wikitext fragment + * presumably extracted from a heading, for example "Header" from + * "== Header ==". + */ + public function guessSectionNameFromWikiText( $text ) { + # Strip out wikitext links(they break the anchor) + $text = $this->stripSectionName( $text ); + $headline = Sanitizer::decodeCharReferences( $text ); + # strip out HTML + $headline = StringUtils::delimiterReplace( '<', '>', '', $headline ); + $headline = trim( $headline ); + $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); + $replacearray = array( + '%3A' => ':', + '%' => '.' + ); + return str_replace( + array_keys( $replacearray ), + array_values( $replacearray ), + $sectionanchor ); + } - function replace( $matches ) { - if ( substr( $matches[1], -1 ) == "\n" ) { - $this->output .= substr( $matches[1], 0, -1 ); - } else { - $this->output .= $matches[1]; + /** + * Strips a text string of wikitext for use in a section anchor + * + * Accepts a text string and then removes all wikitext from the + * string and leaves only the resultant text (i.e. the result of + * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of + * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended + * to create valid section anchors by mimicing the output of the + * parser when headings are parsed. + * + * @param $text string Text string to be stripped of wikitext + * for use in a Section anchor + * @return Filtered text string + */ + public function stripSectionName( $text ) { + # Strip internal link markup + $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text); + $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text); + + # Strip external link markup (FIXME: Not Tolerant to blank link text + # I.E. [http://www.mediawiki.org] will render as [1] or something depending + # on how many empty links there are on the page - need to figure that out. + $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text); + + # Parse wikitext quotes (italics & bold) + $text = $this->doQuotes($text); + + # Strip HTML tags + $text = StringUtils::delimiterReplace( '<', '>', '', $text ); + return $text; + } + + function srvus( $text ) { + return $this->testSrvus( $text, $this->mOutputType ); + } + + /** + * strip/replaceVariables/unstrip for preprocessor regression testing + */ + function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) { + $this->clearState(); + if ( ! ( $title instanceof Title ) ) { + $title = Title::newFromText( $title ); + } + $this->mTitle = $title; + $this->mOptions = $options; + $this->setOutputType( $outputType ); + $text = $this->replaceVariables( $text ); + $text = $this->mStripState->unstripBoth( $text ); + $text = Sanitizer::removeHTMLtags( $text ); + return $text; + } + + function testPst( $text, $title, $options ) { + global $wgUser; + if ( ! ( $title instanceof Title ) ) { + $title = Title::newFromText( $title ); } + return $this->preSaveTransform( $text, $title, $wgUser, $options ); + } + + function testPreprocess( $text, $title, $options ) { + if ( ! ( $title instanceof Title ) ) { + $title = Title::newFromText( $title ); + } + return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS ); + } + + function markerSkipCallback( $s, $callback ) { + $i = 0; + $out = ''; + while ( $i < strlen( $s ) ) { + $markerStart = strpos( $s, $this->mUniqPrefix, $i ); + if ( $markerStart === false ) { + $out .= call_user_func( $callback, substr( $s, $i ) ); + break; + } else { + $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) ); + $markerEnd = strpos( $s, $this->mMarkerSuffix, $markerStart ); + if ( $markerEnd === false ) { + $out .= substr( $s, $markerStart ); + break; + } else { + $markerEnd += strlen( $this->mMarkerSuffix ); + $out .= substr( $s, $markerStart, $markerEnd - $markerStart ); + $i = $markerEnd; + } + } + } + return $out; } } @@ -4906,23 +4865,49 @@ class StripState { function unstripGeneral( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->general->replace( $text ); + do { + $oldText = $text; + $text = $this->general->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } function unstripNoWiki( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->nowiki->replace( $text ); + do { + $oldText = $text; + $text = $this->nowiki->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } function unstripBoth( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->general->replace( $text ); - $text = $this->nowiki->replace( $text ); + do { + $oldText = $text; + $text = $this->general->replace( $text ); + $text = $this->nowiki->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } } + +/** + * @todo document, briefly. + * @addtogroup Parser + */ +class OnlyIncludeReplacer { + var $output = ''; + + function replace( $matches ) { + if ( substr( $matches[1], -1 ) == "\n" ) { + $this->output .= substr( $matches[1], 0, -1 ); + } else { + $this->output .= $matches[1]; + } + } +} + diff --git a/includes/ParserOptions.php b/includes/ParserOptions.php index 2200bfea..996bba21 100644 --- a/includes/ParserOptions.php +++ b/includes/ParserOptions.php @@ -21,7 +21,12 @@ class ParserOptions var $mTidy; # Ask for tidy cleanup var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR var $mMaxIncludeSize; # Maximum size of template expansions, in bytes + var $mMaxPPNodeCount; # Maximum number of nodes touched by PPFrame::expand() + var $mMaxTemplateDepth; # Maximum recursion depth for templates within templates var $mRemoveComments; # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS + var $mTemplateCallback; # Callback for template fetching + var $mEnableLimitReport; # Enable limit report in an HTML comment on output + var $mTimestamp; # Timestamp used for {{CURRENTDAY}} etc. var $mUser; # Stored user object, just used to initialise the skin @@ -36,7 +41,11 @@ class ParserOptions function getTidy() { return $this->mTidy; } function getInterfaceMessage() { return $this->mInterfaceMessage; } function getMaxIncludeSize() { return $this->mMaxIncludeSize; } + function getMaxPPNodeCount() { return $this->mMaxPPNodeCount; } + function getMaxTemplateDepth() { return $this->mMaxTemplateDepth; } function getRemoveComments() { return $this->mRemoveComments; } + function getTemplateCallback() { return $this->mTemplateCallback; } + function getEnableLimitReport() { return $this->mEnableLimitReport; } function getSkin() { if ( !isset( $this->mSkin ) ) { @@ -52,6 +61,13 @@ class ParserOptions return $this->mDateFormat; } + function getTimestamp() { + if ( !isset( $this->mTimestamp ) ) { + $this->mTimestamp = wfTimestampNow(); + } + return $this->mTimestamp; + } + function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); } function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); } function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } @@ -65,7 +81,12 @@ class ParserOptions function setSkin( $x ) { $this->mSkin = $x; } function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); } function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } + function setMaxPPNodeCount( $x ) { return wfSetVar( $this->mMaxPPNodeCount, $x ); } + function setMaxTemplateDepth( $x ) { return wfSetVar( $this->mMaxTemplateDepth, $x ); } function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } + function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); } + function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); } + function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); } function __construct( $user = null ) { $this->initialiseFromUser( $user ); @@ -83,6 +104,7 @@ class ParserOptions function initialiseFromUser( $userInput ) { global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; + global $wgMaxPPNodeCount, $wgMaxTemplateDepth; $fname = 'ParserOptions::initialiseFromUser'; wfProfileIn( $fname ); if ( !$userInput ) { @@ -111,7 +133,11 @@ class ParserOptions $this->mTidy = false; $this->mInterfaceMessage = false; $this->mMaxIncludeSize = $wgMaxArticleSize * 1024; + $this->mMaxPPNodeCount = $wgMaxPPNodeCount; + $this->mMaxTemplateDepth = $wgMaxTemplateDepth; $this->mRemoveComments = true; + $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' ); + $this->mEnableLimitReport = false; wfProfileOut( $fname ); } } diff --git a/includes/ParserOutput.php b/includes/ParserOutput.php index d4daf1d1..9b3c12c1 100644 --- a/includes/ParserOutput.php +++ b/includes/ParserOutput.php @@ -20,7 +20,9 @@ class ParserOutput $mNewSection, # Show a new section link? $mNoGallery, # No gallery on category page? (__NOGALLERY__) $mHeadItems, # Items to put in the <head> section - $mOutputHooks; # Hook tags as per $wgParserOutputHooks + $mOutputHooks, # Hook tags as per $wgParserOutputHooks + $mWarnings, # Warning text to be returned to the user. Wikitext formatted. + $mSections; # Table of contents /** * Overridden title for display @@ -37,6 +39,7 @@ class ParserOutput $this->mCacheTime = ''; $this->mVersion = Parser::VERSION; $this->mTitleText = $titletext; + $this->mSections = array(); $this->mLinks = array(); $this->mTemplates = array(); $this->mImages = array(); @@ -46,6 +49,7 @@ class ParserOutput $this->mHeadItems = array(); $this->mTemplateIds = array(); $this->mOutputHooks = array(); + $this->mWarnings = array(); } function getText() { return $this->mText; } @@ -54,6 +58,7 @@ class ParserOutput function &getCategories() { return $this->mCategories; } function getCacheTime() { return $this->mCacheTime; } function getTitleText() { return $this->mTitleText; } + function getSections() { return $this->mSections; } function &getLinks() { return $this->mLinks; } function &getTemplates() { return $this->mTemplates; } function &getImages() { return $this->mImages; } @@ -61,6 +66,7 @@ class ParserOutput function getNoGallery() { return $this->mNoGallery; } function getSubtitle() { return $this->mSubtitle; } function getOutputHooks() { return (array)$this->mOutputHooks; } + function getWarnings() { return isset( $this->mWarnings ) ? $this->mWarnings : array(); } function containsOldMagic() { return $this->mContainsOldMagic; } function setText( $text ) { return wfSetVar( $this->mText, $text ); } @@ -68,11 +74,13 @@ class ParserOutput function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); } function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } - function setTitleText( $t ) { return wfSetVar($this->mTitleText, $t); } + function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); } + function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); } function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } + function addWarning( $s ) { $this->mWarnings[] = $s; } function addOutputHook( $hook, $data = false ) { $this->mOutputHooks[] = array( $hook, $data ); @@ -165,6 +173,17 @@ class ParserOutput return $this->displayTitle; } + /** + * Fairly generic flag setter thingy. + */ + public function setFlag( $flag ) { + $this->mFlags[$flag] = true; + } + + public function getFlag( $flag ) { + return isset( $this->mFlags[$flag] ); + } + } diff --git a/includes/Parser_DiffTest.php b/includes/Parser_DiffTest.php new file mode 100644 index 00000000..d88709f0 --- /dev/null +++ b/includes/Parser_DiffTest.php @@ -0,0 +1,85 @@ +<?php + +class Parser_DiffTest +{ + var $parsers, $conf; + + var $dfUniqPrefix; + + function __construct( $conf ) { + if ( !isset( $conf['parsers'] ) ) { + throw new MWException( __METHOD__ . ': no parsers specified' ); + } + $this->conf = $conf; + $this->dtUniqPrefix = "\x7fUNIQ" . Parser::getRandomString(); + } + + function init() { + if ( !is_null( $this->parsers ) ) { + return; + } + + global $wgHooks; + static $doneHook = false; + if ( !$doneHook ) { + $doneHook = true; + $wgHooks['ParserClearState'][] = array( $this, 'onClearState' ); + } + + foreach ( $this->conf['parsers'] as $i => $parserConf ) { + if ( !is_array( $parserConf ) ) { + $class = $parserConf; + $parserConf = array( 'class' => $parserConf ); + } else { + $class = $parserConf['class']; + } + $this->parsers[$i] = new $class( $parserConf ); + } + } + + function __call( $name, $args ) { + $this->init(); + $results = array(); + $mismatch = false; + $lastResult = null; + $first = true; + foreach ( $this->parsers as $i => $parser ) { + $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args ); + if ( $first ) { + $first = false; + } else { + if ( is_object( $lastResult ) ) { + if ( $lastResult != $currentResult ) { + $mismatch = true; + } + } else { + if ( $lastResult !== $currentResult ) { + $mismatch = true; + } + } + } + $results[$i] = $currentResult; + $lastResult = $currentResult; + } + if ( $mismatch ) { + throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" . + 'Arguments: ' . var_export( $args, true ) . "\n" . + 'Results: ' . var_export( $results, true ) . "\n" ); + } + return $lastResult; + } + + function setFunctionHook( $id, $callback, $flags = 0 ) { + $this->init(); + foreach ( $this->parsers as $i => $parser ) { + $parser->setFunctionHook( $id, $callback, $flags ); + } + } + + function onClearState( &$parser ) { + // hack marker prefixes to get identical output + $parser->mUniqPrefix = $this->dtUniqPrefix; + return true; + } +} + diff --git a/includes/Parser_OldPP.php b/includes/Parser_OldPP.php new file mode 100644 index 00000000..c10de257 --- /dev/null +++ b/includes/Parser_OldPP.php @@ -0,0 +1,4942 @@ +<?php +/** + * Parser with old preprocessor + */ +class Parser_OldPP +{ + /** + * Update this version number when the ParserOutput format + * changes in an incompatible way, so the parser cache + * can automatically discard old data. + */ + const VERSION = '1.6.4'; + + # Flags for Parser::setFunctionHook + # Also available as global constants from Defines.php + const SFH_NO_HASH = 1; + + # Constants needed for external link processing + # Everything except bracket, space, or control characters + const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]'; + const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/S'; + + // State constants for the definition list colon extraction + const COLON_STATE_TEXT = 0; + const COLON_STATE_TAG = 1; + const COLON_STATE_TAGSTART = 2; + const COLON_STATE_CLOSETAG = 3; + const COLON_STATE_TAGSLASH = 4; + const COLON_STATE_COMMENT = 5; + const COLON_STATE_COMMENTDASH = 6; + const COLON_STATE_COMMENTDASHDASH = 7; + + // Allowed values for $this->mOutputType + // Parameter to startExternalParse(). + const OT_HTML = 1; + const OT_WIKI = 2; + const OT_PREPROCESS = 3; + const OT_MSG = 4; + + /**#@+ + * @private + */ + # Persistent: + var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables, + $mImageParams, $mImageParamsMagicArray, $mExtLinkBracketedRegex; + + # Cleared with clearState(): + var $mOutput, $mAutonumber, $mDTopen, $mStripState; + var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; + var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; + var $mIncludeSizes, $mDefaultSort; + var $mTemplates, // cache of already loaded templates, avoids + // multiple SQL queries for the same string + $mTemplatePath; // stores an unsorted hash of all the templates already loaded + // in this path. Used for loop detection. + + # Temporary + # These are variables reset at least once per parse regardless of $clearState + var $mOptions, // ParserOptions object + $mTitle, // Title context, used for self-link rendering and similar things + $mOutputType, // Output type, one of the OT_xxx constants + $ot, // Shortcut alias, see setOutputType() + $mRevisionId, // ID to display in {{REVISIONID}} tags + $mRevisionTimestamp, // The timestamp of the specified revision ID + $mRevIdForTs; // The revision ID which was used to fetch the timestamp + + /**#@-*/ + + /** + * Constructor + * + * @public + */ + function __construct( $conf = array() ) { + $this->mTagHooks = array(); + $this->mTransparentTagHooks = array(); + $this->mFunctionHooks = array(); + $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); + $this->mFirstCall = true; + $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'. + '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S'; + } + + /** + * Do various kinds of initialisation on the first call of the parser + */ + function firstCallInit() { + if ( !$this->mFirstCall ) { + return; + } + $this->mFirstCall = false; + + wfProfileIn( __METHOD__ ); + global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; + + $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); + + # Syntax for arguments (see self::setFunctionHook): + # "name for lookup in localized magic words array", + # function callback, + # optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...} + # instead of {{#int:...}}) + $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); + $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); + $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); + $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); + $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); + $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); + $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); + $this->setFunctionHook( 'filepath', array( 'CoreParserFunctions', 'filepath' ), SFH_NO_HASH ); + + if ( $wgAllowDisplayTitle ) { + $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); + } + if ( $wgAllowSlowParserFunctions ) { + $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); + } + + $this->initialiseVariables(); + + wfRunHooks( 'ParserFirstCallInit', array( &$this ) ); + wfProfileOut( __METHOD__ ); + } + + /** + * Clear Parser state + * + * @private + */ + function clearState() { + wfProfileIn( __METHOD__ ); + if ( $this->mFirstCall ) { + $this->firstCallInit(); + } + $this->mOutput = new ParserOutput; + $this->mAutonumber = 0; + $this->mLastSection = ''; + $this->mDTopen = false; + $this->mIncludeCount = array(); + $this->mStripState = new StripState; + $this->mArgStack = array(); + $this->mInPre = false; + $this->mInterwikiLinkHolders = array( + 'texts' => array(), + 'titles' => array() + ); + $this->mLinkHolders = array( + 'namespaces' => array(), + 'dbkeys' => array(), + 'queries' => array(), + 'texts' => array(), + 'titles' => array() + ); + $this->mRevisionTimestamp = $this->mRevisionId = null; + + /** + * Prefix for temporary replacement strings for the multipass parser. + * \x07 should never appear in input as it's disallowed in XML. + * Using it at the front also gives us a little extra robustness + * since it shouldn't match when butted up against identifier-like + * string constructs. + */ + $this->mUniqPrefix = "\x07UNIQ" . self::getRandomString(); + + # Clear these on every parse, bug 4549 + $this->mTemplates = array(); + $this->mTemplatePath = array(); + + $this->mShowToc = true; + $this->mForceTocPosition = false; + $this->mIncludeSizes = array( + 'pre-expand' => 0, + 'post-expand' => 0, + 'arg' => 0 + ); + $this->mDefaultSort = false; + + wfRunHooks( 'ParserClearState', array( &$this ) ); + wfProfileOut( __METHOD__ ); + } + + function setOutputType( $ot ) { + $this->mOutputType = $ot; + // Shortcut alias + $this->ot = array( + 'html' => $ot == self::OT_HTML, + 'wiki' => $ot == self::OT_WIKI, + 'msg' => $ot == self::OT_MSG, + 'pre' => $ot == self::OT_PREPROCESS, + ); + } + + /** + * Accessor for mUniqPrefix. + * + * @public + */ + function uniqPrefix() { + return $this->mUniqPrefix; + } + + /** + * Convert wikitext to HTML + * Do not call this function recursively. + * + * @param string $text Text we want to parse + * @param Title &$title A title object + * @param array $options + * @param boolean $linestart + * @param boolean $clearState + * @param int $revid number to pass in {{REVISIONID}} + * @return ParserOutput a ParserOutput + */ + public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { + /** + * First pass--just handle <nowiki> sections, pass the rest off + * to internalParse() which does all the real work. + */ + + global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; + $fname = 'Parser::parse-' . wfGetCaller(); + wfProfileIn( __METHOD__ ); + wfProfileIn( $fname ); + + if ( $clearState ) { + $this->clearState(); + } + + $this->mOptions = $options; + $this->mTitle =& $title; + $oldRevisionId = $this->mRevisionId; + $oldRevisionTimestamp = $this->mRevisionTimestamp; + if( $revid !== null ) { + $this->mRevisionId = $revid; + $this->mRevisionTimestamp = null; + } + $this->setOutputType( self::OT_HTML ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->internalParse( $text ); + $text = $this->mStripState->unstripGeneral( $text ); + + # Clean up special characters, only run once, next-to-last before doBlockLevels + $fixtags = array( + # french spaces, last one Guillemet-left + # only if there is something before the space + '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2', + # french spaces, Guillemet-right + '/(\\302\\253) /' => '\\1 ', + ); + $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); + + # only once and last + $text = $this->doBlockLevels( $text, $linestart ); + + $this->replaceLinkHolders( $text ); + + # the position of the parserConvert() call should not be changed. it + # assumes that the links are all replaced and the only thing left + # is the <nowiki> mark. + # Side-effects: this calls $this->mOutput->setTitleText() + $text = $wgContLang->parserConvert( $text, $this ); + + $text = $this->mStripState->unstripNoWiki( $text ); + + wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); + +//!JF Move to its own function + + $uniq_prefix = $this->mUniqPrefix; + $matches = array(); + $elements = array_keys( $this->mTransparentTagHooks ); + $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + $tagName = strtolower( $element ); + if( isset( $this->mTransparentTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { + $output = $tag; + } + $this->mStripState->general->setPair( $marker, $output ); + } + $text = $this->mStripState->unstripGeneral( $text ); + + $text = Sanitizer::normalizeCharReferences( $text ); + + if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { + $text = self::tidy($text); + } else { + # attempt to sanitize at least some nesting problems + # (bug #2702 and quite a few others) + $tidyregs = array( + # ''Something [http://www.cool.com cool''] --> + # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> + '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => + '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', + # fix up an anchor inside another anchor, only + # at least for a single single nested link (bug 3695) + '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' => + '\\1\\2</a>\\3</a>\\1\\4</a>', + # fix div inside inline elements- doBlockLevels won't wrap a line which + # contains a div, so fix it up here; replace + # div with escaped text + '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' => + '\\1\\3<div\\5>\\6</div>\\8\\9', + # remove empty italic or bold tag pairs, some + # introduced by rules above + '/<([bi])><\/\\1>/' => '', + ); + + $text = preg_replace( + array_keys( $tidyregs ), + array_values( $tidyregs ), + $text ); + } + + wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); + + # Information on include size limits, for the benefit of users who try to skirt them + if ( $this->mOptions->getEnableLimitReport() ) { + $max = $this->mOptions->getMaxIncludeSize(); + $limitReport = + "Pre-expand include size: {$this->mIncludeSizes['pre-expand']}/$max bytes\n" . + "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" . + "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n"; + wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) ); + $text .= "<!-- \n$limitReport-->\n"; + } + $this->mOutput->setText( $text ); + $this->mRevisionId = $oldRevisionId; + $this->mRevisionTimestamp = $oldRevisionTimestamp; + wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); + + return $this->mOutput; + } + + /** + * Recursive parser entry point that can be called from an extension tag + * hook. + */ + function recursiveTagParse( $text ) { + wfProfileIn( __METHOD__ ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->internalParse( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Expand templates and variables in the text, producing valid, static wikitext. + * Also removes comments. + */ + function preprocess( $text, $title, $options, $revid = null ) { + wfProfileIn( __METHOD__ ); + $this->clearState(); + $this->setOutputType( self::OT_PREPROCESS ); + $this->mOptions = $options; + $this->mTitle = $title; + if( $revid !== null ) { + $this->mRevisionId = $revid; + } + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); + if ( $this->mOptions->getRemoveComments() ) { + $text = Sanitizer::removeHTMLcomments( $text ); + } + $text = $this->replaceVariables( $text ); + $text = $this->mStripState->unstripBoth( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Get a random string + * + * @private + * @static + */ + function getRandomString() { + return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff)); + } + + function &getTitle() { return $this->mTitle; } + function getOptions() { return $this->mOptions; } + + function getFunctionLang() { + global $wgLang, $wgContLang; + return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang; + } + + /** + * Replaces all occurrences of HTML-style comments and the given tags + * in the text with a random marker and returns teh next text. The output + * parameter $matches will be an associative array filled with data in + * the form: + * 'UNIQ-xxxxx' => array( + * 'element', + * 'tag content', + * array( 'param' => 'x' ), + * '<element param="x">tag content</element>' ) ) + * + * @param $elements list of element names. Comments are always extracted. + * @param $text Source text string. + * @param $uniq_prefix + * + * @public + * @static + */ + function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ + static $n = 1; + $stripped = ''; + $matches = array(); + + $taglist = implode( '|', $elements ); + $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i"; + + while ( '' != $text ) { + $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); + $stripped .= $p[0]; + if( count( $p ) < 5 ) { + break; + } + if( count( $p ) > 5 ) { + // comment + $element = $p[4]; + $attributes = ''; + $close = ''; + $inside = $p[5]; + } else { + // tag + $element = $p[1]; + $attributes = $p[2]; + $close = $p[3]; + $inside = $p[4]; + } + + $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07"; + $stripped .= $marker; + + if ( $close === '/>' ) { + // Empty element tag, <tag /> + $content = null; + $text = $inside; + $tail = null; + } else { + if( $element == '!--' ) { + $end = '/(-->)/'; + } else { + $end = "/(<\\/$element\\s*>)/i"; + } + $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE ); + $content = $q[0]; + if( count( $q ) < 3 ) { + # No end tag -- let it run out to the end of the text. + $tail = ''; + $text = ''; + } else { + $tail = $q[1]; + $text = $q[2]; + } + } + + $matches[$marker] = array( $element, + $content, + Sanitizer::decodeTagAttributes( $attributes ), + "<$element$attributes$close$content$tail" ); + } + return $stripped; + } + + /** + * Strips and renders nowiki, pre, math, hiero + * If $render is set, performs necessary rendering operations on plugins + * Returns the text, and fills an array with data needed in unstrip() + * + * @param StripState $state + * + * @param bool $stripcomments when set, HTML comments <!-- like this --> + * will be stripped in addition to other tags. This is important + * for section editing, where these comments cause confusion when + * counting the sections in the wikisource + * + * @param array dontstrip contains tags which should not be stripped; + * used to prevent stipping of <gallery> when saving (fixes bug 2700) + * + * @private + */ + function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { + global $wgContLang; + wfProfileIn( __METHOD__ ); + $render = ($this->mOutputType == self::OT_HTML); + + $uniq_prefix = $this->mUniqPrefix; + $commentState = new ReplacementArray; + $nowikiItems = array(); + $generalItems = array(); + + $elements = array_merge( + array( 'nowiki', 'gallery' ), + array_keys( $this->mTagHooks ) ); + global $wgRawHtml; + if( $wgRawHtml ) { + $elements[] = 'html'; + } + if( $this->mOptions->getUseTeX() ) { + $elements[] = 'math'; + } + + # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700) + foreach ( $elements AS $k => $v ) { + if ( !in_array ( $v , $dontstrip ) ) continue; + unset ( $elements[$k] ); + } + + $matches = array(); + $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + if( $render ) { + $tagName = strtolower( $element ); + wfProfileIn( __METHOD__."-render-$tagName" ); + switch( $tagName ) { + case '!--': + // Comment + if( substr( $tag, -3 ) == '-->' ) { + $output = $tag; + } else { + // Unclosed comment in input. + // Close it so later stripping can remove it + $output = "$tag-->"; + } + break; + case 'html': + if( $wgRawHtml ) { + $output = $content; + break; + } + // Shouldn't happen otherwise. :) + case 'nowiki': + $output = Xml::escapeTagsOnly( $content ); + break; + case 'math': + $output = $wgContLang->armourMath( + MathRenderer::renderMath( $content, $params ) ); + break; + case 'gallery': + $output = $this->renderImageGallery( $content, $params ); + break; + default: + if( isset( $this->mTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { + throw new MWException( "Invalid call hook $element" ); + } + } + wfProfileOut( __METHOD__."-render-$tagName" ); + } else { + // Just stripping tags; keep the source + $output = $tag; + } + + // Unstrip the output, to support recursive strip() calls + $output = $state->unstripBoth( $output ); + + if( !$stripcomments && $element == '!--' ) { + $commentState->setPair( $marker, $output ); + } elseif ( $element == 'html' || $element == 'nowiki' ) { + $nowikiItems[$marker] = $output; + } else { + $generalItems[$marker] = $output; + } + } + # Add the new items to the state + # We do this after the loop instead of during it to avoid slowing + # down the recursive unstrip + $state->nowiki->mergeArray( $nowikiItems ); + $state->general->mergeArray( $generalItems ); + + # Unstrip comments unless explicitly told otherwise. + # (The comments are always stripped prior to this point, so as to + # not invoke any extension tags / parser hooks contained within + # a comment.) + if ( !$stripcomments ) { + // Put them all back and forget them + $text = $commentState->replace( $text ); + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Restores pre, math, and other extensions removed by strip() + * + * always call unstripNoWiki() after this one + * @private + * @deprecated use $this->mStripState->unstrip() + */ + function unstrip( $text, $state ) { + return $state->unstripGeneral( $text ); + } + + /** + * Always call this after unstrip() to preserve the order + * + * @private + * @deprecated use $this->mStripState->unstrip() + */ + function unstripNoWiki( $text, $state ) { + return $state->unstripNoWiki( $text ); + } + + /** + * @deprecated use $this->mStripState->unstripBoth() + */ + function unstripForHTML( $text ) { + return $this->mStripState->unstripBoth( $text ); + } + + /** + * Add an item to the strip state + * Returns the unique tag which must be inserted into the stripped text + * The tag will be replaced with the original text in unstrip() + * + * @private + */ + function insertStripItem( $text, &$state ) { + $rnd = $this->mUniqPrefix . '-item' . self::getRandomString(); + $state->general->setPair( $rnd, $text ); + return $rnd; + } + + /** + * Interface with html tidy, used if $wgUseTidy = true. + * If tidy isn't able to correct the markup, the original will be + * returned in all its glory with a warning comment appended. + * + * Either the external tidy program or the in-process tidy extension + * will be used depending on availability. Override the default + * $wgTidyInternal setting to disable the internal if it's not working. + * + * @param string $text Hideous HTML input + * @return string Corrected HTML output + * @public + * @static + */ + function tidy( $text ) { + global $wgTidyInternal; + $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'. +' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'. +'<head><title>test</title></head><body>'.$text.'</body></html>'; + if( $wgTidyInternal ) { + $correctedtext = self::internalTidy( $wrappedtext ); + } else { + $correctedtext = self::externalTidy( $wrappedtext ); + } + if( is_null( $correctedtext ) ) { + wfDebug( "Tidy error detected!\n" ); + return $text . "\n<!-- Tidy found serious XHTML errors -->\n"; + } + return $correctedtext; + } + + /** + * Spawn an external HTML tidy process and get corrected markup back from it. + * + * @private + * @static + */ + function externalTidy( $text ) { + global $wgTidyConf, $wgTidyBin, $wgTidyOpts; + $fname = 'Parser::externalTidy'; + wfProfileIn( $fname ); + + $cleansource = ''; + $opts = ' -utf8'; + + $descriptorspec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('file', wfGetNull(), 'a') + ); + $pipes = array(); + $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); + if (is_resource($process)) { + // Theoretically, this style of communication could cause a deadlock + // here. If the stdout buffer fills up, then writes to stdin could + // block. This doesn't appear to happen with tidy, because tidy only + // writes to stdout after it's finished reading from stdin. Search + // for tidyParseStdin and tidySaveStdout in console/tidy.c + fwrite($pipes[0], $text); + fclose($pipes[0]); + while (!feof($pipes[1])) { + $cleansource .= fgets($pipes[1], 1024); + } + fclose($pipes[1]); + proc_close($process); + } + + wfProfileOut( $fname ); + + if( $cleansource == '' && $text != '') { + // Some kind of error happened, so we couldn't get the corrected text. + // Just give up; we'll use the source text and append a warning. + return null; + } else { + return $cleansource; + } + } + + /** + * Use the HTML tidy PECL extension to use the tidy library in-process, + * saving the overhead of spawning a new process. + * + * 'pear install tidy' should be able to compile the extension module. + * + * @private + * @static + */ + function internalTidy( $text ) { + global $wgTidyConf, $IP; + $fname = 'Parser::internalTidy'; + wfProfileIn( $fname ); + + $tidy = new tidy; + $tidy->parseString( $text, $wgTidyConf, 'utf8' ); + $tidy->cleanRepair(); + if( $tidy->getStatus() == 2 ) { + // 2 is magic number for fatal error + // http://www.php.net/manual/en/function.tidy-get-status.php + $cleansource = null; + } else { + $cleansource = tidy_get_output( $tidy ); + } + wfProfileOut( $fname ); + return $cleansource; + } + + /** + * parse the wiki syntax used to render tables + * + * @private + */ + function doTableStuff ( $text ) { + $fname = 'Parser::doTableStuff'; + wfProfileIn( $fname ); + + $lines = explode ( "\n" , $text ); + $td_history = array (); // Is currently a td tag open? + $last_tag_history = array (); // Save history of last lag activated (td, th or caption) + $tr_history = array (); // Is currently a tr tag open? + $tr_attributes = array (); // history of tr attributes + $has_opened_tr = array(); // Did this table open a <tr> element? + $indent_level = 0; // indent level of the table + foreach ( $lines as $key => $line ) + { + $line = trim ( $line ); + + if( $line == '' ) { // empty line, go to next line + continue; + } + $first_character = $line{0}; + $matches = array(); + + if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) { + // First check if we are starting a new table + $indent_level = strlen( $matches[1] ); + + $attributes = $this->mStripState->unstripBoth( $matches[2] ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' ); + + $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>"; + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); + array_push ( $tr_history , false ); + array_push ( $tr_attributes , '' ); + array_push ( $has_opened_tr , false ); + } else if ( count ( $td_history ) == 0 ) { + // Don't do any of the following + continue; + } else if ( substr ( $line , 0 , 2 ) == '|}' ) { + // We are ending a table + $line = '</table>' . substr ( $line , 2 ); + $last_tag = array_pop ( $last_tag_history ); + + if ( !array_pop ( $has_opened_tr ) ) { + $line = "<tr><td></td></tr>{$line}"; + } + + if ( array_pop ( $tr_history ) ) { + $line = "</tr>{$line}"; + } + + if ( array_pop ( $td_history ) ) { + $line = "</{$last_tag}>{$line}"; + } + array_pop ( $tr_attributes ); + $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level ); + } else if ( substr ( $line , 0 , 2 ) == '|-' ) { + // Now we have a table row + $line = preg_replace( '#^\|-+#', '', $line ); + + // Whats after the tag is now only attributes + $attributes = $this->mStripState->unstripBoth( $line ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' ); + array_pop ( $tr_attributes ); + array_push ( $tr_attributes , $attributes ); + + $line = ''; + $last_tag = array_pop ( $last_tag_history ); + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ); + + if ( array_pop ( $tr_history ) ) { + $line = '</tr>'; + } + + if ( array_pop ( $td_history ) ) { + $line = "</{$last_tag}>{$line}"; + } + + $lines[$key] = $line; + array_push ( $tr_history , false ); + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); + } + else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { + // This might be cell elements, td, th or captions + if ( substr ( $line , 0 , 2 ) == '|+' ) { + $first_character = '+'; + $line = substr ( $line , 1 ); + } + + $line = substr ( $line , 1 ); + + if ( $first_character == '!' ) { + $line = str_replace ( '!!' , '||' , $line ); + } + + // Split up multiple cells on the same line. + // FIXME : This can result in improper nesting of tags processed + // by earlier parser steps, but should avoid splitting up eg + // attribute values containing literal "||". + $cells = StringUtils::explodeMarkup( '||' , $line ); + + $lines[$key] = ''; + + // Loop through each table cell + foreach ( $cells as $cell ) + { + $previous = ''; + if ( $first_character != '+' ) + { + $tr_after = array_pop ( $tr_attributes ); + if ( !array_pop ( $tr_history ) ) { + $previous = "<tr{$tr_after}>\n"; + } + array_push ( $tr_history , true ); + array_push ( $tr_attributes , '' ); + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ); + } + + $last_tag = array_pop ( $last_tag_history ); + + if ( array_pop ( $td_history ) ) { + $previous = "</{$last_tag}>{$previous}"; + } + + if ( $first_character == '|' ) { + $last_tag = 'td'; + } else if ( $first_character == '!' ) { + $last_tag = 'th'; + } else if ( $first_character == '+' ) { + $last_tag = 'caption'; + } else { + $last_tag = ''; + } + + array_push ( $last_tag_history , $last_tag ); + + // A cell could contain both parameters and data + $cell_data = explode ( '|' , $cell , 2 ); + + // Bug 553: Note that a '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if ( strpos( $cell_data[0], '[[' ) !== false ) { + $cell = "{$previous}<{$last_tag}>{$cell}"; + } else if ( count ( $cell_data ) == 1 ) + $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; + else { + $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); + $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag ); + $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; + } + + $lines[$key] .= $cell; + array_push ( $td_history , true ); + } + } + } + + // Closing open td, tr && table + while ( count ( $td_history ) > 0 ) + { + if ( array_pop ( $td_history ) ) { + $lines[] = '</td>' ; + } + if ( array_pop ( $tr_history ) ) { + $lines[] = '</tr>' ; + } + if ( !array_pop ( $has_opened_tr ) ) { + $lines[] = "<tr><td></td></tr>" ; + } + + $lines[] = '</table>' ; + } + + $output = implode ( "\n" , $lines ) ; + + // special case: don't return empty table + if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) { + $output = ''; + } + + wfProfileOut( $fname ); + + return $output; + } + + /** + * Helper function for parse() that transforms wiki markup into + * HTML. Only called for $mOutputType == OT_HTML. + * + * @private + */ + function internalParse( $text ) { + $args = array(); + $isMain = true; + $fname = 'Parser::internalParse'; + wfProfileIn( $fname ); + + # Hook to suspend the parser in this state + if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { + wfProfileOut( $fname ); + return $text ; + } + + # Remove <noinclude> tags and <includeonly> sections + $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) ); + $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') ); + $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text ); + + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) ); + + $text = $this->replaceVariables( $text, $args ); + wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); + + // Tables need to come after variable replacement for things to work + // properly; putting them before other transformations should keep + // exciting things like link expansions from showing up in surprising + // places. + $text = $this->doTableStuff( $text ); + + $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text ); + + $text = $this->stripToc( $text ); + $this->stripNoGallery( $text ); + $text = $this->doHeadings( $text ); + if($this->mOptions->getUseDynamicDates()) { + $df =& DateFormatter::getInstance(); + $text = $df->reformat( $this->mOptions->getDateFormat(), $text ); + } + $text = $this->doAllQuotes( $text ); + $text = $this->replaceInternalLinks( $text ); + $text = $this->replaceExternalLinks( $text ); + + # replaceInternalLinks may sometimes leave behind + # absolute URLs, which have to be masked to hide them from replaceExternalLinks + $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text); + + $text = $this->doMagicLinks( $text ); + $text = $this->formatHeadings( $text, $isMain ); + + wfProfileOut( $fname ); + return $text; + } + + /** + * Replace special strings like "ISBN xxx" and "RFC xxx" with + * magic external links. + * + * @private + */ + function &doMagicLinks( &$text ) { + wfProfileIn( __METHOD__ ); + $text = preg_replace_callback( + '!(?: # Start cases + <a.*?</a> | # Skip link text + <.*?> | # Skip stuff inside HTML elements + (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] + ISBN\s+(\b # ISBN, capture number as m[2] + (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix + (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters + [0-9Xx] # check digit + \b) + )!x', array( &$this, 'magicLinkCallback' ), $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + function magicLinkCallback( $m ) { + if ( substr( $m[0], 0, 1 ) == '<' ) { + # Skip HTML element + return $m[0]; + } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { + $isbn = $m[2]; + $num = strtr( $isbn, array( + '-' => '', + ' ' => '', + 'x' => 'X', + )); + $titleObj = SpecialPage::getTitleFor( 'Booksources' ); + $text = '<a href="' . + $titleObj->escapeLocalUrl( "isbn=$num" ) . + "\" class=\"internal\">ISBN $isbn</a>"; + } else { + if ( substr( $m[0], 0, 3 ) == 'RFC' ) { + $keyword = 'RFC'; + $urlmsg = 'rfcurl'; + $id = $m[1]; + } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) { + $keyword = 'PMID'; + $urlmsg = 'pubmedurl'; + $id = $m[1]; + } else { + throw new MWException( __METHOD__.': unrecognised match type "' . + substr($m[0], 0, 20 ) . '"' ); + } + + $url = wfMsg( $urlmsg, $id); + $sk = $this->mOptions->getSkin(); + $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); + $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; + } + return $text; + } + + /** + * Parse headers and return html + * + * @private + */ + function doHeadings( $text ) { + $fname = 'Parser::doHeadings'; + wfProfileIn( $fname ); + for ( $i = 6; $i >= 1; --$i ) { + $h = str_repeat( '=', $i ); + $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m", + "<h{$i}>\\1</h{$i}>\\2", $text ); + } + wfProfileOut( $fname ); + return $text; + } + + /** + * Replace single quotes with HTML markup + * @private + * @return string the altered text + */ + function doAllQuotes( $text ) { + $fname = 'Parser::doAllQuotes'; + wfProfileIn( $fname ); + $outtext = ''; + $lines = explode( "\n", $text ); + foreach ( $lines as $line ) { + $outtext .= $this->doQuotes ( $line ) . "\n"; + } + $outtext = substr($outtext, 0,-1); + wfProfileOut( $fname ); + return $outtext; + } + + /** + * Helper function for doAllQuotes() + */ + public function doQuotes( $text ) { + $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + if ( count( $arr ) == 1 ) + return $text; + else + { + # First, do some preliminary work. This may shift some apostrophes from + # being mark-up to being text. It also counts the number of occurrences + # of bold and italics mark-ups. + $i = 0; + $numbold = 0; + $numitalics = 0; + foreach ( $arr as $r ) + { + if ( ( $i % 2 ) == 1 ) + { + # If there are ever four apostrophes, assume the first is supposed to + # be text, and the remaining three constitute mark-up for bold text. + if ( strlen( $arr[$i] ) == 4 ) + { + $arr[$i-1] .= "'"; + $arr[$i] = "'''"; + } + # If there are more than 5 apostrophes in a row, assume they're all + # text except for the last 5. + else if ( strlen( $arr[$i] ) > 5 ) + { + $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 ); + $arr[$i] = "'''''"; + } + # Count the number of occurrences of bold and italics mark-ups. + # We are not counting sequences of five apostrophes. + if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; } + else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; } + else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } + } + $i++; + } + + # If there is an odd number of both bold and italics, it is likely + # that one of the bold ones was meant to be an apostrophe followed + # by italics. Which one we cannot know for certain, but it is more + # likely to be one that has a single-letter word before it. + if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) + { + $i = 0; + $firstsingleletterword = -1; + $firstmultiletterword = -1; + $firstspace = -1; + foreach ( $arr as $r ) + { + if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) + { + $x1 = substr ($arr[$i-1], -1); + $x2 = substr ($arr[$i-1], -2, 1); + if ($x1 == ' ') { + if ($firstspace == -1) $firstspace = $i; + } else if ($x2 == ' ') { + if ($firstsingleletterword == -1) $firstsingleletterword = $i; + } else { + if ($firstmultiletterword == -1) $firstmultiletterword = $i; + } + } + $i++; + } + + # If there is a single-letter word, use it! + if ($firstsingleletterword > -1) + { + $arr [ $firstsingleletterword ] = "''"; + $arr [ $firstsingleletterword-1 ] .= "'"; + } + # If not, but there's a multi-letter word, use that one. + else if ($firstmultiletterword > -1) + { + $arr [ $firstmultiletterword ] = "''"; + $arr [ $firstmultiletterword-1 ] .= "'"; + } + # ... otherwise use the first one that has neither. + # (notice that it is possible for all three to be -1 if, for example, + # there is only one pentuple-apostrophe in the line) + else if ($firstspace > -1) + { + $arr [ $firstspace ] = "''"; + $arr [ $firstspace-1 ] .= "'"; + } + } + + # Now let's actually convert our apostrophic mush to HTML! + $output = ''; + $buffer = ''; + $state = ''; + $i = 0; + foreach ($arr as $r) + { + if (($i % 2) == 0) + { + if ($state == 'both') + $buffer .= $r; + else + $output .= $r; + } + else + { + if (strlen ($r) == 2) + { + if ($state == 'i') + { $output .= '</i>'; $state = ''; } + else if ($state == 'bi') + { $output .= '</i>'; $state = 'b'; } + else if ($state == 'ib') + { $output .= '</b></i><b>'; $state = 'b'; } + else if ($state == 'both') + { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; } + else # $state can be 'b' or '' + { $output .= '<i>'; $state .= 'i'; } + } + else if (strlen ($r) == 3) + { + if ($state == 'b') + { $output .= '</b>'; $state = ''; } + else if ($state == 'bi') + { $output .= '</i></b><i>'; $state = 'i'; } + else if ($state == 'ib') + { $output .= '</b>'; $state = 'i'; } + else if ($state == 'both') + { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; } + else # $state can be 'i' or '' + { $output .= '<b>'; $state .= 'b'; } + } + else if (strlen ($r) == 5) + { + if ($state == 'b') + { $output .= '</b><i>'; $state = 'i'; } + else if ($state == 'i') + { $output .= '</i><b>'; $state = 'b'; } + else if ($state == 'bi') + { $output .= '</i></b>'; $state = ''; } + else if ($state == 'ib') + { $output .= '</b></i>'; $state = ''; } + else if ($state == 'both') + { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; } + else # ($state == '') + { $buffer = ''; $state = 'both'; } + } + } + $i++; + } + # Now close all remaining tags. Notice that the order is important. + if ($state == 'b' || $state == 'ib') + $output .= '</b>'; + if ($state == 'i' || $state == 'bi' || $state == 'ib') + $output .= '</i>'; + if ($state == 'bi') + $output .= '</b>'; + # There might be lonely ''''', so make sure we have a buffer + if ($state == 'both' && $buffer) + $output .= '<b><i>'.$buffer.'</i></b>'; + return $output; + } + } + + /** + * Replace external links + * + * Note: this is all very hackish and the order of execution matters a lot. + * Make sure to run maintenance/parserTests.php if you change this code. + * + * @private + */ + function replaceExternalLinks( $text ) { + global $wgContLang; + $fname = 'Parser::replaceExternalLinks'; + wfProfileIn( $fname ); + + $sk = $this->mOptions->getSkin(); + + $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + + $s = $this->replaceFreeExternalLinks( array_shift( $bits ) ); + + $i = 0; + while ( $i<count( $bits ) ) { + $url = $bits[$i++]; + $protocol = $bits[$i++]; + $text = $bits[$i++]; + $trail = $bits[$i++]; + + # The characters '<' and '>' (which were escaped by + # removeHTMLtags()) should not be included in + # URLs, per RFC 2396. + $m2 = array(); + if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { + $text = substr($url, $m2[0][1]) . ' ' . $text; + $url = substr($url, 0, $m2[0][1]); + } + + # If the link text is an image URL, replace it with an <img> tag + # This happened by accident in the original parser, but some people used it extensively + $img = $this->maybeMakeExternalImage( $text ); + if ( $img !== false ) { + $text = $img; + } + + $dtrail = ''; + + # Set linktype for CSS - if URL==text, link is essentially free + $linktype = ($text == $url) ? 'free' : 'text'; + + # No link text, e.g. [http://domain.tld/some.link] + if ( $text == '' ) { + # Autonumber if allowed. See bug #5918 + if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) { + $text = '[' . ++$this->mAutonumber . ']'; + $linktype = 'autonumber'; + } else { + # Otherwise just use the URL + $text = htmlspecialchars( $url ); + $linktype = 'free'; + } + } else { + # Have link text, e.g. [http://domain.tld/some.link text]s + # Check for trail + list( $dtrail, $trail ) = Linker::splitTrail( $trail ); + } + + $text = $wgContLang->markNoConversion($text); + + $url = Sanitizer::cleanUrl( $url ); + + # Process the trail (i.e. everything after this link up until start of the next link), + # replacing any non-bracketed links + $trail = $this->replaceFreeExternalLinks( $trail ); + + # Use the encoded URL + # This means that users can paste URLs directly into the text + # Funny characters like ö aren't valid in URLs anyway + # This was changed in August 2004 + $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; + + # Register link in the output object. + # Replace unnecessary URL escape codes with the referenced character + # This prevents spammers from hiding links from the filters + $pasteurized = self::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); + } + + wfProfileOut( $fname ); + return $s; + } + + /** + * Replace anything that looks like a URL with a link + * @private + */ + function replaceFreeExternalLinks( $text ) { + global $wgContLang; + $fname = 'Parser::replaceFreeExternalLinks'; + wfProfileIn( $fname ); + + $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + $s = array_shift( $bits ); + $i = 0; + + $sk = $this->mOptions->getSkin(); + + while ( $i < count( $bits ) ){ + $protocol = $bits[$i++]; + $remainder = $bits[$i++]; + + $m = array(); + if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { + # Found some characters after the protocol that look promising + $url = $protocol . $m[1]; + $trail = $m[2]; + + # special case: handle urls as url args: + # http://www.example.com/foo?=http://www.example.com/bar + if(strlen($trail) == 0 && + isset($bits[$i]) && + preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && + preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) + { + # add protocol, arg + $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link + $i += 2; + $trail = $m[2]; + } + + # The characters '<' and '>' (which were escaped by + # removeHTMLtags()) should not be included in + # URLs, per RFC 2396. + $m2 = array(); + if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { + $trail = substr($url, $m2[0][1]) . $trail; + $url = substr($url, 0, $m2[0][1]); + } + + # Move trailing punctuation to $trail + $sep = ',;\.:!?'; + # If there is no left bracket, then consider right brackets fair game too + if ( strpos( $url, '(' ) === false ) { + $sep .= ')'; + } + + $numSepChars = strspn( strrev( $url ), $sep ); + if ( $numSepChars ) { + $trail = substr( $url, -$numSepChars ) . $trail; + $url = substr( $url, 0, -$numSepChars ); + } + + $url = Sanitizer::cleanUrl( $url ); + + # Is this an external image? + $text = $this->maybeMakeExternalImage( $url ); + if ( $text === false ) { + # Not an image, make a link + $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() ); + # Register it in the output object... + # Replace unnecessary URL escape codes with their equivalent characters + $pasteurized = self::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); + } + $s .= $text . $trail; + } else { + $s .= $protocol . $remainder; + } + } + wfProfileOut( $fname ); + return $s; + } + + /** + * Replace unusual URL escape codes with their equivalent characters + * @param string + * @return string + * @static + * @todo This can merge genuinely required bits in the path or query string, + * breaking legit URLs. A proper fix would treat the various parts of + * the URL differently; as a workaround, just use the output for + * statistical records, not for actual linking/output. + */ + static function replaceUnusualEscapes( $url ) { + return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', + array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url ); + } + + /** + * Callback function used in replaceUnusualEscapes(). + * Replaces unusual URL escape codes with their equivalent character + * @static + * @private + */ + private static function replaceUnusualEscapesCallback( $matches ) { + $char = urldecode( $matches[0] ); + $ord = ord( $char ); + // Is it an unsafe or HTTP reserved character according to RFC 1738? + if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { + // No, shouldn't be escaped + return $char; + } else { + // Yes, leave it escaped + return $matches[0]; + } + } + + /** + * make an image if it's allowed, either through the global + * option or through the exception + * @private + */ + function maybeMakeExternalImage( $url ) { + $sk = $this->mOptions->getSkin(); + $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); + $imagesexception = !empty($imagesfrom); + $text = false; + if ( $this->mOptions->getAllowExternalImages() + || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { + if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { + # Image found + $text = $sk->makeExternalImage( htmlspecialchars( $url ) ); + } + } + return $text; + } + + /** + * Process [[ ]] wikilinks + * + * @private + */ + function replaceInternalLinks( $s ) { + global $wgContLang; + static $fname = 'Parser::replaceInternalLinks' ; + + wfProfileIn( $fname ); + + wfProfileIn( $fname.'-setup' ); + static $tc = FALSE; + # the % is needed to support urlencoded titles as well + if ( !$tc ) { $tc = Title::legalChars() . '#%'; } + + $sk = $this->mOptions->getSkin(); + + #split the entire text string on occurences of [[ + $a = explode( '[[', ' ' . $s ); + #get the first element (all text up to first [[), and remove the space we added + $s = array_shift( $a ); + $s = substr( $s, 1 ); + + # Match a link having the form [[namespace:link|alternate]]trail + static $e1 = FALSE; + if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; } + # Match cases where there is no "]]", which might still be images + static $e1_img = FALSE; + if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; } + # Match the end of a line for a word that's not followed by whitespace, + # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched + $e2 = wfMsgForContent( 'linkprefix' ); + + $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); + if( is_null( $this->mTitle ) ) { + throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); + } + $nottalk = !$this->mTitle->isTalkPage(); + + if ( $useLinkPrefixExtension ) { + $m = array(); + if ( preg_match( $e2, $s, $m ) ) { + $first_prefix = $m[2]; + } else { + $first_prefix = false; + } + } else { + $prefix = ''; + } + + if($wgContLang->hasVariants()) { + $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); + } else { + $selflink = array($this->mTitle->getPrefixedText()); + } + $useSubpages = $this->areSubpagesAllowed(); + wfProfileOut( $fname.'-setup' ); + + # Loop for each link + for ($k = 0; isset( $a[$k] ); $k++) { + $line = $a[$k]; + if ( $useLinkPrefixExtension ) { + wfProfileIn( $fname.'-prefixhandling' ); + if ( preg_match( $e2, $s, $m ) ) { + $prefix = $m[2]; + $s = $m[1]; + } else { + $prefix=''; + } + # first link + if($first_prefix) { + $prefix = $first_prefix; + $first_prefix = false; + } + wfProfileOut( $fname.'-prefixhandling' ); + } + + $might_be_img = false; + + wfProfileIn( "$fname-e1" ); + if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt + $text = $m[2]; + # If we get a ] at the beginning of $m[3] that means we have a link that's something like: + # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, + # the real problem is with the $e1 regex + # See bug 1300. + # + # Still some problems for cases where the ] is meant to be outside punctuation, + # and no image is in sight. See bug 2095. + # + if( $text !== '' && + substr( $m[3], 0, 1 ) === ']' && + strpos($text, '[') !== false + ) + { + $text .= ']'; # so that replaceExternalLinks($text) works later + $m[3] = substr( $m[3], 1 ); + } + # fix up urlencoded title texts + if( strpos( $m[1], '%' ) !== false ) { + # Should anchors '#' also be rejected? + $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + } + $trail = $m[3]; + } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption + $might_be_img = true; + $text = $m[2]; + if ( strpos( $m[1], '%' ) !== false ) { + $m[1] = urldecode($m[1]); + } + $trail = ""; + } else { # Invalid form; output directly + $s .= $prefix . '[[' . $line ; + wfProfileOut( "$fname-e1" ); + continue; + } + wfProfileOut( "$fname-e1" ); + wfProfileIn( "$fname-misc" ); + + # Don't allow internal links to pages containing + # PROTO: where PROTO is a valid URL protocol; these + # should be external links. + if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) { + $s .= $prefix . '[[' . $line ; + continue; + } + + # Make subpage if necessary + if( $useSubpages ) { + $link = $this->maybeDoSubpageLink( $m[1], $text ); + } else { + $link = $m[1]; + } + + $noforce = (substr($m[1], 0, 1) != ':'); + if (!$noforce) { + # Strip off leading ':' + $link = substr($link, 1); + } + + wfProfileOut( "$fname-misc" ); + wfProfileIn( "$fname-title" ); + $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) ); + if( !$nt ) { + $s .= $prefix . '[[' . $line; + wfProfileOut( "$fname-title" ); + continue; + } + + $ns = $nt->getNamespace(); + $iw = $nt->getInterWiki(); + wfProfileOut( "$fname-title" ); + + if ($might_be_img) { # if this is actually an invalid link + wfProfileIn( "$fname-might_be_img" ); + if ($ns == NS_IMAGE && $noforce) { #but might be an image + $found = false; + while (isset ($a[$k+1]) ) { + #look at the next 'line' to see if we can close it there + $spliced = array_splice( $a, $k + 1, 1 ); + $next_line = array_shift( $spliced ); + $m = explode( ']]', $next_line, 3 ); + if ( count( $m ) == 3 ) { + # the first ]] closes the inner link, the second the image + $found = true; + $text .= "[[{$m[0]}]]{$m[1]}"; + $trail = $m[2]; + break; + } elseif ( count( $m ) == 2 ) { + #if there's exactly one ]] that's fine, we'll keep looking + $text .= "[[{$m[0]}]]{$m[1]}"; + } else { + #if $next_line is invalid too, we need look no further + $text .= '[[' . $next_line; + break; + } + } + if ( !$found ) { + # we couldn't find the end of this imageLink, so output it raw + #but don't ignore what might be perfectly normal links in the text we've examined + $text = $this->replaceInternalLinks($text); + $s .= "{$prefix}[[$link|$text"; + # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); + continue; + } + } else { #it's not an image, so output it raw + $s .= "{$prefix}[[$link|$text"; + # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); + continue; + } + wfProfileOut( "$fname-might_be_img" ); + } + + $wasblank = ( '' == $text ); + if( $wasblank ) $text = $link; + + # Link not escaped by : , create the various objects + if( $noforce ) { + + # Interwikis + wfProfileIn( "$fname-interwiki" ); + if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { + $this->mOutput->addLanguageLink( $nt->getFullText() ); + $s = rtrim($s . $prefix); + $s .= trim($trail, "\n") == '' ? '': $prefix . $trail; + wfProfileOut( "$fname-interwiki" ); + continue; + } + wfProfileOut( "$fname-interwiki" ); + + if ( $ns == NS_IMAGE ) { + wfProfileIn( "$fname-image" ); + if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { + # recursively parse links inside the image caption + # actually, this will parse them in any other parameters, too, + # but it might be hard to fix that, and it doesn't matter ATM + $text = $this->replaceExternalLinks($text); + $text = $this->replaceInternalLinks($text); + + # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them + $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); + + wfProfileOut( "$fname-image" ); + continue; + } else { + # We still need to record the image's presence on the page + $this->mOutput->addImage( $nt->getDBkey() ); + } + wfProfileOut( "$fname-image" ); + + } + + if ( $ns == NS_CATEGORY ) { + wfProfileIn( "$fname-category" ); + $s = rtrim($s . "\n"); # bug 87 + + if ( $wasblank ) { + $sortkey = $this->getDefaultSort(); + } else { + $sortkey = $text; + } + $sortkey = Sanitizer::decodeCharReferences( $sortkey ); + $sortkey = str_replace( "\n", '', $sortkey ); + $sortkey = $wgContLang->convertCategoryKey( $sortkey ); + $this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); + + /** + * Strip the whitespace Category links produce, see bug 87 + * @todo We might want to use trim($tmp, "\n") here. + */ + $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + + wfProfileOut( "$fname-category" ); + continue; + } + } + + # Self-link checking + if( $nt->getFragment() === '' ) { + if( in_array( $nt->getPrefixedText(), $selflink, true ) ) { + $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); + continue; + } + } + + # Special and Media are pseudo-namespaces; no pages actually exist in them + if( $ns == NS_MEDIA ) { + $link = $sk->makeMediaLinkObj( $nt, $text ); + # Cloak with NOPARSE to avoid replacement in replaceExternalLinks + $s .= $prefix . $this->armorLinks( $link ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); + continue; + } elseif( $ns == NS_SPECIAL ) { + if( SpecialPage::exists( $nt->getDBkey() ) ) { + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + } else { + $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); + } + continue; + } elseif( $ns == NS_IMAGE ) { + $img = wfFindFile( $nt ); + if( $img ) { + // Force a blue link if the file exists; may be a remote + // upload on the shared repository, and we want to see its + // auto-generated page. + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + $this->mOutput->addLink( $nt ); + continue; + } + } + $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); + } + wfProfileOut( $fname ); + return $s; + } + + /** + * Make a link placeholder. The text returned can be later resolved to a real link with + * replaceLinkHolders(). This is done for two reasons: firstly to avoid further + * parsing of interwiki links, and secondly to allow all existence checks and + * article length checks (for stub links) to be bundled into a single query. + * + */ + function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + wfProfileIn( __METHOD__ ); + if ( ! is_object($nt) ) { + # Fail gracefully + $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}"; + } else { + # Separate the link trail from the rest of the link + list( $inside, $trail ) = Linker::splitTrail( $trail ); + + if ( $nt->isExternal() ) { + $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside ); + $this->mInterwikiLinkHolders['titles'][] = $nt; + $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}"; + } else { + $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() ); + $this->mLinkHolders['dbkeys'][] = $nt->getDBkey(); + $this->mLinkHolders['queries'][] = $query; + $this->mLinkHolders['texts'][] = $prefix.$text.$inside; + $this->mLinkHolders['titles'][] = $nt; + + $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}"; + } + } + wfProfileOut( __METHOD__ ); + return $retVal; + } + + /** + * Render a forced-blue link inline; protect against double expansion of + * URLs if we're in a mode that prepends full URL prefixes to internal links. + * Since this little disaster has to split off the trail text to avoid + * breaking URLs in the following text without breaking trails on the + * wiki links, it's been made into a horrible function. + * + * @param Title $nt + * @param string $text + * @param string $query + * @param string $trail + * @param string $prefix + * @return string HTML-wikitext mix oh yuck + */ + function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $sk = $this->mOptions->getSkin(); + $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix ); + return $this->armorLinks( $link ) . $trail; + } + + /** + * Insert a NOPARSE hacky thing into any inline links in a chunk that's + * going to go through further parsing steps before inline URL expansion. + * + * In particular this is important when using action=render, which causes + * full URLs to be included. + * + * Oh man I hate our multi-layer parser! + * + * @param string more-or-less HTML + * @return string less-or-more HTML with NOPARSE bits + */ + function armorLinks( $text ) { + return preg_replace( '/\b(' . wfUrlProtocols() . ')/', + "{$this->mUniqPrefix}NOPARSE$1", $text ); + } + + /** + * Return true if subpage links should be expanded on this page. + * @return bool + */ + function areSubpagesAllowed() { + # Some namespaces don't allow subpages + global $wgNamespacesWithSubpages; + return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]); + } + + /** + * Handle link to subpage if necessary + * @param string $target the source of the link + * @param string &$text the link text, modified as necessary + * @return string the full name of the link + * @private + */ + function maybeDoSubpageLink($target, &$text) { + # Valid link forms: + # Foobar -- normal + # :Foobar -- override special treatment of prefix (images, language links) + # /Foobar -- convert to CurrentPage/Foobar + # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text + # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage + # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage + + $fname = 'Parser::maybeDoSubpageLink'; + wfProfileIn( $fname ); + $ret = $target; # default return value is no change + + # Some namespaces don't allow subpages, + # so only perform processing if subpages are allowed + if( $this->areSubpagesAllowed() ) { + $hash = strpos( $target, '#' ); + if( $hash !== false ) { + $suffix = substr( $target, $hash ); + $target = substr( $target, 0, $hash ); + } else { + $suffix = ''; + } + # bug 7425 + $target = trim( $target ); + # Look at the first character + if( $target != '' && $target{0} == '/' ) { + # / at end means we don't want the slash to be shown + $m = array(); + $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); + if( $trailingSlashes ) { + $noslash = $target = substr( $target, 1, -strlen($m[0][0]) ); + } else { + $noslash = substr( $target, 1 ); + } + + $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix; + if( '' === $text ) { + $text = $target . $suffix; + } # this might be changed for ugliness reasons + } else { + # check for .. subpage backlinks + $dotdotcount = 0; + $nodotdot = $target; + while( strncmp( $nodotdot, "../", 3 ) == 0 ) { + ++$dotdotcount; + $nodotdot = substr( $nodotdot, 3 ); + } + if($dotdotcount > 0) { + $exploded = explode( '/', $this->mTitle->GetPrefixedText() ); + if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page + $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) ); + # / at the end means don't show full path + if( substr( $nodotdot, -1, 1 ) == '/' ) { + $nodotdot = substr( $nodotdot, 0, -1 ); + if( '' === $text ) { + $text = $nodotdot . $suffix; + } + } + $nodotdot = trim( $nodotdot ); + if( $nodotdot != '' ) { + $ret .= '/' . $nodotdot; + } + $ret .= $suffix; + } + } + } + } + + wfProfileOut( $fname ); + return $ret; + } + + /**#@+ + * Used by doBlockLevels() + * @private + */ + /* private */ function closeParagraph() { + $result = ''; + if ( '' != $this->mLastSection ) { + $result = '</' . $this->mLastSection . ">\n"; + } + $this->mInPre = false; + $this->mLastSection = ''; + return $result; + } + # getCommon() returns the length of the longest common substring + # of both arguments, starting at the beginning of both. + # + /* private */ function getCommon( $st1, $st2 ) { + $fl = strlen( $st1 ); + $shorter = strlen( $st2 ); + if ( $fl < $shorter ) { $shorter = $fl; } + + for ( $i = 0; $i < $shorter; ++$i ) { + if ( $st1{$i} != $st2{$i} ) { break; } + } + return $i; + } + # These next three functions open, continue, and close the list + # element appropriate to the prefix character passed into them. + # + /* private */ function openList( $char ) { + $result = $this->closeParagraph(); + + if ( '*' == $char ) { $result .= '<ul><li>'; } + else if ( '#' == $char ) { $result .= '<ol><li>'; } + else if ( ':' == $char ) { $result .= '<dl><dd>'; } + else if ( ';' == $char ) { + $result .= '<dl><dt>'; + $this->mDTopen = true; + } + else { $result = '<!-- ERR 1 -->'; } + + return $result; + } + + /* private */ function nextItem( $char ) { + if ( '*' == $char || '#' == $char ) { return '</li><li>'; } + else if ( ':' == $char || ';' == $char ) { + $close = '</dd>'; + if ( $this->mDTopen ) { $close = '</dt>'; } + if ( ';' == $char ) { + $this->mDTopen = true; + return $close . '<dt>'; + } else { + $this->mDTopen = false; + return $close . '<dd>'; + } + } + return '<!-- ERR 2 -->'; + } + + /* private */ function closeList( $char ) { + if ( '*' == $char ) { $text = '</li></ul>'; } + else if ( '#' == $char ) { $text = '</li></ol>'; } + else if ( ':' == $char ) { + if ( $this->mDTopen ) { + $this->mDTopen = false; + $text = '</dt></dl>'; + } else { + $text = '</dd></dl>'; + } + } + else { return '<!-- ERR 3 -->'; } + return $text."\n"; + } + /**#@-*/ + + /** + * Make lists from lines starting with ':', '*', '#', etc. + * + * @private + * @return string the lists rendered as HTML + */ + function doBlockLevels( $text, $linestart ) { + $fname = 'Parser::doBlockLevels'; + wfProfileIn( $fname ); + + # Parsing through the text line by line. The main thing + # happening here is handling of block-level elements p, pre, + # and making lists from lines starting with * # : etc. + # + $textLines = explode( "\n", $text ); + + $lastPrefix = $output = ''; + $this->mDTopen = $inBlockElem = false; + $prefixLength = 0; + $paragraphStack = false; + + if ( !$linestart ) { + $output .= array_shift( $textLines ); + } + foreach ( $textLines as $oLine ) { + $lastPrefixLength = strlen( $lastPrefix ); + $preCloseMatch = preg_match('/<\\/pre/i', $oLine ); + $preOpenMatch = preg_match('/<pre/i', $oLine ); + if ( !$this->mInPre ) { + # Multiple prefixes may abut each other for nested lists. + $prefixLength = strspn( $oLine, '*#:;' ); + $pref = substr( $oLine, 0, $prefixLength ); + + # eh? + $pref2 = str_replace( ';', ':', $pref ); + $t = substr( $oLine, $prefixLength ); + $this->mInPre = !empty($preOpenMatch); + } else { + # Don't interpret any other prefixes in preformatted text + $prefixLength = 0; + $pref = $pref2 = ''; + $t = $oLine; + } + + # List generation + if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) { + # Same as the last item, so no need to deal with nesting or opening stuff + $output .= $this->nextItem( substr( $pref, -1 ) ); + $paragraphStack = false; + + if ( substr( $pref, -1 ) == ';') { + # The one nasty exception: definition lists work like this: + # ; title : definition text + # So we check for : in the remainder text to split up the + # title and definition, without b0rking links. + $term = $t2 = ''; + if ($this->findColonNoLinks($t, $term, $t2) !== false) { + $t = $t2; + $output .= $term . $this->nextItem( ':' ); + } + } + } elseif( $prefixLength || $lastPrefixLength ) { + # Either open or close a level... + $commonPrefixLength = $this->getCommon( $pref, $lastPrefix ); + $paragraphStack = false; + + while( $commonPrefixLength < $lastPrefixLength ) { + $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} ); + --$lastPrefixLength; + } + if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) { + $output .= $this->nextItem( $pref{$commonPrefixLength-1} ); + } + while ( $prefixLength > $commonPrefixLength ) { + $char = substr( $pref, $commonPrefixLength, 1 ); + $output .= $this->openList( $char ); + + if ( ';' == $char ) { + # FIXME: This is dupe of code above + if ($this->findColonNoLinks($t, $term, $t2) !== false) { + $t = $t2; + $output .= $term . $this->nextItem( ':' ); + } + } + ++$commonPrefixLength; + } + $lastPrefix = $pref2; + } + if( 0 == $prefixLength ) { + wfProfileIn( "$fname-paragraph" ); + # No prefix (not in list)--go to paragraph mode + // XXX: use a stack for nestable elements like span, table and div + $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); + $closematch = preg_match( + '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. + '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); + if ( $openmatch or $closematch ) { + $paragraphStack = false; + # TODO bug 5718: paragraph closed + $output .= $this->closeParagraph(); + if ( $preOpenMatch and !$preCloseMatch ) { + $this->mInPre = true; + } + if ( $closematch ) { + $inBlockElem = false; + } else { + $inBlockElem = true; + } + } else if ( !$inBlockElem && !$this->mInPre ) { + if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) { + // pre + if ($this->mLastSection != 'pre') { + $paragraphStack = false; + $output .= $this->closeParagraph().'<pre>'; + $this->mLastSection = 'pre'; + } + $t = substr( $t, 1 ); + } else { + // paragraph + if ( '' == trim($t) ) { + if ( $paragraphStack ) { + $output .= $paragraphStack.'<br />'; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else { + if ($this->mLastSection != 'p' ) { + $output .= $this->closeParagraph(); + $this->mLastSection = ''; + $paragraphStack = '<p>'; + } else { + $paragraphStack = '</p><p>'; + } + } + } else { + if ( $paragraphStack ) { + $output .= $paragraphStack; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else if ($this->mLastSection != 'p') { + $output .= $this->closeParagraph().'<p>'; + $this->mLastSection = 'p'; + } + } + } + } + wfProfileOut( "$fname-paragraph" ); + } + // somewhere above we forget to get out of pre block (bug 785) + if($preCloseMatch && $this->mInPre) { + $this->mInPre = false; + } + if ($paragraphStack === false) { + $output .= $t."\n"; + } + } + while ( $prefixLength ) { + $output .= $this->closeList( $pref2{$prefixLength-1} ); + --$prefixLength; + } + if ( '' != $this->mLastSection ) { + $output .= '</' . $this->mLastSection . '>'; + $this->mLastSection = ''; + } + + wfProfileOut( $fname ); + return $output; + } + + /** + * Split up a string on ':', ignoring any occurences inside tags + * to prevent illegal overlapping. + * @param string $str the string to split + * @param string &$before set to everything before the ':' + * @param string &$after set to everything after the ':' + * return string the position of the ':', or false if none found + */ + function findColonNoLinks($str, &$before, &$after) { + $fname = 'Parser::findColonNoLinks'; + wfProfileIn( $fname ); + + $pos = strpos( $str, ':' ); + if( $pos === false ) { + // Nothing to find! + wfProfileOut( $fname ); + return false; + } + + $lt = strpos( $str, '<' ); + if( $lt === false || $lt > $pos ) { + // Easy; no tag nesting to worry about + $before = substr( $str, 0, $pos ); + $after = substr( $str, $pos+1 ); + wfProfileOut( $fname ); + return $pos; + } + + // Ugly state machine to walk through avoiding tags. + $state = self::COLON_STATE_TEXT; + $stack = 0; + $len = strlen( $str ); + for( $i = 0; $i < $len; $i++ ) { + $c = $str{$i}; + + switch( $state ) { + // (Using the number is a performance hack for common cases) + case 0: // self::COLON_STATE_TEXT: + switch( $c ) { + case "<": + // Could be either a <start> tag or an </end> tag + $state = self::COLON_STATE_TAGSTART; + break; + case ":": + if( $stack == 0 ) { + // We found it! + $before = substr( $str, 0, $i ); + $after = substr( $str, $i + 1 ); + wfProfileOut( $fname ); + return $i; + } + // Embedded in a tag; don't break it. + break; + default: + // Skip ahead looking for something interesting + $colon = strpos( $str, ':', $i ); + if( $colon === false ) { + // Nothing else interesting + wfProfileOut( $fname ); + return false; + } + $lt = strpos( $str, '<', $i ); + if( $stack === 0 ) { + if( $lt === false || $colon < $lt ) { + // We found it! + $before = substr( $str, 0, $colon ); + $after = substr( $str, $colon + 1 ); + wfProfileOut( $fname ); + return $i; + } + } + if( $lt === false ) { + // Nothing else interesting to find; abort! + // We're nested, but there's no close tags left. Abort! + break 2; + } + // Skip ahead to next tag start + $i = $lt; + $state = self::COLON_STATE_TAGSTART; + } + break; + case 1: // self::COLON_STATE_TAG: + // In a <tag> + switch( $c ) { + case ">": + $stack++; + $state = self::COLON_STATE_TEXT; + break; + case "/": + // Slash may be followed by >? + $state = self::COLON_STATE_TAGSLASH; + break; + default: + // ignore + } + break; + case 2: // self::COLON_STATE_TAGSTART: + switch( $c ) { + case "/": + $state = self::COLON_STATE_CLOSETAG; + break; + case "!": + $state = self::COLON_STATE_COMMENT; + break; + case ">": + // Illegal early close? This shouldn't happen D: + $state = self::COLON_STATE_TEXT; + break; + default: + $state = self::COLON_STATE_TAG; + } + break; + case 3: // self::COLON_STATE_CLOSETAG: + // In a </tag> + if( $c == ">" ) { + $stack--; + if( $stack < 0 ) { + wfDebug( "Invalid input in $fname; too many close tags\n" ); + wfProfileOut( $fname ); + return false; + } + $state = self::COLON_STATE_TEXT; + } + break; + case self::COLON_STATE_TAGSLASH: + if( $c == ">" ) { + // Yes, a self-closed tag <blah/> + $state = self::COLON_STATE_TEXT; + } else { + // Probably we're jumping the gun, and this is an attribute + $state = self::COLON_STATE_TAG; + } + break; + case 5: // self::COLON_STATE_COMMENT: + if( $c == "-" ) { + $state = self::COLON_STATE_COMMENTDASH; + } + break; + case self::COLON_STATE_COMMENTDASH: + if( $c == "-" ) { + $state = self::COLON_STATE_COMMENTDASHDASH; + } else { + $state = self::COLON_STATE_COMMENT; + } + break; + case self::COLON_STATE_COMMENTDASHDASH: + if( $c == ">" ) { + $state = self::COLON_STATE_TEXT; + } else { + $state = self::COLON_STATE_COMMENT; + } + break; + default: + throw new MWException( "State machine error in $fname" ); + } + } + if( $stack > 0 ) { + wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" ); + return false; + } + wfProfileOut( $fname ); + return false; + } + + /** + * Return value of a magic variable (like PAGENAME) + * + * @private + */ + function getVariableValue( $index ) { + global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath; + + /** + * Some of these require message or data lookups and can be + * expensive to check many times. + */ + static $varCache = array(); + if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) { + if ( isset( $varCache[$index] ) ) { + return $varCache[$index]; + } + } + + $ts = time(); + wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); + + # Use the time zone + global $wgLocaltimezone; + if ( isset( $wgLocaltimezone ) ) { + $oldtz = getenv( 'TZ' ); + putenv( 'TZ='.$wgLocaltimezone ); + } + + wfSuppressWarnings(); // E_STRICT system time bitching + $localTimestamp = date( 'YmdHis', $ts ); + $localMonth = date( 'm', $ts ); + $localMonthName = date( 'n', $ts ); + $localDay = date( 'j', $ts ); + $localDay2 = date( 'd', $ts ); + $localDayOfWeek = date( 'w', $ts ); + $localWeek = date( 'W', $ts ); + $localYear = date( 'Y', $ts ); + $localHour = date( 'H', $ts ); + if ( isset( $wgLocaltimezone ) ) { + putenv( 'TZ='.$oldtz ); + } + wfRestoreWarnings(); + + switch ( $index ) { + case 'currentmonth': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); + case 'currentmonthname': + return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); + case 'currentmonthnamegen': + return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); + case 'currentmonthabbrev': + return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); + case 'currentday': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); + case 'currentday2': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); + case 'localmonth': + return $varCache[$index] = $wgContLang->formatNum( $localMonth ); + case 'localmonthname': + return $varCache[$index] = $wgContLang->getMonthName( $localMonthName ); + case 'localmonthnamegen': + return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); + case 'localmonthabbrev': + return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); + case 'localday': + return $varCache[$index] = $wgContLang->formatNum( $localDay ); + case 'localday2': + return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); + case 'pagename': + return wfEscapeWikiText( $this->mTitle->getText() ); + case 'pagenamee': + return $this->mTitle->getPartialURL(); + case 'fullpagename': + return wfEscapeWikiText( $this->mTitle->getPrefixedText() ); + case 'fullpagenamee': + return $this->mTitle->getPrefixedURL(); + case 'subpagename': + return wfEscapeWikiText( $this->mTitle->getSubpageText() ); + case 'subpagenamee': + return $this->mTitle->getSubpageUrlForm(); + case 'basepagename': + return wfEscapeWikiText( $this->mTitle->getBaseText() ); + case 'basepagenamee': + return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ); + case 'talkpagename': + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return wfEscapeWikiText( $talkPage->getPrefixedText() ); + } else { + return ''; + } + case 'talkpagenamee': + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return $talkPage->getPrefixedUrl(); + } else { + return ''; + } + case 'subjectpagename': + $subjPage = $this->mTitle->getSubjectPage(); + return wfEscapeWikiText( $subjPage->getPrefixedText() ); + case 'subjectpagenamee': + $subjPage = $this->mTitle->getSubjectPage(); + return $subjPage->getPrefixedUrl(); + case 'revisionid': + return $this->mRevisionId; + case 'revisionday': + return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); + case 'revisionday2': + return substr( $this->getRevisionTimestamp(), 6, 2 ); + case 'revisionmonth': + return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); + case 'revisionyear': + return substr( $this->getRevisionTimestamp(), 0, 4 ); + case 'revisiontimestamp': + return $this->getRevisionTimestamp(); + case 'namespace': + return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case 'namespacee': + return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case 'talkspace': + return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : ''; + case 'talkspacee': + return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; + case 'subjectspace': + return $this->mTitle->getSubjectNsText(); + case 'subjectspacee': + return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); + case 'currentdayname': + return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); + case 'currentyear': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true ); + case 'currenttime': + return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); + case 'currenthour': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true ); + case 'currentweek': + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); + case 'currentdow': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); + case 'localdayname': + return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); + case 'localyear': + return $varCache[$index] = $wgContLang->formatNum( $localYear, true ); + case 'localtime': + return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false ); + case 'localhour': + return $varCache[$index] = $wgContLang->formatNum( $localHour, true ); + case 'localweek': + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek ); + case 'localdow': + return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); + case 'numberofarticles': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); + case 'numberoffiles': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() ); + case 'numberofusers': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() ); + case 'numberofpages': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); + case 'numberofadmins': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); + case 'numberofedits': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); + case 'currenttimestamp': + return $varCache[$index] = wfTimestampNow(); + case 'localtimestamp': + return $varCache[$index] = $localTimestamp; + case 'currentversion': + return $varCache[$index] = SpecialVersion::getVersion(); + case 'sitename': + return $wgSitename; + case 'server': + return $wgServer; + case 'servername': + return $wgServerName; + case 'scriptpath': + return $wgScriptPath; + case 'directionmark': + return $wgContLang->getDirMark(); + case 'contentlanguage': + global $wgContLanguageCode; + return $wgContLanguageCode; + default: + $ret = null; + if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) ) + return $ret; + else + return null; + } + } + + /** + * initialise the magic variables (like CURRENTMONTHNAME) + * + * @private + */ + function initialiseVariables() { + $fname = 'Parser::initialiseVariables'; + wfProfileIn( $fname ); + $variableIDs = MagicWord::getVariableIDs(); + + $this->mVariables = array(); + foreach ( $variableIDs as $id ) { + $mw =& MagicWord::get( $id ); + $mw->addToArray( $this->mVariables, $id ); + } + wfProfileOut( $fname ); + } + + /** + * parse any parentheses in format ((title|part|part)) + * and call callbacks to get a replacement text for any found piece + * + * @param string $text The text to parse + * @param array $callbacks rules in form: + * '{' => array( # opening parentheses + * 'end' => '}', # closing parentheses + * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found + * 3 => callback # replacement callback to call if {{{..}}} is found + * ) + * ) + * 'min' => 2, # Minimum parenthesis count in cb + * 'max' => 3, # Maximum parenthesis count in cb + * @private + */ + function replace_callback ($text, $callbacks) { + wfProfileIn( __METHOD__ ); + $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet + $lastOpeningBrace = -1; # last not closed parentheses + + $validOpeningBraces = implode( '', array_keys( $callbacks ) ); + + $i = 0; + while ( $i < strlen( $text ) ) { + # Find next opening brace, closing brace or pipe + if ( $lastOpeningBrace == -1 ) { + $currentClosing = ''; + $search = $validOpeningBraces; + } else { + $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd']; + $search = $validOpeningBraces . '|' . $currentClosing; + } + $rule = null; + $i += strcspn( $text, $search, $i ); + if ( $i < strlen( $text ) ) { + if ( $text[$i] == '|' ) { + $found = 'pipe'; + } elseif ( $text[$i] == $currentClosing ) { + $found = 'close'; + } elseif ( isset( $callbacks[$text[$i]] ) ) { + $found = 'open'; + $rule = $callbacks[$text[$i]]; + } else { + # Some versions of PHP have a strcspn which stops on null characters + # Ignore and continue + ++$i; + continue; + } + } else { + # All done + break; + } + + if ( $found == 'open' ) { + # found opening brace, let's add it to parentheses stack + $piece = array('brace' => $text[$i], + 'braceEnd' => $rule['end'], + 'title' => '', + 'parts' => null); + + # count opening brace characters + $piece['count'] = strspn( $text, $piece['brace'], $i ); + $piece['startAt'] = $piece['partStart'] = $i + $piece['count']; + $i += $piece['count']; + + # we need to add to stack only if opening brace count is enough for one of the rules + if ( $piece['count'] >= $rule['min'] ) { + $lastOpeningBrace ++; + $openingBraceStack[$lastOpeningBrace] = $piece; + } + } elseif ( $found == 'close' ) { + # lets check if it is enough characters for closing brace + $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; + $count = strspn( $text, $text[$i], $i, $maxCount ); + + # check for maximum matching characters (if there are 5 closing + # characters, we will probably need only 3 - depending on the rules) + $matchingCount = 0; + $matchingCallback = null; + $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; + if ( $count > $cbType['max'] ) { + # The specified maximum exists in the callback array, unless the caller + # has made an error + $matchingCount = $cbType['max']; + } else { + # Count is less than the maximum + # Skip any gaps in the callback array to find the true largest match + # Need to use array_key_exists not isset because the callback can be null + $matchingCount = $count; + while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) { + --$matchingCount; + } + } + + if ($matchingCount <= 0) { + $i += $count; + continue; + } + $matchingCallback = $cbType['cb'][$matchingCount]; + + # let's set a title or last part (if '|' was found) + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } + + $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; + $pieceEnd = $i + $matchingCount; + + if( is_callable( $matchingCallback ) ) { + $cbArgs = array ( + 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), + 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), + 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], + 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), + ); + # finally we can call a user callback and replace piece of text + $replaceWith = call_user_func( $matchingCallback, $cbArgs ); + $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); + $i = $pieceStart + strlen($replaceWith); + } else { + # null value for callback means that parentheses should be parsed, but not replaced + $i += $matchingCount; + } + + # reset last opening parentheses, but keep it in case there are unused characters + $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], + 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], + 'count' => $openingBraceStack[$lastOpeningBrace]['count'], + 'title' => '', + 'parts' => null, + 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); + $openingBraceStack[$lastOpeningBrace--] = null; + + if ($matchingCount < $piece['count']) { + $piece['count'] -= $matchingCount; + $piece['startAt'] -= $matchingCount; + $piece['partStart'] = $piece['startAt']; + # do we still qualify for any callback with remaining count? + $currentCbList = $callbacks[$piece['brace']]['cb']; + while ( $piece['count'] ) { + if ( array_key_exists( $piece['count'], $currentCbList ) ) { + $lastOpeningBrace++; + $openingBraceStack[$lastOpeningBrace] = $piece; + break; + } + --$piece['count']; + } + } + } elseif ( $found == 'pipe' ) { + # lets set a title if it is a first separator, or next part otherwise + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + $openingBraceStack[$lastOpeningBrace]['parts'] = array(); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } + $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Replace magic variables, templates, and template arguments + * with the appropriate text. Templates are substituted recursively, + * taking care to avoid infinite loops. + * + * Note that the substitution depends on value of $mOutputType: + * self::OT_WIKI: only {{subst:}} templates + * self::OT_MSG: only magic variables + * self::OT_HTML: all templates and magic variables + * + * @param string $tex The text to transform + * @param array $args Key-value pairs representing template parameters to substitute + * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion + * @private + */ + function replaceVariables( $text, $args = array(), $argsOnly = false ) { + # Prevent too big inclusions + if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { + return $text; + } + + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; + wfProfileIn( $fname ); + + # This function is called recursively. To keep track of arguments we need a stack: + array_push( $this->mArgStack, $args ); + + $braceCallbacks = array(); + if ( !$argsOnly ) { + $braceCallbacks[2] = array( &$this, 'braceSubstitution' ); + } + if ( $this->mOutputType != self::OT_MSG ) { + $braceCallbacks[3] = array( &$this, 'argSubstitution' ); + } + if ( $braceCallbacks ) { + $callbacks = array( + '{' => array( + 'end' => '}', + 'cb' => $braceCallbacks, + 'min' => $argsOnly ? 3 : 2, + 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, + ), + '[' => array( + 'end' => ']', + 'cb' => array(2=>null), + 'min' => 2, + 'max' => 2, + ) + ); + $text = $this->replace_callback ($text, $callbacks); + + array_pop( $this->mArgStack ); + } + wfProfileOut( $fname ); + return $text; + } + + /** + * Replace magic variables + * @private + */ + function variableSubstitution( $matches ) { + global $wgContLang; + $fname = 'Parser::variableSubstitution'; + $varname = $wgContLang->lc($matches[1]); + wfProfileIn( $fname ); + $skip = false; + if ( $this->mOutputType == self::OT_WIKI ) { + # Do only magic variables prefixed by SUBST + $mwSubst =& MagicWord::get( 'subst' ); + if (!$mwSubst->matchStartAndRemove( $varname )) + $skip = true; + # Note that if we don't substitute the variable below, + # we don't remove the {{subst:}} magic word, in case + # it is a template rather than a magic variable. + } + if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) { + $id = $this->mVariables[$varname]; + # Now check if we did really match, case sensitive or not + $mw =& MagicWord::get( $id ); + if ($mw->match($matches[1])) { + $text = $this->getVariableValue( $id ); + if (MagicWord::getCacheTTL($id)>-1) + $this->mOutput->mContainsOldMagic = true; + } else { + $text = $matches[0]; + } + } else { + $text = $matches[0]; + } + wfProfileOut( $fname ); + return $text; + } + + + /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. + static function createAssocArgs( $args ) { + $assocArgs = array(); + $index = 1; + foreach( $args as $arg ) { + $eqpos = strpos( $arg, '=' ); + if ( $eqpos === false ) { + $assocArgs[$index++] = $arg; + } else { + $name = trim( substr( $arg, 0, $eqpos ) ); + $value = trim( substr( $arg, $eqpos+1 ) ); + if ( $value === false ) { + $value = ''; + } + if ( $name !== false ) { + $assocArgs[$name] = $value; + } + } + } + + return $assocArgs; + } + + /** + * Return the text of a template, after recursively + * replacing any variables or templates within the template. + * + * @param array $piece The parts of the template + * $piece['text']: matched text + * $piece['title']: the title, i.e. the part before the | + * $piece['parts']: the parameter array + * @return string the text of the template + * @private + */ + function braceSubstitution( $piece ) { + global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; + wfProfileIn( $fname ); + wfProfileIn( __METHOD__.'-setup' ); + + # Flags + $found = false; # $text has been filled + $nowiki = false; # wiki markup in $text should be escaped + $noparse = false; # Unsafe HTML tags should not be stripped, etc. + $noargs = false; # Don't replace triple-brace arguments in $text + $replaceHeadings = false; # Make the edit section links go to the template not the article + $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. + $isHTML = false; # $text is HTML, armour it against wikitext transformation + $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered + + # Title object, where $text came from + $title = NULL; + + $linestart = ''; + + + # $part1 is the bit before the first |, and must contain only title characters + # $args is a list of arguments, starting from index 0, not including $part1 + + $titleText = $part1 = $piece['title']; + # If the third subpattern matched anything, it will start with | + + if (null == $piece['parts']) { + $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title'])); + if ($replaceWith != $piece['text']) { + $text = $replaceWith; + $found = true; + $noparse = true; + $noargs = true; + } + } + + $args = (null == $piece['parts']) ? array() : $piece['parts']; + wfProfileOut( __METHOD__.'-setup' ); + + # SUBST + wfProfileIn( __METHOD__.'-modifiers' ); + if ( !$found ) { + $mwSubst =& MagicWord::get( 'subst' ); + if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) { + # One of two possibilities is true: + # 1) Found SUBST but not in the PST phase + # 2) Didn't find SUBST and in the PST phase + # In either case, return without further processing + $text = $piece['text']; + $found = true; + $noparse = true; + $noargs = true; + } + } + + # MSG, MSGNW and RAW + if ( !$found ) { + # Check for MSGNW: + $mwMsgnw =& MagicWord::get( 'msgnw' ); + if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { + $nowiki = true; + } else { + # Remove obsolete MSG: + $mwMsg =& MagicWord::get( 'msg' ); + $mwMsg->matchStartAndRemove( $part1 ); + } + + # Check for RAW: + $mwRaw =& MagicWord::get( 'raw' ); + if ( $mwRaw->matchStartAndRemove( $part1 ) ) { + $forceRawInterwiki = true; + } + } + wfProfileOut( __METHOD__.'-modifiers' ); + + //save path level before recursing into functions & templates. + $lastPathLevel = $this->mTemplatePath; + + # Parser functions + if ( !$found ) { + wfProfileIn( __METHOD__ . '-pfunc' ); + + $colonPos = strpos( $part1, ':' ); + if ( $colonPos !== false ) { + # Case sensitive functions + $function = substr( $part1, 0, $colonPos ); + if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { + $function = $this->mFunctionSynonyms[1][$function]; + } else { + # Case insensitive functions + $function = strtolower( $function ); + if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { + $function = $this->mFunctionSynonyms[0][$function]; + } else { + $function = false; + } + } + if ( $function ) { + $funcArgs = array_map( 'trim', $args ); + $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs ); + $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs ); + $found = true; + + // The text is usually already parsed, doesn't need triple-brace tags expanded, etc. + //$noargs = true; + //$noparse = true; + + if ( is_array( $result ) ) { + if ( isset( $result[0] ) ) { + $text = $linestart . $result[0]; + unset( $result[0] ); + } + + // Extract flags into the local scope + // This allows callers to set flags such as nowiki, noparse, found, etc. + extract( $result ); + } else { + $text = $linestart . $result; + } + } + } + wfProfileOut( __METHOD__ . '-pfunc' ); + } + + # Template table test + + # Did we encounter this template already? If yes, it is in the cache + # and we need to check for loops. + if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) { + $found = true; + + # Infinite loop test + if ( isset( $this->mTemplatePath[$part1] ) ) { + $noparse = true; + $noargs = true; + $found = true; + $text = $linestart . + "[[$part1]]<!-- WARNING: template loop detected -->"; + wfDebug( __METHOD__.": template loop broken at '$part1'\n" ); + } else { + # set $text to cached message. + $text = $linestart . $this->mTemplates[$piece['title']]; + #treat title for cached page the same as others + $ns = NS_TEMPLATE; + $subpage = ''; + $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); + if ($subpage !== '') { + $ns = $this->mTitle->getNamespace(); + } + $title = Title::newFromText( $part1, $ns ); + //used by include size checking + $titleText = $title->getPrefixedText(); + //used by edit section links + $replaceHeadings = true; + + } + } + + # Load from database + if ( !$found ) { + wfProfileIn( __METHOD__ . '-loadtpl' ); + $ns = NS_TEMPLATE; + # declaring $subpage directly in the function call + # does not work correctly with references and breaks + # {{/subpage}}-style inclusions + $subpage = ''; + $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); + if ($subpage !== '') { + $ns = $this->mTitle->getNamespace(); + } + $title = Title::newFromText( $part1, $ns ); + + + if ( !is_null( $title ) ) { + $titleText = $title->getPrefixedText(); + # Check for language variants if the template is not found + if($wgContLang->hasVariants() && $title->getArticleID() == 0){ + $wgContLang->findVariantLink($part1, $title); + } + + if ( !$title->isExternal() ) { + if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { + $text = SpecialPage::capturePath( $title ); + if ( is_string( $text ) ) { + $found = true; + $noparse = true; + $noargs = true; + $isHTML = true; + $this->disableCache(); + } + } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { + $found = false; //access denied + wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); + } else { + list($articleContent,$title) = $this->fetchTemplateAndtitle( $title ); + if ( $articleContent !== false ) { + $found = true; + $text = $articleContent; + $replaceHeadings = true; + } + } + + # If the title is valid but undisplayable, make a link to it + if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = "[[:$titleText]]"; + $found = true; + } + } elseif ( $title->isTrans() ) { + // Interwiki transclusion + if ( $this->ot['html'] && !$forceRawInterwiki ) { + $text = $this->interwikiTransclude( $title, 'render' ); + $isHTML = true; + $noparse = true; + } else { + $text = $this->interwikiTransclude( $title, 'raw' ); + $replaceHeadings = true; + } + $found = true; + } + + # Template cache array insertion + # Use the original $piece['title'] not the mangled $part1, so that + # modifiers such as RAW: produce separate cache entries + if( $found ) { + if( $isHTML ) { + // A special page; don't store it in the template cache. + } else { + $this->mTemplates[$piece['title']] = $text; + } + $text = $linestart . $text; + } + } + wfProfileOut( __METHOD__ . '-loadtpl' ); + } + + if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) { + # Error, oversize inclusion + $text = $linestart . + "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->"; + $noparse = true; + $noargs = true; + } + + # Recursive parsing, escaping and link table handling + # Only for HTML output + if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = wfEscapeWikiText( $text ); + } elseif ( !$this->ot['msg'] && $found ) { + if ( $noargs ) { + $assocArgs = array(); + } else { + # Clean up argument array + $assocArgs = self::createAssocArgs($args); + # Add a new element to the templace recursion path + $this->mTemplatePath[$part1] = 1; + } + + if ( !$noparse ) { + # If there are any <onlyinclude> tags, only include them + if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) { + $replacer = new OnlyIncludeReplacer; + StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>', + array( &$replacer, 'replace' ), $text ); + $text = $replacer->output; + } + # Remove <noinclude> sections and <includeonly> tags + $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text ); + $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) ); + + if( $this->ot['html'] || $this->ot['pre'] ) { + # Strip <nowiki>, <pre>, etc. + $text = $this->strip( $text, $this->mStripState ); + if ( $this->ot['html'] ) { + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs ); + } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) { + $text = Sanitizer::removeHTMLcomments( $text ); + } + } + $text = $this->replaceVariables( $text, $assocArgs ); + + # If the template begins with a table or block-level + # element, it should be treated as beginning a new line. + if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{ + $text = "\n" . $text; + } + } elseif ( !$noargs ) { + # $noparse and !$noargs + # Just replace the arguments, not any double-brace items + # This is used for rendered interwiki transclusion + $text = $this->replaceVariables( $text, $assocArgs, true ); + } + } + # Prune lower levels off the recursion check path + $this->mTemplatePath = $lastPathLevel; + + if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { + # Error, oversize inclusion + $text = $linestart . + "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->"; + $noparse = true; + $noargs = true; + } + + if ( !$found ) { + wfProfileOut( $fname ); + return $piece['text']; + } else { + wfProfileIn( __METHOD__ . '-placeholders' ); + if ( $isHTML ) { + # Replace raw HTML by a placeholder + # Add a blank line preceding, to prevent it from mucking up + # immediately preceding headings + $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState ); + } else { + # replace ==section headers== + # XXX this needs to go away once we have a better parser. + if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) { + if( !is_null( $title ) ) + $encodedname = base64_encode($title->getPrefixedDBkey()); + else + $encodedname = base64_encode(""); + $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1, + PREG_SPLIT_DELIM_CAPTURE); + $text = ''; + $nsec = $headingOffset; + + for( $i = 0; $i < count($m); $i += 2 ) { + $text .= $m[$i]; + if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue; + $hl = $m[$i + 1]; + if( strstr($hl, "<!--MWTEMPLATESECTION") ) { + $text .= $hl; + continue; + } + $m2 = array(); + preg_match('/^(={1,6})(.*?)(={1,6}\s*?)$/m', $hl, $m2); + $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION=" + . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3]; + + $nsec++; + } + } + } + wfProfileOut( __METHOD__ . '-placeholders' ); + } + + # Prune lower levels off the recursion check path + $this->mTemplatePath = $lastPathLevel; + + if ( !$found ) { + wfProfileOut( $fname ); + return $piece['text']; + } else { + wfProfileOut( $fname ); + return $text; + } + } + + /** + * Fetch the unparsed text of a template and register a reference to it. + */ + function fetchTemplateAndTitle( $title ) { + $templateCb = $this->mOptions->getTemplateCallback(); + $stuff = call_user_func( $templateCb, $title ); + $text = $stuff['text']; + $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; + if ( isset( $stuff['deps'] ) ) { + foreach ( $stuff['deps'] as $dep ) { + $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); + } + } + return array($text,$finalTitle); + } + + function fetchTemplate( $title ) { + $rv = $this->fetchTemplateAndtitle($title); + return $rv[0]; + } + + /** + * Static function to get a template + * Can be overridden via ParserOptions::setTemplateCallback(). + * + * Returns an associative array: + * text The unparsed template text + * finalTitle (Optional) The title after following redirects + * deps (Optional) An array of associative array dependencies: + * title: The dependency title, to be registered in templatelinks + * page_id: The page_id of the title + * rev_id: The revision ID loaded + */ + static function statelessFetchTemplate( $title ) { + $text = $skip = false; + $finalTitle = $title; + $deps = array(); + + // Loop to fetch the article, with up to 1 redirect + for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { + # Give extensions a chance to select the revision instead + $id = false; // Assume current + wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( false, &$title, &$skip, &$id ) ); + + if( $skip ) { + $text = false; + $deps[] = array( + 'title' => $title, + 'page_id' => $title->getArticleID(), + 'rev_id' => null ); + break; + } + $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title ); + $rev_id = $rev ? $rev->getId() : 0; + + $deps[] = array( + 'title' => $title, + 'page_id' => $title->getArticleID(), + 'rev_id' => $rev_id ); + + if( $rev ) { + $text = $rev->getText(); + } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { + global $wgLang; + $message = $wgLang->lcfirst( $title->getText() ); + $text = wfMsgForContentNoTrans( $message ); + if( wfEmptyMsg( $message, $text ) ) { + $text = false; + break; + } + } else { + break; + } + if ( $text === false ) { + break; + } + // Redirect? + $finalTitle = $title; + $title = Title::newFromRedirect( $text ); + } + return array( + 'text' => $text, + 'finalTitle' => $finalTitle, + 'deps' => $deps ); + } + + /** + * Transclude an interwiki link. + */ + function interwikiTransclude( $title, $action ) { + global $wgEnableScaryTranscluding; + + if (!$wgEnableScaryTranscluding) + return wfMsg('scarytranscludedisabled'); + + $url = $title->getFullUrl( "action=$action" ); + + if (strlen($url) > 255) + return wfMsg('scarytranscludetoolong'); + return $this->fetchScaryTemplateMaybeFromCache($url); + } + + function fetchScaryTemplateMaybeFromCache($url) { + global $wgTranscludeCacheExpiry; + $dbr = wfGetDB(DB_SLAVE); + $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'), + array('tc_url' => $url)); + if ($obj) { + $time = $obj->tc_time; + $text = $obj->tc_contents; + if ($time && time() < $time + $wgTranscludeCacheExpiry ) { + return $text; + } + } + + $text = Http::get($url); + if (!$text) + return wfMsg('scarytranscludefailed', $url); + + $dbw = wfGetDB(DB_MASTER); + $dbw->replace('transcache', array('tc_url'), array( + 'tc_url' => $url, + 'tc_time' => time(), + 'tc_contents' => $text)); + return $text; + } + + + /** + * Triple brace replacement -- used for template arguments + * @private + */ + function argSubstitution( $matches ) { + $arg = trim( $matches['title'] ); + $text = $matches['text']; + $inputArgs = end( $this->mArgStack ); + + if ( array_key_exists( $arg, $inputArgs ) ) { + $text = $inputArgs[$arg]; + } else if (($this->mOutputType == self::OT_HTML || $this->mOutputType == self::OT_PREPROCESS ) && + null != $matches['parts'] && count($matches['parts']) > 0) { + $text = $matches['parts'][0]; + } + if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { + $text = $matches['text'] . + '<!-- WARNING: argument omitted, expansion size too large -->'; + } + + return $text; + } + + /** + * Increment an include size counter + * + * @param string $type The type of expansion + * @param integer $size The size of the text + * @return boolean False if this inclusion would take it over the maximum, true otherwise + */ + function incrementIncludeSize( $type, $size ) { + if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { + return false; + } else { + $this->mIncludeSizes[$type] += $size; + return true; + } + } + + /** + * Detect __NOGALLERY__ magic word and set a placeholder + */ + function stripNoGallery( &$text ) { + # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML, + # do not add TOC + $mw = MagicWord::get( 'nogallery' ); + $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ; + } + + /** + * Find the first __TOC__ magic word and set a <!--MWTOC--> + * placeholder that will then be replaced by the real TOC in + * ->formatHeadings, this works because at this points real + * comments will have already been discarded by the sanitizer. + * + * Any additional __TOC__ magic words left over will be discarded + * as there can only be one TOC on the page. + */ + function stripToc( $text ) { + # if the string __NOTOC__ (not case-sensitive) occurs in the HTML, + # do not add TOC + $mw = MagicWord::get( 'notoc' ); + if( $mw->matchAndRemove( $text ) ) { + $this->mShowToc = false; + } + + $mw = MagicWord::get( 'toc' ); + if( $mw->match( $text ) ) { + $this->mShowToc = true; + $this->mForceTocPosition = true; + + // Set a placeholder. At the end we'll fill it in with the TOC. + $text = $mw->replace( '<!--MWTOC-->', $text, 1 ); + + // Only keep the first one. + $text = $mw->replace( '', $text ); + } + return $text; + } + + /** + * This function accomplishes several tasks: + * 1) Auto-number headings if that option is enabled + * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page + * 3) Add a Table of contents on the top for users who have enabled the option + * 4) Auto-anchor headings + * + * It loops through all headlines, collects the necessary data, then splits up the + * string and re-inserts the newly formatted headlines. + * + * @param string $text + * @param boolean $isMain + * @private + */ + function formatHeadings( $text, $isMain=true ) { + global $wgMaxTocLevel, $wgContLang; + + $doNumberHeadings = $this->mOptions->getNumberHeadings(); + if( !$this->mTitle->quickUserCan( 'edit' ) ) { + $showEditLink = 0; + } else { + $showEditLink = $this->mOptions->getEditSection(); + } + + # Inhibit editsection links if requested in the page + $esw =& MagicWord::get( 'noeditsection' ); + if( $esw->matchAndRemove( $text ) ) { + $showEditLink = 0; + } + + # Get all headlines for numbering them and adding funky stuff like [edit] + # links - this is for later, but we need the number of headlines right now + $matches = array(); + $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches ); + + # if there are fewer than 4 headlines in the article, do not show TOC + # unless it's been explicitly enabled. + $enoughToc = $this->mShowToc && + (($numMatches >= 4) || $this->mForceTocPosition); + + # Allow user to stipulate that a page should have a "new section" + # link added via __NEWSECTIONLINK__ + $mw =& MagicWord::get( 'newsectionlink' ); + if( $mw->matchAndRemove( $text ) ) + $this->mOutput->setNewSection( true ); + + # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, + # override above conditions and always show TOC above first header + $mw =& MagicWord::get( 'forcetoc' ); + if ($mw->matchAndRemove( $text ) ) { + $this->mShowToc = true; + $enoughToc = true; + } + + # We need this to perform operations on the HTML + $sk = $this->mOptions->getSkin(); + + # headline counter + $headlineCount = 0; + $sectionCount = 0; # headlineCount excluding template sections + $numVisible = 0; + + # Ugh .. the TOC should have neat indentation levels which can be + # passed to the skin functions. These are determined here + $toc = ''; + $full = ''; + $head = array(); + $sublevelCount = array(); + $levelCount = array(); + $toclevel = 0; + $level = 0; + $prevlevel = 0; + $toclevel = 0; + $prevtoclevel = 0; + $tocraw = array(); + + foreach( $matches[3] as $headline ) { + $istemplate = 0; + $templatetitle = ''; + $templatesection = 0; + $numbering = ''; + $mat = array(); + if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) { + $istemplate = 1; + $templatetitle = base64_decode($mat[1]); + $templatesection = 1 + (int)base64_decode($mat[2]); + $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline); + } + + if( $toclevel ) { + $prevlevel = $level; + $prevtoclevel = $toclevel; + } + $level = $matches[1][$headlineCount]; + + if( $doNumberHeadings || $enoughToc ) { + + if ( $level > $prevlevel ) { + # Increase TOC level + $toclevel++; + $sublevelCount[$toclevel] = 0; + if( $toclevel<$wgMaxTocLevel ) { + $prevtoclevel = $toclevel; + $toc .= $sk->tocIndent(); + $numVisible++; + } + } + elseif ( $level < $prevlevel && $toclevel > 1 ) { + # Decrease TOC level, find level to jump to + + if ( $toclevel == 2 && $level <= $levelCount[1] ) { + # Can only go down to level 1 + $toclevel = 1; + } else { + for ($i = $toclevel; $i > 0; $i--) { + if ( $levelCount[$i] == $level ) { + # Found last matching level + $toclevel = $i; + break; + } + elseif ( $levelCount[$i] < $level ) { + # Found first matching level below current level + $toclevel = $i + 1; + break; + } + } + } + if( $toclevel<$wgMaxTocLevel ) { + if($prevtoclevel < $wgMaxTocLevel) { + # Unindent only if the previous toc level was shown :p + $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); + } else { + $toc .= $sk->tocLineEnd(); + } + } + } + else { + # No change in level, end TOC line + if( $toclevel<$wgMaxTocLevel ) { + $toc .= $sk->tocLineEnd(); + } + } + + $levelCount[$toclevel] = $level; + + # count number of headlines for each level + @$sublevelCount[$toclevel]++; + $dot = 0; + for( $i = 1; $i <= $toclevel; $i++ ) { + if( !empty( $sublevelCount[$i] ) ) { + if( $dot ) { + $numbering .= '.'; + } + $numbering .= $wgContLang->formatNum( $sublevelCount[$i] ); + $dot = 1; + } + } + } + + # The canonized header is a version of the header text safe to use for links + # Avoid insertion of weird stuff like <math> by expanding the relevant sections + $canonized_headline = $this->mStripState->unstripBoth( $headline ); + + # Remove link placeholders by the link text. + # <!--LINK number--> + # turns into + # link text with suffix + $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e', + "\$this->mLinkHolders['texts'][\$1]", + $canonized_headline ); + $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e', + "\$this->mInterwikiLinkHolders['texts'][\$1]", + $canonized_headline ); + + # Strip out HTML (other than plain <sup> and <sub>: bug 8393) + $tocline = preg_replace( + array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ), + array( '', '<$1>'), + $canonized_headline + ); + $tocline = trim( $tocline ); + + # For the anchor, strip out HTML-y stuff period + $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline ); + $canonized_headline = trim( $canonized_headline ); + + # Save headline for section edit hint before it's escaped + $headline_hint = $canonized_headline; + $canonized_headline = Sanitizer::escapeId( $canonized_headline ); + $refers[$headlineCount] = $canonized_headline; + + # count how many in assoc. array so we can track dupes in anchors + isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1; + $refcount[$headlineCount]=$refers[$canonized_headline]; + + # Don't number the heading if it is the only one (looks silly) + if( $doNumberHeadings && count( $matches[3] ) > 1) { + # the two are different if the line contains a link + $headline=$numbering . ' ' . $headline; + } + + # Create the anchor for linking from the TOC to the section + $anchor = $canonized_headline; + if($refcount[$headlineCount] > 1 ) { + $anchor .= '_' . $refcount[$headlineCount]; + } + if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { + $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); + $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering ); + } + # give headline the correct <h#> tag + if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) { + if( $istemplate ) + $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection); + else + $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); + } else { + $editlink = ''; + } + $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); + + $headlineCount++; + if( !$istemplate ) + $sectionCount++; + } + + $this->mOutput->setSections( $tocraw ); + + # Never ever show TOC if no headers + if( $numVisible < 1 ) { + $enoughToc = false; + } + + if( $enoughToc ) { + if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { + $toc .= $sk->tocUnindent( $prevtoclevel - 1 ); + } + $toc = $sk->tocList( $toc ); + } + + # split up and insert constructed headlines + + $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text ); + $i = 0; + + foreach( $blocks as $block ) { + if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) { + # This is the [edit] link that appears for the top block of text when + # section editing is enabled + + # Disabled because it broke block formatting + # For example, a bullet point in the top line + # $full .= $sk->editSectionLink(0); + } + $full .= $block; + if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) { + # Top anchor now in skin + $full = $full.$toc; + } + + if( !empty( $head[$i] ) ) { + $full .= $head[$i]; + } + $i++; + } + if( $this->mForceTocPosition ) { + return str_replace( '<!--MWTOC-->', $toc, $full ); + } else { + return $full; + } + } + + /** + * Transform wiki markup when saving a page by doing \r\n -> \n + * conversion, substitting signatures, {{subst:}} templates, etc. + * + * @param string $text the text to transform + * @param Title &$title the Title object for the current article + * @param User &$user the User object describing the current user + * @param ParserOptions $options parsing options + * @param bool $clearState whether to clear the parser state first + * @return string the altered wiki markup + * @public + */ + function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) { + $this->mOptions = $options; + $this->mTitle =& $title; + $this->setOutputType( self::OT_WIKI ); + + if ( $clearState ) { + $this->clearState(); + } + + $stripState = new StripState; + $pairs = array( + "\r\n" => "\n", + ); + $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); + $text = $this->strip( $text, $stripState, true, array( 'gallery' ) ); + $text = $this->pstPass2( $text, $stripState, $user ); + $text = $stripState->unstripBoth( $text ); + return $text; + } + + /** + * Pre-save transform helper function + * @private + */ + function pstPass2( $text, &$stripState, $user ) { + global $wgContLang, $wgLocaltimezone; + + /* Note: This is the timestamp saved as hardcoded wikitext to + * the database, we use $wgContLang here in order to give + * everyone the same signature and use the default one rather + * than the one selected in each user's preferences. + */ + if ( isset( $wgLocaltimezone ) ) { + $oldtz = getenv( 'TZ' ); + putenv( 'TZ='.$wgLocaltimezone ); + } + $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) . + ' (' . date( 'T' ) . ')'; + if ( isset( $wgLocaltimezone ) ) { + putenv( 'TZ='.$oldtz ); + } + + # Variable replacement + # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags + $text = $this->replaceVariables( $text ); + + # Strip out <nowiki> etc. added via replaceVariables + $text = $this->strip( $text, $stripState, false, array( 'gallery' ) ); + + # Signatures + $sigText = $this->getUserSig( $user ); + $text = strtr( $text, array( + '~~~~~' => $d, + '~~~~' => "$sigText $d", + '~~~' => $sigText + ) ); + + # Context links: [[|name]] and [[name (context)|]] + # + global $wgLegalTitleChars; + $tc = "[$wgLegalTitleChars]"; + $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! + + $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]] + $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] + $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] + + # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" + $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); + $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); + + $t = $this->mTitle->getText(); + $m = array(); + if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); + } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); + } else { + # if there's no context, don't bother duplicating the title + $text = preg_replace( $p2, '[[\\1]]', $text ); + } + + # Trim trailing whitespace + $text = rtrim( $text ); + + return $text; + } + + /** + * Fetch the user's signature text, if any, and normalize to + * validated, ready-to-insert wikitext. + * + * @param User $user + * @return string + * @private + */ + function getUserSig( &$user ) { + global $wgMaxSigChars; + + $username = $user->getName(); + $nickname = $user->getOption( 'nickname' ); + $nickname = $nickname === '' ? $username : $nickname; + + if( mb_strlen( $nickname ) > $wgMaxSigChars ) { + $nickname = $username; + wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); + } elseif( $user->getBoolOption( 'fancysig' ) !== false ) { + # Sig. might contain markup; validate this + if( $this->validateSig( $nickname ) !== false ) { + # Validated; clean up (if needed) and return it + return $this->cleanSig( $nickname, true ); + } else { + # Failed to validate; fall back to the default + $nickname = $username; + wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" ); + } + } + + // Make sure nickname doesnt get a sig in a sig + $nickname = $this->cleanSigInSig( $nickname ); + + # If we're still here, make it a link to the user page + $userText = wfEscapeWikiText( $username ); + $nickText = wfEscapeWikiText( $nickname ); + if ( $user->isAnon() ) { + return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText ); + } else { + return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText ); + } + } + + /** + * Check that the user's signature contains no bad XML + * + * @param string $text + * @return mixed An expanded string, or false if invalid. + */ + function validateSig( $text ) { + return( wfIsWellFormedXmlFragment( $text ) ? $text : false ); + } + + /** + * Clean up signature text + * + * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig + * 2) Substitute all transclusions + * + * @param string $text + * @param $parsing Whether we're cleaning (preferences save) or parsing + * @return string Signature text + */ + function cleanSig( $text, $parsing = false ) { + global $wgTitle; + $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG ); + + $substWord = MagicWord::get( 'subst' ); + $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); + $substText = '{{' . $substWord->getSynonym( 0 ); + + $text = preg_replace( $substRegex, $substText, $text ); + $text = $this->cleanSigInSig( $text ); + $text = $this->replaceVariables( $text ); + + $this->clearState(); + return $text; + } + + /** + * Strip ~~~, ~~~~ and ~~~~~ out of signatures + * @param string $text + * @return string Signature text with /~{3,5}/ removed + */ + function cleanSigInSig( $text ) { + $text = preg_replace( '/~{3,5}/', '', $text ); + return $text; + } + + /** + * Set up some variables which are usually set up in parse() + * so that an external function can call some class members with confidence + * @public + */ + function startExternalParse( &$title, $options, $outputType, $clearState = true ) { + $this->mTitle =& $title; + $this->mOptions = $options; + $this->setOutputType( $outputType ); + if ( $clearState ) { + $this->clearState(); + } + } + + /** + * Transform a MediaWiki message by replacing magic variables. + * + * @param string $text the text to transform + * @param ParserOptions $options options + * @return string the text with variables substituted + * @public + */ + function transformMsg( $text, $options ) { + global $wgTitle; + static $executing = false; + + $fname = "Parser::transformMsg"; + + # Guard against infinite recursion + if ( $executing ) { + return $text; + } + $executing = true; + + wfProfileIn($fname); + + if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) { + $this->mTitle = $wgTitle; + } else { + $this->mTitle = Title::newFromText('msg'); + } + $this->mOptions = $options; + $this->setOutputType( self::OT_MSG ); + $this->clearState(); + $text = $this->replaceVariables( $text ); + + $executing = false; + wfProfileOut($fname); + return $text; + } + + /** + * Create an HTML-style tag, e.g. <yourtag>special text</yourtag> + * The callback should have the following form: + * function myParserHook( $text, $params, &$parser ) { ... } + * + * Transform and return $text. Use $parser for any required context, e.g. use + * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions + * + * @public + * + * @param mixed $tag The tag to use, e.g. 'hook' for <hook> + * @param mixed $callback The callback function (and object) to use for the tag + * + * @return The old value of the mTagHooks array associated with the hook + */ + function setHook( $tag, $callback ) { + $tag = strtolower( $tag ); + $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; + $this->mTagHooks[$tag] = $callback; + + return $oldVal; + } + + function setTransparentTagHook( $tag, $callback ) { + $tag = strtolower( $tag ); + $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; + $this->mTransparentTagHooks[$tag] = $callback; + + return $oldVal; + } + + /** + * Create a function, e.g. {{sum:1|2|3}} + * The callback function should have the form: + * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } + * + * The callback may either return the text result of the function, or an array with the text + * in element 0, and a number of flags in the other elements. The names of the flags are + * specified in the keys. Valid flags are: + * found The text returned is valid, stop processing the template. This + * is on by default. + * nowiki Wiki markup in the return value should be escaped + * noparse Unsafe HTML tags should not be stripped, etc. + * noargs Don't replace triple-brace arguments in the return value + * isHTML The returned text is HTML, armour it against wikitext transformation + * + * @public + * + * @param string $id The magic word ID + * @param mixed $callback The callback function (and object) to use + * @param integer $flags a combination of the following flags: + * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} + * + * @return The old callback function for this name, if any + */ + function setFunctionHook( $id, $callback, $flags = 0 ) { + $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null; + $this->mFunctionHooks[$id] = $callback; + + # Add to function cache + $mw = MagicWord::get( $id ); + if( !$mw ) + throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' ); + + $synonyms = $mw->getSynonyms(); + $sensitive = intval( $mw->isCaseSensitive() ); + + foreach ( $synonyms as $syn ) { + # Case + if ( !$sensitive ) { + $syn = strtolower( $syn ); + } + # Add leading hash + if ( !( $flags & SFH_NO_HASH ) ) { + $syn = '#' . $syn; + } + # Remove trailing colon + if ( substr( $syn, -1, 1 ) == ':' ) { + $syn = substr( $syn, 0, -1 ); + } + $this->mFunctionSynonyms[$sensitive][$syn] = $id; + } + return $oldVal; + } + + /** + * Get all registered function hook identifiers + * + * @return array + */ + function getFunctionHooks() { + return array_keys( $this->mFunctionHooks ); + } + + /** + * Replace <!--LINK--> link placeholders with actual links, in the buffer + * Placeholders created in Skin::makeLinkObj() + * Returns an array of links found, indexed by PDBK: + * 0 - broken + * 1 - normal link + * 2 - stub + * $options is a bit field, RLH_FOR_UPDATE to select for update + */ + function replaceLinkHolders( &$text, $options = 0 ) { + global $wgUser; + global $wgContLang; + + $fname = 'Parser::replaceLinkHolders'; + wfProfileIn( $fname ); + + $pdbks = array(); + $colours = array(); + $sk = $this->mOptions->getSkin(); + $linkCache =& LinkCache::singleton(); + + if ( !empty( $this->mLinkHolders['namespaces'] ) ) { + wfProfileIn( $fname.'-check' ); + $dbr = wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $threshold = $wgUser->getOption('stubthreshold'); + + # Sort by namespace + asort( $this->mLinkHolders['namespaces'] ); + + # Generate query + $query = false; + $current = null; + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { + # Make title object + $title = $this->mLinkHolders['titles'][$key]; + + # Skip invalid entries. + # Result will be ugly, but prevents crash. + if ( is_null( $title ) ) { + continue; + } + $pdbk = $pdbks[$key] = $title->getPrefixedDBkey(); + + # Check if it's a static known link, e.g. interwiki + if ( $title->isAlwaysKnown() ) { + $colours[$pdbk] = 1; + } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { + $colours[$pdbk] = 1; + $this->mOutput->addLink( $title, $id ); + } elseif ( $linkCache->isBadLink( $pdbk ) ) { + $colours[$pdbk] = 0; + } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) { + $colours[$pdbk] = 0; + } else { + # Not in the link cache, add it to the query + if ( !isset( $current ) ) { + $current = $ns; + $query = "SELECT page_id, page_namespace, page_title"; + if ( $threshold > 0 ) { + $query .= ', page_len, page_is_redirect'; + } + $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; + } elseif ( $current != $ns ) { + $current = $ns; + $query .= ")) OR (page_namespace=$ns AND page_title IN("; + } else { + $query .= ', '; + } + + $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] ); + } + } + if ( $query ) { + $query .= '))'; + if ( $options & RLH_FOR_UPDATE ) { + $query .= ' FOR UPDATE'; + } + + $res = $dbr->query( $query, $fname ); + + # Fetch data and form into an associative array + # non-existent = broken + # 1 = known + # 2 = stub + while ( $s = $dbr->fetchObject($res) ) { + $title = Title::makeTitle( $s->page_namespace, $s->page_title ); + $pdbk = $title->getPrefixedDBkey(); + $linkCache->addGoodLinkObj( $s->page_id, $title ); + $this->mOutput->addLink( $title, $s->page_id ); + + $colours[$pdbk] = ( $threshold == 0 || ( + $s->page_len >= $threshold || # always true if $threshold <= 0 + $s->page_is_redirect || + !Namespace::isContent( $s->page_namespace ) ) + ? 1 : 2 ); + } + } + wfProfileOut( $fname.'-check' ); + + # Do a second query for different language variants of links and categories + if($wgContLang->hasVariants()){ + $linkBatch = new LinkBatch(); + $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) + $categoryMap = array(); // maps $category_variant => $category (dbkeys) + $varCategories = array(); // category replacements oldDBkey => newDBkey + + $categories = $this->mOutput->getCategoryLinks(); + + // Add variants of links to link batch + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) + continue; + + $pdbk = $title->getPrefixedDBkey(); + $titleText = $title->getText(); + + // generate all variants of the link title text + $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); + + // if link was not found (in first query), add all variants to query + if ( !isset($colours[$pdbk]) ){ + foreach($allTextVariants as $textVariant){ + if($textVariant != $titleText){ + $variantTitle = Title::makeTitle( $ns, $textVariant ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + } + } + } + } + + // process categories, check if a category exists in some variant + foreach( $categories as $category ){ + $variants = $wgContLang->convertLinkToAllVariants($category); + foreach($variants as $variant){ + if($variant != $category){ + $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $categoryMap[$variant] = $category; + } + } + } + + + if(!$linkBatch->isEmpty()){ + // construct query + $titleClause = $linkBatch->constructSet('page', $dbr); + + $variantQuery = "SELECT page_id, page_namespace, page_title"; + if ( $threshold > 0 ) { + $variantQuery .= ', page_len, page_is_redirect'; + } + + $variantQuery .= " FROM $page WHERE $titleClause"; + if ( $options & RLH_FOR_UPDATE ) { + $variantQuery .= ' FOR UPDATE'; + } + + $varRes = $dbr->query( $variantQuery, $fname ); + + // for each found variants, figure out link holders and replace + while ( $s = $dbr->fetchObject($varRes) ) { + + $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); + $varPdbk = $variantTitle->getPrefixedDBkey(); + $vardbk = $variantTitle->getDBkey(); + + $holderKeys = array(); + if(isset($variantMap[$varPdbk])){ + $holderKeys = $variantMap[$varPdbk]; + $linkCache->addGoodLinkObj( $s->page_id, $variantTitle ); + $this->mOutput->addLink( $variantTitle, $s->page_id ); + } + + // loop over link holders + foreach($holderKeys as $key){ + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) continue; + + $pdbk = $title->getPrefixedDBkey(); + + if(!isset($colours[$pdbk])){ + // found link in some of the variants, replace the link holder data + $this->mLinkHolders['titles'][$key] = $variantTitle; + $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); + + // set pdbk and colour + $pdbks[$key] = $varPdbk; + if ( $threshold > 0 ) { + $size = $s->page_len; + if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { + $colours[$varPdbk] = 1; + } else { + $colours[$varPdbk] = 2; + } + } + else { + $colours[$varPdbk] = 1; + } + } + } + + // check if the object is a variant of a category + if(isset($categoryMap[$vardbk])){ + $oldkey = $categoryMap[$vardbk]; + if($oldkey != $vardbk) + $varCategories[$oldkey]=$vardbk; + } + } + + // rebuild the categories in original order (if there are replacements) + if(count($varCategories)>0){ + $newCats = array(); + $originalCats = $this->mOutput->getCategories(); + foreach($originalCats as $cat => $sortkey){ + // make the replacement + if( array_key_exists($cat,$varCategories) ) + $newCats[$varCategories[$cat]] = $sortkey; + else $newCats[$cat] = $sortkey; + } + $this->mOutput->setCategoryLinks($newCats); + } + } + } + + # Construct search and replace arrays + wfProfileIn( $fname.'-construct' ); + $replacePairs = array(); + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { + $pdbk = $pdbks[$key]; + $searchkey = "<!--LINK $key-->"; + $title = $this->mLinkHolders['titles'][$key]; + if ( empty( $colours[$pdbk] ) ) { + $linkCache->addBadLinkObj( $title ); + $colours[$pdbk] = 0; + $this->mOutput->addLink( $title, 0 ); + $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, + $this->mLinkHolders['texts'][$key], + $this->mLinkHolders['queries'][$key] ); + } elseif ( $colours[$pdbk] == 1 ) { + $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title, + $this->mLinkHolders['texts'][$key], + $this->mLinkHolders['queries'][$key] ); + } elseif ( $colours[$pdbk] == 2 ) { + $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title, + $this->mLinkHolders['texts'][$key], + $this->mLinkHolders['queries'][$key] ); + } + } + $replacer = new HashtableReplacer( $replacePairs, 1 ); + wfProfileOut( $fname.'-construct' ); + + # Do the thing + wfProfileIn( $fname.'-replace' ); + $text = preg_replace_callback( + '/(<!--LINK .*?-->)/', + $replacer->cb(), + $text); + + wfProfileOut( $fname.'-replace' ); + } + + # Now process interwiki link holders + # This is quite a bit simpler than internal links + if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) { + wfProfileIn( $fname.'-interwiki' ); + # Make interwiki link HTML + $replacePairs = array(); + foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) { + $title = $this->mInterwikiLinkHolders['titles'][$key]; + $replacePairs[$key] = $sk->makeLinkObj( $title, $link ); + } + $replacer = new HashtableReplacer( $replacePairs, 1 ); + + $text = preg_replace_callback( + '/<!--IWLINK (.*?)-->/', + $replacer->cb(), + $text ); + wfProfileOut( $fname.'-interwiki' ); + } + + wfProfileOut( $fname ); + return $colours; + } + + /** + * Replace <!--LINK--> link placeholders with plain text of links + * (not HTML-formatted). + * @param string $text + * @return string + */ + function replaceLinkHoldersText( $text ) { + $fname = 'Parser::replaceLinkHoldersText'; + wfProfileIn( $fname ); + + $text = preg_replace_callback( + '/<!--(LINK|IWLINK) (.*?)-->/', + array( &$this, 'replaceLinkHoldersTextCallback' ), + $text ); + + wfProfileOut( $fname ); + return $text; + } + + /** + * @param array $matches + * @return string + * @private + */ + function replaceLinkHoldersTextCallback( $matches ) { + $type = $matches[1]; + $key = $matches[2]; + if( $type == 'LINK' ) { + if( isset( $this->mLinkHolders['texts'][$key] ) ) { + return $this->mLinkHolders['texts'][$key]; + } + } elseif( $type == 'IWLINK' ) { + if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) { + return $this->mInterwikiLinkHolders['texts'][$key]; + } + } + return $matches[0]; + } + + /** + * Tag hook handler for 'pre'. + */ + function renderPreTag( $text, $attribs ) { + // Backwards-compatibility hack + $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' ); + + $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); + return wfOpenElement( 'pre', $attribs ) . + Xml::escapeTagsOnly( $content ) . + '</pre>'; + } + + /** + * Renders an image gallery from a text with one line per image. + * text labels may be given by using |-style alternative text. E.g. + * Image:one.jpg|The number "1" + * Image:tree.jpg|A tree + * given as text will return the HTML of a gallery with two images, + * labeled 'The number "1"' and + * 'A tree'. + */ + function renderImageGallery( $text, $params ) { + $ig = new ImageGallery(); + $ig->setContextTitle( $this->mTitle ); + $ig->setShowBytes( false ); + $ig->setShowFilename( false ); + $ig->setParser( $this ); + $ig->setHideBadImages(); + $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); + $ig->useSkin( $this->mOptions->getSkin() ); + $ig->mRevisionId = $this->mRevisionId; + + if( isset( $params['caption'] ) ) { + $caption = $params['caption']; + $caption = htmlspecialchars( $caption ); + $caption = $this->replaceInternalLinks( $caption ); + $ig->setCaptionHtml( $caption ); + } + if( isset( $params['perrow'] ) ) { + $ig->setPerRow( $params['perrow'] ); + } + if( isset( $params['widths'] ) ) { + $ig->setWidths( $params['widths'] ); + } + if( isset( $params['heights'] ) ) { + $ig->setHeights( $params['heights'] ); + } + + wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); + + $lines = explode( "\n", $text ); + foreach ( $lines as $line ) { + # match lines like these: + # Image:someimage.jpg|This is some image + $matches = array(); + preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); + # Skip empty lines + if ( count( $matches ) == 0 ) { + continue; + } + $tp = Title::newFromText( $matches[1] ); + $nt =& $tp; + if( is_null( $nt ) ) { + # Bogus title. Ignore these so we don't bomb out later. + continue; + } + if ( isset( $matches[3] ) ) { + $label = $matches[3]; + } else { + $label = ''; + } + + $pout = $this->parse( $label, + $this->mTitle, + $this->mOptions, + false, // Strip whitespace...? + false // Don't clear state! + ); + $html = $pout->getText(); + + $ig->add( $nt, $html ); + + # Only add real images (bug #5586) + if ( $nt->getNamespace() == NS_IMAGE ) { + $this->mOutput->addImage( $nt->getDBkey() ); + } + } + return $ig->toHTML(); + } + + function getImageParams( $handler ) { + if ( $handler ) { + $handlerClass = get_class( $handler ); + } else { + $handlerClass = ''; + } + if ( !isset( $this->mImageParams[$handlerClass] ) ) { + // Initialise static lists + static $internalParamNames = array( + 'horizAlign' => array( 'left', 'right', 'center', 'none' ), + 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', + 'bottom', 'text-bottom' ), + 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', + 'upright', 'border' ), + ); + static $internalParamMap; + if ( !$internalParamMap ) { + $internalParamMap = array(); + foreach ( $internalParamNames as $type => $names ) { + foreach ( $names as $name ) { + $magicName = str_replace( '-', '_', "img_$name" ); + $internalParamMap[$magicName] = array( $type, $name ); + } + } + } + + // Add handler params + $paramMap = $internalParamMap; + if ( $handler ) { + $handlerParamMap = $handler->getParamMap(); + foreach ( $handlerParamMap as $magic => $paramName ) { + $paramMap[$magic] = array( 'handler', $paramName ); + } + } + $this->mImageParams[$handlerClass] = $paramMap; + $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) ); + } + return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ); + } + + /** + * Parse image options text and use it to make an image + */ + function makeImage( $title, $options ) { + # @TODO: let the MediaHandler specify its transform parameters + # + # Check if the options text is of the form "options|alt text" + # Options are: + # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang + # * left no resizing, just left align. label is used for alt= only + # * right same, but right aligned + # * none same, but not aligned + # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox + # * center center the image + # * framed Keep original image size, no magnify-button. + # * frameless like 'thumb' but without a frame. Keeps user preferences for width + # * upright reduce width for upright images, rounded to full __0 px + # * border draw a 1px border around the image + # vertical-align values (no % or length right now): + # * baseline + # * sub + # * super + # * top + # * text-top + # * middle + # * bottom + # * text-bottom + + $parts = array_map( 'trim', explode( '|', $options) ); + $sk = $this->mOptions->getSkin(); + + # Give extensions a chance to select the file revision for us + $skip = $time = false; + wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) ); + + if ( $skip ) { + return $sk->makeLinkObj( $title ); + } + + # Get parameter map + $file = wfFindFile( $title, $time ); + $handler = $file ? $file->getHandler() : false; + + list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); + + # Process the input parameters + $caption = ''; + $params = array( 'frame' => array(), 'handler' => array(), + 'horizAlign' => array(), 'vertAlign' => array() ); + foreach( $parts as $part ) { + list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); + if ( isset( $paramMap[$magicName] ) ) { + list( $type, $paramName ) = $paramMap[$magicName]; + $params[$type][$paramName] = $value; + + // Special case; width and height come in one variable together + if( $type == 'handler' && $paramName == 'width' ) { + $m = array(); + if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) { + $params[$type]['width'] = intval( $m[1] ); + $params[$type]['height'] = intval( $m[2] ); + } else { + $params[$type]['width'] = intval( $value ); + } + } + } else { + $caption = $part; + } + } + + # Process alignment parameters + if ( $params['horizAlign'] ) { + $params['frame']['align'] = key( $params['horizAlign'] ); + } + if ( $params['vertAlign'] ) { + $params['frame']['valign'] = key( $params['vertAlign'] ); + } + + # Validate the handler parameters + if ( $handler ) { + foreach ( $params['handler'] as $name => $value ) { + if ( !$handler->validateParam( $name, $value ) ) { + unset( $params['handler'][$name] ); + } + } + } + + # Strip bad stuff out of the alt text + $alt = $this->replaceLinkHoldersText( $caption ); + + # make sure there are no placeholders in thumbnail attributes + # that are later expanded to html- so expand them now and + # remove the tags + $alt = $this->mStripState->unstripBoth( $alt ); + $alt = Sanitizer::stripAllTags( $alt ); + + $params['frame']['alt'] = $alt; + $params['frame']['caption'] = $caption; + + # Linker does the rest + $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] ); + + # Give the handler a chance to modify the parser object + if ( $handler ) { + $handler->parserTransformHook( $this, $file ); + } + + return $ret; + } + + /** + * Set a flag in the output object indicating that the content is dynamic and + * shouldn't be cached. + */ + function disableCache() { + wfDebug( "Parser output marked as uncacheable.\n" ); + $this->mOutput->mCacheTime = -1; + } + + /**#@+ + * Callback from the Sanitizer for expanding items found in HTML attribute + * values, so they can be safely tested and escaped. + * @param string $text + * @param array $args + * @return string + * @private + */ + function attributeStripCallback( &$text, $args ) { + $text = $this->replaceVariables( $text, $args ); + $text = $this->mStripState->unstripBoth( $text ); + return $text; + } + + /**#@-*/ + + /**#@+ + * Accessor/mutator + */ + function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); } + function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); } + function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); } + /**#@-*/ + + /**#@+ + * Accessor + */ + function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); } + /**#@-*/ + + + /** + * Break wikitext input into sections, and either pull or replace + * some particular section's text. + * + * External callers should use the getSection and replaceSection methods. + * + * @param $text Page wikitext + * @param $section Numbered section. 0 pulls the text before the first + * heading; other numbers will pull the given section + * along with its lower-level subsections. + * @param $mode One of "get" or "replace" + * @param $newtext Replacement text for section data. + * @return string for "get", the extracted section text. + * for "replace", the whole page with the section replaced. + */ + private function extractSections( $text, $section, $mode, $newtext='' ) { + # I.... _hope_ this is right. + # Otherwise, sometimes we don't have things initialized properly. + $this->clearState(); + + # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML + # comments to be stripped as well) + $stripState = new StripState; + + $oldOutputType = $this->mOutputType; + $oldOptions = $this->mOptions; + $this->mOptions = new ParserOptions(); + $this->setOutputType( self::OT_WIKI ); + + $striptext = $this->strip( $text, $stripState, true ); + + $this->setOutputType( $oldOutputType ); + $this->mOptions = $oldOptions; + + # now that we can be sure that no pseudo-sections are in the source, + # split it up by section + $uniq = preg_quote( $this->uniqPrefix(), '/' ); + $comment = "(?:$uniq-!--.*?QINU\x07)"; + $secs = preg_split( + "/ + ( + ^ + (?:$comment|<\/?noinclude>)* # Initial comments will be stripped + (=+) # Should this be limited to 6? + .+? # Section title... + \\2 # Ending = count must match start + (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok + $ + | + <h([1-6])\b.*?> + .*? + <\/h\\3\s*> + ) + /mix", + $striptext, -1, + PREG_SPLIT_DELIM_CAPTURE); + + if( $mode == "get" ) { + if( $section == 0 ) { + // "Section 0" returns the content before any other section. + $rv = $secs[0]; + } else { + //track missing section, will replace if found. + $rv = $newtext; + } + } elseif( $mode == "replace" ) { + if( $section == 0 ) { + $rv = $newtext . "\n\n"; + $remainder = true; + } else { + $rv = $secs[0]; + $remainder = false; + } + } + $count = 0; + $sectionLevel = 0; + for( $index = 1; $index < count( $secs ); ) { + $headerLine = $secs[$index++]; + if( $secs[$index] ) { + // A wiki header + $headerLevel = strlen( $secs[$index++] ); + } else { + // An HTML header + $index++; + $headerLevel = intval( $secs[$index++] ); + } + $content = $secs[$index++]; + + $count++; + if( $mode == "get" ) { + if( $count == $section ) { + $rv = $headerLine . $content; + $sectionLevel = $headerLevel; + } elseif( $count > $section ) { + if( $sectionLevel && $headerLevel > $sectionLevel ) { + $rv .= $headerLine . $content; + } else { + // Broke out to a higher-level section + break; + } + } + } elseif( $mode == "replace" ) { + if( $count < $section ) { + $rv .= $headerLine . $content; + } elseif( $count == $section ) { + $rv .= $newtext . "\n\n"; + $sectionLevel = $headerLevel; + } elseif( $count > $section ) { + if( $headerLevel <= $sectionLevel ) { + // Passed the section's sub-parts. + $remainder = true; + } + if( $remainder ) { + $rv .= $headerLine . $content; + } + } + } + } + if (is_string($rv)) + # reinsert stripped tags + $rv = trim( $stripState->unstripBoth( $rv ) ); + + return $rv; + } + + /** + * This function returns the text of a section, specified by a number ($section). + * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or + * the first section before any such heading (section 0). + * + * If a section contains subsections, these are also returned. + * + * @param $text String: text to look in + * @param $section Integer: section number + * @param $deftext: default to return if section is not found + * @return string text of the requested section + */ + public function getSection( $text, $section, $deftext='' ) { + return $this->extractSections( $text, $section, "get", $deftext ); + } + + public function replaceSection( $oldtext, $section, $text ) { + return $this->extractSections( $oldtext, $section, "replace", $text ); + } + + /** + * Get the timestamp associated with the current revision, adjusted for + * the default server-local timestamp + */ + function getRevisionTimestamp() { + if ( is_null( $this->mRevisionTimestamp ) ) { + wfProfileIn( __METHOD__ ); + global $wgContLang; + $dbr = wfGetDB( DB_SLAVE ); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $this->mRevisionId ), __METHOD__ ); + + // Normalize timestamp to internal MW format for timezone processing. + // This has the added side-effect of replacing a null value with + // the current time, which gives us more sensible behavior for + // previews. + $timestamp = wfTimestamp( TS_MW, $timestamp ); + + // The cryptic '' timezone parameter tells to use the site-default + // timezone offset instead of the user settings. + // + // Since this value will be saved into the parser cache, served + // to other users, and potentially even used inside links and such, + // it needs to be consistent for all visitors. + $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); + + wfProfileOut( __METHOD__ ); + } + return $this->mRevisionTimestamp; + } + + /** + * Mutator for $mDefaultSort + * + * @param $sort New value + */ + public function setDefaultSort( $sort ) { + $this->mDefaultSort = $sort; + } + + /** + * Accessor for $mDefaultSort + * Will use the title/prefixed title if none is set + * + * @return string + */ + public function getDefaultSort() { + if( $this->mDefaultSort !== false ) { + return $this->mDefaultSort; + } else { + return $this->mTitle->getNamespace() == NS_CATEGORY + ? $this->mTitle->getText() + : $this->mTitle->getPrefixedText(); + } + } + + /** + * Try to guess the section anchor name based on a wikitext fragment + * presumably extracted from a heading, for example "Header" from + * "== Header ==". + */ + public function guessSectionNameFromWikiText( $text ) { + # Strip out wikitext links(they break the anchor) + $text = $this->stripSectionName( $text ); + $headline = Sanitizer::decodeCharReferences( $text ); + # strip out HTML + $headline = StringUtils::delimiterReplace( '<', '>', '', $headline ); + $headline = trim( $headline ); + $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); + $replacearray = array( + '%3A' => ':', + '%' => '.' + ); + return str_replace( + array_keys( $replacearray ), + array_values( $replacearray ), + $sectionanchor ); + } + + /** + * Strips a text string of wikitext for use in a section anchor + * + * Accepts a text string and then removes all wikitext from the + * string and leaves only the resultant text (i.e. the result of + * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of + * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended + * to create valid section anchors by mimicing the output of the + * parser when headings are parsed. + * + * @param $text string Text string to be stripped of wikitext + * for use in a Section anchor + * @return Filtered text string + */ + public function stripSectionName( $text ) { + # Strip internal link markup + $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text); + $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text); + + # Strip external link markup (FIXME: Not Tolerant to blank link text + # I.E. [http://www.mediawiki.org] will render as [1] or something depending + # on how many empty links there are on the page - need to figure that out. + $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text); + + # Parse wikitext quotes (italics & bold) + $text = $this->doQuotes($text); + + # Strip HTML tags + $text = StringUtils::delimiterReplace( '<', '>', '', $text ); + return $text; + } + + /** + * strip/replaceVariables/unstrip for preprocessor regression testing + */ + function srvus( $text ) { + $text = $this->strip( $text, $this->mStripState ); + $text = Sanitizer::removeHTMLtags( $text ); + $text = $this->replaceVariables( $text ); + $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text ); + $text = $this->mStripState->unstripBoth( $text ); + return $text; + } +} + diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php new file mode 100644 index 00000000..bddfb9f1 --- /dev/null +++ b/includes/PrefixSearch.php @@ -0,0 +1,135 @@ +<?php + +class PrefixSearch { + /** + * Do a prefix search of titles and return a list of matching page names. + * @param string $search + * @param int $limit + * @return array of strings + */ + public static function titleSearch( $search, $limit ) { + $search = trim( $search ); + if( $search == '' ) { + return array(); // Return empty result + } + + $title = Title::newFromText( $search ); + if( $title && $title->getInterwiki() == '' ) { + $ns = $title->getNamespace(); + return self::searchBackend( + $title->getNamespace(), $title->getText(), $limit ); + } + + // Is this a namespace prefix? + $title = Title::newFromText( $search . 'Dummy' ); + if( $title && $title->getText() == 'Dummy' + && $title->getNamespace() != NS_MAIN + && $title->getInterwiki() == '' ) { + return self::searchBackend( + $title->getNamespace(), '', $limit ); + } + + return self::searchBackend( 0, $search, $limit ); + } + + + /** + * Do a prefix search of titles and return a list of matching page names. + * @param string $search + * @param int $limit + * @return array of strings + */ + protected static function searchBackend( $ns, $search, $limit ) { + if( $ns == NS_MEDIA ) { + $ns = NS_IMAGE; + } elseif( $ns == NS_SPECIAL ) { + return self::specialSearch( $search, $limit ); + } + + $srchres = array(); + if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) { + return self::defaultSearchBackend( $ns, $search, $limit ); + } + return $srchres; + } + + /** + * Prefix search special-case for Special: namespace. + */ + protected static function specialSearch( $search, $limit ) { + global $wgContLang; + $searchKey = $wgContLang->caseFold( $search ); + + // Unlike SpecialPage itself, we want the canonical forms of both + // canonical and alias title forms... + SpecialPage::initList(); + SpecialPage::initAliasList(); + $keys = array(); + foreach( array_keys( SpecialPage::$mList ) as $page ) { + $keys[$wgContLang->caseFold( $page )] = $page; + } + foreach( $wgContLang->getSpecialPageAliases() as $page => $aliases ) { + foreach( $aliases as $alias ) { + $keys[$wgContLang->caseFold( $alias )] = $alias; + } + } + ksort( $keys ); + + $srchres = array(); + foreach( $keys as $pageKey => $page ) { + if( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { + $srchres[] = Title::makeTitle( NS_SPECIAL, $page )->getPrefixedText(); + } + if( count( $srchres ) >= $limit ) { + break; + } + } + return $srchres; + } + + /** + * Unless overridden by PrefixSearchBackend hook... + * This is case-sensitive except the first letter (per $wgCapitalLinks) + * + * @param int $ns Namespace to search in + * @param string $search term + * @param int $limit max number of items to return + * @return array of title strings + */ + protected static function defaultSearchBackend( $ns, $search, $limit ) { + global $wgCapitalLinks, $wgContLang; + + if( $wgCapitalLinks ) { + $search = $wgContLang->ucfirst( $search ); + } + + // Prepare nested request + $req = new FauxRequest(array ( + 'action' => 'query', + 'list' => 'allpages', + 'apnamespace' => $ns, + 'aplimit' => $limit, + 'apprefix' => $search + )); + + // Execute + $module = new ApiMain($req); + $module->execute(); + + // Get resulting data + $data = $module->getResultData(); + + // Reformat useful data for future printing by JSON engine + $srchres = array (); + foreach ($data['query']['allpages'] as & $pageinfo) { + // Note: this data will no be printable by the xml engine + // because it does not support lists of unnamed items + $srchres[] = $pageinfo['title']; + } + + return $srchres; + } + +} + +?>
\ No newline at end of file diff --git a/includes/Preprocessor.php b/includes/Preprocessor.php new file mode 100644 index 00000000..34bc1e5b --- /dev/null +++ b/includes/Preprocessor.php @@ -0,0 +1,154 @@ +<?php + +interface Preprocessor { + /** Create a new preprocessor object based on an initialised Parser object */ + function __construct( $parser ); + + /** Create a new top-level frame for expansion of a page */ + function newFrame(); + + /** Preprocess text to a PPNode */ + function preprocessToObj( $text, $flags = 0 ); +} + +interface PPFrame { + const NO_ARGS = 1; + const NO_TEMPLATES = 2; + const STRIP_COMMENTS = 4; + const NO_IGNORE = 8; + const RECOVER_COMMENTS = 16; + + const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet + + /** + * Create a child frame + */ + function newChild( $args = false, $title = false ); + + /** + * Expand a document tree node + */ + function expand( $root, $flags = 0 ); + + /** + * Implode with flags for expand() + */ + function implodeWithFlags( $sep, $flags /*, ... */ ); + + /** + * Implode with no flags specified + */ + function implode( $sep /*, ... */ ); + + /** + * Makes an object that, when expand()ed, will be the same as one obtained + * with implode() + */ + function virtualImplode( $sep /*, ... */ ); + + /** + * Virtual implode with brackets + */ + function virtualBracketedImplode( $start, $sep, $end /*, ... */ ); + + /** + * Returns true if there are no arguments in this frame + */ + function isEmpty(); + + /** + * Get an argument to this frame by name + */ + function getArgument( $name ); + + /** + * Returns true if the infinite loop check is OK, false if a loop is detected + */ + function loopCheck( $title ); + + /** + * Return true if the frame is a template frame + */ + function isTemplate(); +} + +/** + * There are three types of nodes: + * * Tree nodes, which have a name and contain other nodes as children + * * Array nodes, which also contain other nodes but aren't considered part of a tree + * * Leaf nodes, which contain the actual data + * + * This interface provides access to the tree structure and to the contents of array nodes, + * but it does not provide access to the internal structure of leaf nodes. Access to leaf + * data is provided via two means: + * * PPFrame::expand(), which provides expanded text + * * The PPNode::split*() functions, which provide metadata about certain types of tree node + */ +interface PPNode { + /** + * Get an array-type node containing the children of this node. + * Returns false if this is not a tree node. + */ + function getChildren(); + + /** + * Get the first child of a tree node. False if there isn't one. + */ + function getFirstChild(); + + /** + * Get the next sibling of any node. False if there isn't one + */ + function getNextSibling(); + + /** + * Get all children of this tree node which have a given name. + * Returns an array-type node, or false if this is not a tree node. + */ + function getChildrenOfType( $type ); + + + /** + * Returns the length of the array, or false if this is not an array-type node + */ + function getLength(); + + /** + * Returns an item of an array-type node + */ + function item( $i ); + + /** + * Get the name of this node. The following names are defined here: + * + * h A heading node. + * template A double-brace node. + * tplarg A triple-brace node. + * title The first argument to a template or tplarg node. + * part Subsequent arguments to a template or tplarg node. + * #nodelist An array-type node + * + * The subclass may define various other names for tree and leaf nodes. + */ + function getName(); + + /** + * Split a <part> node into an associative array containing: + * name PPNode name + * index String index + * value PPNode value + */ + function splitArg(); + + /** + * Split an <ext> node into an associative array containing name, attr, inner and close + * All values in the resulting array are PPNodes. Inner and close are optional. + */ + function splitExt(); + + /** + * Split an <h> node + */ + function splitHeading(); +} + diff --git a/includes/Preprocessor_DOM.php b/includes/Preprocessor_DOM.php new file mode 100644 index 00000000..0e2e9a16 --- /dev/null +++ b/includes/Preprocessor_DOM.php @@ -0,0 +1,1356 @@ +<?php + +class Preprocessor_DOM implements Preprocessor { + var $parser, $memoryLimit; + + function __construct( $parser ) { + $this->parser = $parser; + $mem = ini_get( 'memory_limit' ); + $this->memoryLimit = false; + if ( strval( $mem ) !== '' && $mem != -1 ) { + if ( preg_match( '/^\d+$/', $mem ) ) { + $this->memoryLimit = $mem; + } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) { + $this->memoryLimit = $m[1] * 1048576; + } + } + } + + function newFrame() { + return new PPFrame_DOM( $this ); + } + + function memCheck() { + if ( $this->memoryLimit === false ) { + return; + } + $usage = memory_get_usage(); + if ( $usage > $this->memoryLimit * 0.9 ) { + $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 ); + throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" ); + } + return $usage <= $this->memoryLimit * 0.8; + } + + /** + * Preprocess some wikitext and return the document tree. + * This is the ghost of Parser::replace_variables(). + * + * @param string $text The text to parse + * @param integer flags Bitwise combination of: + * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * included. Default is to assume a direct page view. + * + * The generated DOM tree must depend only on the input text and the flags. + * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. + * + * Any flag added to the $flags parameter here, or any other parameter liable to cause a + * change in the DOM tree for a given text, must be passed through the section identifier + * in the section edit link and thus back to extractSections(). + * + * The output of this function is currently only cached in process memory, but a persistent + * cache may be implemented at a later date which takes further advantage of these strict + * dependency requirements. + * + * @private + */ + function preprocessToObj( $text, $flags = 0 ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__.'-makexml' ); + + $rules = array( + '{' => array( + 'end' => '}', + 'names' => array( + 2 => 'template', + 3 => 'tplarg', + ), + 'min' => 2, + 'max' => 3, + ), + '[' => array( + 'end' => ']', + 'names' => array( 2 => null ), + 'min' => 2, + 'max' => 2, + ) + ); + + $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; + + $xmlishElements = $this->parser->getStripList(); + $enableOnlyinclude = false; + if ( $forInclusion ) { + $ignoredTags = array( 'includeonly', '/includeonly' ); + $ignoredElements = array( 'noinclude' ); + $xmlishElements[] = 'noinclude'; + if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) { + $enableOnlyinclude = true; + } + } else { + $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); + $ignoredElements = array( 'includeonly' ); + $xmlishElements[] = 'includeonly'; + } + $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); + + // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset + $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; + + $stack = new PPDStack; + + $searchBase = "[{<\n"; #} + $revText = strrev( $text ); // For fast reverse searches + + $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start + $accum =& $stack->getAccum(); # Current accumulator + $accum = '<root>'; + $findEquals = false; # True to find equals signs in arguments + $findPipe = false; # True to take notice of pipe characters + $headingIndex = 1; + $inHeading = false; # True if $i is inside a possible heading + $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i + $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude> + $fakeLineStart = true; # Do a line-start run without outputting an LF character + + while ( true ) { + //$this->memCheck(); + + if ( $findOnlyinclude ) { + // Ignore all input up to the next <onlyinclude> + $startPos = strpos( $text, '<onlyinclude>', $i ); + if ( $startPos === false ) { + // Ignored section runs to the end + $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>'; + break; + } + $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end + $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>'; + $i = $tagEndPos; + $findOnlyinclude = false; + } + + if ( $fakeLineStart ) { + $found = 'line-start'; + $curChar = ''; + } else { + # Find next opening brace, closing brace or pipe + $search = $searchBase; + if ( $stack->top === false ) { + $currentClosing = ''; + } else { + $currentClosing = $stack->top->close; + $search .= $currentClosing; + } + if ( $findPipe ) { + $search .= '|'; + } + if ( $findEquals ) { + // First equals will be for the template + $search .= '='; + } + $rule = null; + # Output literal section, advance input counter + $literalLength = strcspn( $text, $search, $i ); + if ( $literalLength > 0 ) { + $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); + $i += $literalLength; + } + if ( $i >= strlen( $text ) ) { + if ( $currentClosing == "\n" ) { + // Do a past-the-end run to finish off the heading + $curChar = ''; + $found = 'line-end'; + } else { + # All done + break; + } + } else { + $curChar = $text[$i]; + if ( $curChar == '|' ) { + $found = 'pipe'; + } elseif ( $curChar == '=' ) { + $found = 'equals'; + } elseif ( $curChar == '<' ) { + $found = 'angle'; + } elseif ( $curChar == "\n" ) { + if ( $inHeading ) { + $found = 'line-end'; + } else { + $found = 'line-start'; + } + } elseif ( $curChar == $currentClosing ) { + $found = 'close'; + } elseif ( isset( $rules[$curChar] ) ) { + $found = 'open'; + $rule = $rules[$curChar]; + } else { + # Some versions of PHP have a strcspn which stops on null characters + # Ignore and continue + ++$i; + continue; + } + } + } + + if ( $found == 'angle' ) { + $matches = false; + // Handle </onlyinclude> + if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) { + $findOnlyinclude = true; + continue; + } + + // Determine element name + if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { + // Element name missing or not listed + $accum .= '<'; + ++$i; + continue; + } + // Handle comments + if ( isset( $matches[2] ) && $matches[2] == '!--' ) { + // To avoid leaving blank lines, when a comment is both preceded + // and followed by a newline (ignoring spaces), trim leading and + // trailing spaces and one of the newlines. + + // Find the end + $endPos = strpos( $text, '-->', $i + 4 ); + if ( $endPos === false ) { + // Unclosed comment in input, runs to end + $inner = substr( $text, $i ); + $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; + $i = strlen( $text ); + } else { + // Search backwards for leading whitespace + $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0; + // Search forwards for trailing whitespace + // $wsEnd will be the position of the last space + $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); + // Eat the line if possible + // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at + // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but + // it's a possible beneficial b/c break. + if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" + && substr( $text, $wsEnd + 1, 1 ) == "\n" ) + { + $startPos = $wsStart; + $endPos = $wsEnd + 1; + // Remove leading whitespace from the end of the accumulator + // Sanity check first though + $wsLength = $i - $wsStart; + if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) { + $accum = substr( $accum, 0, -$wsLength ); + } + // Do a line-start run next time to look for headings after the comment + $fakeLineStart = true; + } else { + // No line to eat, just take the comment itself + $startPos = $i; + $endPos += 2; + } + + if ( $stack->top ) { + $part = $stack->top->getCurrentPart(); + if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) { + // Comments abutting, no change in visual end + $part->commentEnd = $wsEnd; + } else { + $part->visualEnd = $wsStart; + $part->commentEnd = $endPos; + } + } + $i = $endPos + 1; + $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); + $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; + } + continue; + } + $name = $matches[1]; + $attrStart = $i + strlen( $name ) + 1; + + // Find end of tag + $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); + if ( $tagEndPos === false ) { + // Infinite backtrack + // Disable tag search to prevent worst-case O(N^2) performance + $noMoreGT = true; + $accum .= '<'; + ++$i; + continue; + } + + // Handle ignored tags + if ( in_array( $name, $ignoredTags ) ) { + $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>'; + $i = $tagEndPos + 1; + continue; + } + + $tagStartPos = $i; + if ( $text[$tagEndPos-1] == '/' ) { + $attrEnd = $tagEndPos - 1; + $inner = null; + $i = $tagEndPos + 1; + $close = ''; + } else { + $attrEnd = $tagEndPos; + // Find closing tag + if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) { + $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); + $i = $matches[0][1] + strlen( $matches[0][0] ); + $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>'; + } else { + // No end tag -- let it run out to the end of the text. + $inner = substr( $text, $tagEndPos + 1 ); + $i = strlen( $text ); + $close = ''; + } + } + // <includeonly> and <noinclude> just become <ignore> tags + if ( in_array( $name, $ignoredElements ) ) { + $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) + . '</ignore>'; + continue; + } + + $accum .= '<ext>'; + if ( $attrEnd <= $attrStart ) { + $attr = ''; + } else { + $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); + } + $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' . + // Note that the attr element contains the whitespace between name and attribute, + // this is necessary for precise reconstruction during pre-save transform. + '<attr>' . htmlspecialchars( $attr ) . '</attr>'; + if ( $inner !== null ) { + $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>'; + } + $accum .= $close . '</ext>'; + } + + elseif ( $found == 'line-start' ) { + // Is this the start of a heading? + // Line break belongs before the heading element in any case + if ( $fakeLineStart ) { + $fakeLineStart = false; + } else { + $accum .= $curChar; + $i++; + } + + $count = strspn( $text, '=', $i, 6 ); + if ( $count == 1 && $findEquals ) { + // DWIM: This looks kind of like a name/value separator + // Let's let the equals handler have it and break the potential heading + // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. + } elseif ( $count > 0 ) { + $piece = array( + 'open' => "\n", + 'close' => "\n", + 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ), + 'startPos' => $i, + 'count' => $count ); + $stack->push( $piece ); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + $i += $count; + } + } + + elseif ( $found == 'line-end' ) { + $piece = $stack->top; + // A heading must be open, otherwise \n wouldn't have been in the search list + assert( $piece->open == "\n" ); + $part = $piece->getCurrentPart(); + // Search back through the input to see if it has a proper close + // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient + $wsLength = strspn( $revText, " \t", strlen( $text ) - $i ); + $searchStart = $i - $wsLength; + if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { + // Comment found at line end + // Search for equals signs before the comment + $searchStart = $part->visualEnd; + $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart ); + } + $count = $piece->count; + $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart ); + if ( $equalsLength > 0 ) { + if ( $i - $equalsLength == $piece->startPos ) { + // This is just a single string of equals signs on its own line + // Replicate the doHeadings behaviour /={count}(.+)={count}/ + // First find out how many equals signs there really are (don't stop at 6) + $count = $equalsLength; + if ( $count < 3 ) { + $count = 0; + } else { + $count = min( 6, intval( ( $count - 1 ) / 2 ) ); + } + } else { + $count = min( $equalsLength, $count ); + } + if ( $count > 0 ) { + // Normal match, output <h> + $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>"; + $headingIndex++; + } else { + // Single equals sign on its own line, count=0 + $element = $accum; + } + } else { + // No match, no <h>, just pass down the inner text + $element = $accum; + } + // Unwind the stack + $stack->pop(); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + + // Append the result to the enclosing accumulator + $accum .= $element; + // Note that we do NOT increment the input pointer. + // This is because the closing linebreak could be the opening linebreak of + // another heading. Infinite loops are avoided because the next iteration MUST + // hit the heading open case above, which unconditionally increments the + // input pointer. + } + + elseif ( $found == 'open' ) { + # count opening brace characters + $count = strspn( $text, $curChar, $i ); + + # we need to add to stack only if opening brace count is enough for one of the rules + if ( $count >= $rule['min'] ) { + # Add it to the stack + $piece = array( + 'open' => $curChar, + 'close' => $rule['end'], + 'count' => $count, + 'lineStart' => ($i > 0 && $text[$i-1] == "\n"), + ); + + $stack->push( $piece ); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + } else { + # Add literal brace(s) + $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); + } + $i += $count; + } + + elseif ( $found == 'close' ) { + $piece = $stack->top; + # lets check if there are enough characters for closing brace + $maxCount = $piece->count; + $count = strspn( $text, $curChar, $i, $maxCount ); + + # check for maximum matching characters (if there are 5 closing + # characters, we will probably need only 3 - depending on the rules) + $matchingCount = 0; + $rule = $rules[$piece->open]; + if ( $count > $rule['max'] ) { + # The specified maximum exists in the callback array, unless the caller + # has made an error + $matchingCount = $rule['max']; + } else { + # Count is less than the maximum + # Skip any gaps in the callback array to find the true largest match + # Need to use array_key_exists not isset because the callback can be null + $matchingCount = $count; + while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { + --$matchingCount; + } + } + + if ($matchingCount <= 0) { + # No matching element found in callback array + # Output a literal closing brace and continue + $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); + $i += $count; + continue; + } + $name = $rule['names'][$matchingCount]; + if ( $name === null ) { + // No element, just literal text + $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount ); + } else { + # Create XML element + # Note: $parts is already XML, does not need to be encoded further + $parts = $piece->parts; + $title = $parts[0]->out; + unset( $parts[0] ); + + # The invocation is at the start of the line if lineStart is set in + # the stack, and all opening brackets are used up. + if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { + $attr = ' lineStart="1"'; + } else { + $attr = ''; + } + + $element = "<$name$attr>"; + $element .= "<title>$title</title>"; + $argIndex = 1; + foreach ( $parts as $partIndex => $part ) { + if ( isset( $part->eqpos ) ) { + $argName = substr( $part->out, 0, $part->eqpos ); + $argValue = substr( $part->out, $part->eqpos + 1 ); + $element .= "<part><name>$argName</name>=<value>$argValue</value></part>"; + } else { + $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>"; + $argIndex++; + } + } + $element .= "</$name>"; + } + + # Advance input pointer + $i += $matchingCount; + + # Unwind the stack + $stack->pop(); + $accum =& $stack->getAccum(); + + # Re-add the old stack element if it still has unmatched opening characters remaining + if ($matchingCount < $piece->count) { + $piece->parts = array( new PPDPart ); + $piece->count -= $matchingCount; + # do we still qualify for any callback with remaining count? + $names = $rules[$piece->open]['names']; + $skippedBraces = 0; + $enclosingAccum =& $accum; + while ( $piece->count ) { + if ( array_key_exists( $piece->count, $names ) ) { + $stack->push( $piece ); + $accum =& $stack->getAccum(); + break; + } + --$piece->count; + $skippedBraces ++; + } + $enclosingAccum .= str_repeat( $piece->open, $skippedBraces ); + } + + extract( $stack->getFlags() ); + + # Add XML element to the enclosing accumulator + $accum .= $element; + } + + elseif ( $found == 'pipe' ) { + $findEquals = true; // shortcut for getFlags() + $stack->addPart(); + $accum =& $stack->getAccum(); + ++$i; + } + + elseif ( $found == 'equals' ) { + $findEquals = false; // shortcut for getFlags() + $stack->getCurrentPart()->eqpos = strlen( $accum ); + $accum .= '='; + ++$i; + } + } + + # Output any remaining unclosed brackets + foreach ( $stack->stack as $piece ) { + $stack->rootAccum .= $piece->breakSyntax(); + } + $stack->rootAccum .= '</root>'; + $xml = $stack->rootAccum; + + wfProfileOut( __METHOD__.'-makexml' ); + wfProfileIn( __METHOD__.'-loadXML' ); + $dom = new DOMDocument; + wfSuppressWarnings(); + $result = $dom->loadXML( $xml ); + wfRestoreWarnings(); + if ( !$result ) { + // Try running the XML through UtfNormal to get rid of invalid characters + $xml = UtfNormal::cleanUp( $xml ); + $result = $dom->loadXML( $xml ); + if ( !$result ) { + throw new MWException( __METHOD__.' generated invalid XML' ); + } + } + $obj = new PPNode_DOM( $dom->documentElement ); + wfProfileOut( __METHOD__.'-loadXML' ); + wfProfileOut( __METHOD__ ); + return $obj; + } +} + +/** + * Stack class to help Preprocessor::preprocessToObj() + */ +class PPDStack { + var $stack, $rootAccum, $top; + var $out; + var $elementClass = 'PPDStackElement'; + + static $false = false; + + function __construct() { + $this->stack = array(); + $this->top = false; + $this->rootAccum = ''; + $this->accum =& $this->rootAccum; + } + + function count() { + return count( $this->stack ); + } + + function &getAccum() { + return $this->accum; + } + + function getCurrentPart() { + if ( $this->top === false ) { + return false; + } else { + return $this->top->getCurrentPart(); + } + } + + function push( $data ) { + if ( $data instanceof $this->elementClass ) { + $this->stack[] = $data; + } else { + $class = $this->elementClass; + $this->stack[] = new $class( $data ); + } + $this->top = $this->stack[ count( $this->stack ) - 1 ]; + $this->accum =& $this->top->getAccum(); + } + + function pop() { + if ( !count( $this->stack ) ) { + throw new MWException( __METHOD__.': no elements remaining' ); + } + $temp = array_pop( $this->stack ); + + if ( count( $this->stack ) ) { + $this->top = $this->stack[ count( $this->stack ) - 1 ]; + $this->accum =& $this->top->getAccum(); + } else { + $this->top = self::$false; + $this->accum =& $this->rootAccum; + } + return $temp; + } + + function addPart( $s = '' ) { + $this->top->addPart( $s ); + $this->accum =& $this->top->getAccum(); + } + + function getFlags() { + if ( !count( $this->stack ) ) { + return array( + 'findEquals' => false, + 'findPipe' => false, + 'inHeading' => false, + ); + } else { + return $this->top->getFlags(); + } + } +} + +class PPDStackElement { + var $open, // Opening character (\n for heading) + $close, // Matching closing character + $count, // Number of opening characters found (number of "=" for heading) + $parts, // Array of PPDPart objects describing pipe-separated parts. + $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. + + var $partClass = 'PPDPart'; + + function __construct( $data = array() ) { + $class = $this->partClass; + $this->parts = array( new $class ); + + foreach ( $data as $name => $value ) { + $this->$name = $value; + } + } + + function &getAccum() { + return $this->parts[count($this->parts) - 1]->out; + } + + function addPart( $s = '' ) { + $class = $this->partClass; + $this->parts[] = new $class( $s ); + } + + function getCurrentPart() { + return $this->parts[count($this->parts) - 1]; + } + + function getFlags() { + $partCount = count( $this->parts ); + $findPipe = $this->open != "\n" && $this->open != '['; + return array( + 'findPipe' => $findPipe, + 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ), + 'inHeading' => $this->open == "\n", + ); + } + + /** + * Get the output string that would result if the close is not found. + */ + function breakSyntax( $openingCount = false ) { + if ( $this->open == "\n" ) { + $s = $this->parts[0]->out; + } else { + if ( $openingCount === false ) { + $openingCount = $this->count; + } + $s = str_repeat( $this->open, $openingCount ); + $first = true; + foreach ( $this->parts as $part ) { + if ( $first ) { + $first = false; + } else { + $s .= '|'; + } + $s .= $part->out; + } + } + return $s; + } +} + +class PPDPart { + var $out; // Output accumulator string + + // Optional member variables: + // eqpos Position of equals sign in output accumulator + // commentEnd Past-the-end input pointer for the last comment encountered + // visualEnd Past-the-end input pointer for the end of the accumulator minus comments + + function __construct( $out = '' ) { + $this->out = $out; + } +} + +/** + * An expansion frame, used as a context to expand the result of preprocessToObj() + */ +class PPFrame_DOM implements PPFrame { + var $preprocessor, $parser, $title; + var $titleCache; + + /** + * Hashtable listing templates which are disallowed for expansion in this frame, + * having been encountered previously in parent frames. + */ + var $loopCheckHash; + + /** + * Recursion depth of this frame, top = 0 + */ + var $depth; + + + /** + * Construct a new preprocessor frame. + * @param Preprocessor $preprocessor The parent preprocessor + */ + function __construct( $preprocessor ) { + $this->preprocessor = $preprocessor; + $this->parser = $preprocessor->parser; + $this->title = $this->parser->mTitle; + $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); + $this->loopCheckHash = array(); + $this->depth = 0; + } + + /** + * Create a new child frame + * $args is optionally a multi-root PPNode or array containing the template arguments + */ + function newChild( $args = false, $title = false ) { + $namedArgs = array(); + $numberedArgs = array(); + if ( $title === false ) { + $title = $this->title; + } + if ( $args !== false ) { + $xpath = false; + if ( $args instanceof PPNode ) { + $args = $args->node; + } + foreach ( $args as $arg ) { + if ( !$xpath ) { + $xpath = new DOMXPath( $arg->ownerDocument ); + } + + $nameNodes = $xpath->query( 'name', $arg ); + $value = $xpath->query( 'value', $arg ); + if ( $nameNodes->item( 0 )->hasAttributes() ) { + // Numbered parameter + $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; + $numberedArgs[$index] = $value->item( 0 ); + unset( $namedArgs[$index] ); + } else { + // Named parameter + $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) ); + $namedArgs[$name] = $value->item( 0 ); + unset( $numberedArgs[$name] ); + } + } + } + return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); + } + + function expand( $root, $flags = 0 ) { + if ( is_string( $root ) ) { + return $root; + } + + if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount ) + { + return '<span class="error">Node-count limit exceeded</span>'; + } + + if ( $root instanceof PPNode_DOM ) { + $root = $root->node; + } + if ( $root instanceof DOMDocument ) { + $root = $root->documentElement; + } + + $outStack = array( '', '' ); + $iteratorStack = array( false, $root ); + $indexStack = array( 0, 0 ); + + while ( count( $iteratorStack ) > 1 ) { + $level = count( $outStack ) - 1; + $iteratorNode =& $iteratorStack[ $level ]; + $out =& $outStack[$level]; + $index =& $indexStack[$level]; + + if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node; + + if ( is_array( $iteratorNode ) ) { + if ( $index >= count( $iteratorNode ) ) { + // All done with this iterator + $iteratorStack[$level] = false; + $contextNode = false; + } else { + $contextNode = $iteratorNode[$index]; + $index++; + } + } elseif ( $iteratorNode instanceof DOMNodeList ) { + if ( $index >= $iteratorNode->length ) { + // All done with this iterator + $iteratorStack[$level] = false; + $contextNode = false; + } else { + $contextNode = $iteratorNode->item( $index ); + $index++; + } + } else { + // Copy to $contextNode and then delete from iterator stack, + // because this is not an iterator but we do have to execute it once + $contextNode = $iteratorStack[$level]; + $iteratorStack[$level] = false; + } + + if ( $contextNode instanceof PPNode_DOM ) $contextNode = $contextNode->node; + + $newIterator = false; + + if ( $contextNode === false ) { + // nothing to do + } elseif ( is_string( $contextNode ) ) { + $out .= $contextNode; + } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) { + $newIterator = $contextNode; + } elseif ( $contextNode instanceof DOMNode ) { + if ( $contextNode->nodeType == XML_TEXT_NODE ) { + $out .= $contextNode->nodeValue; + } elseif ( $contextNode->nodeName == 'template' ) { + # Double-brace expansion + $xpath = new DOMXPath( $contextNode->ownerDocument ); + $titles = $xpath->query( 'title', $contextNode ); + $title = $titles->item( 0 ); + $parts = $xpath->query( 'part', $contextNode ); + if ( $flags & self::NO_TEMPLATES ) { + $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts ); + } else { + $lineStart = $contextNode->getAttribute( 'lineStart' ); + $params = array( + 'title' => new PPNode_DOM( $title ), + 'parts' => new PPNode_DOM( $parts ), + 'lineStart' => $lineStart ); + $ret = $this->parser->braceSubstitution( $params, $this ); + if ( isset( $ret['object'] ) ) { + $newIterator = $ret['object']; + } else { + $out .= $ret['text']; + } + } + } elseif ( $contextNode->nodeName == 'tplarg' ) { + # Triple-brace expansion + $xpath = new DOMXPath( $contextNode->ownerDocument ); + $titles = $xpath->query( 'title', $contextNode ); + $title = $titles->item( 0 ); + $parts = $xpath->query( 'part', $contextNode ); + if ( $flags & self::NO_ARGS ) { + $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts ); + } else { + $params = array( + 'title' => new PPNode_DOM( $title ), + 'parts' => new PPNode_DOM( $parts ) ); + $ret = $this->parser->argSubstitution( $params, $this ); + if ( isset( $ret['object'] ) ) { + $newIterator = $ret['object']; + } else { + $out .= $ret['text']; + } + } + } elseif ( $contextNode->nodeName == 'comment' ) { + # HTML-style comment + # Remove it in HTML, pre+remove and STRIP_COMMENTS modes + if ( $this->parser->ot['html'] + || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) + || ( $flags & self::STRIP_COMMENTS ) ) + { + $out .= ''; + } + # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result + # Not in RECOVER_COMMENTS mode (extractSections) though + elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) { + $out .= $this->parser->insertStripItem( $contextNode->textContent ); + } + # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove + else { + $out .= $contextNode->textContent; + } + } elseif ( $contextNode->nodeName == 'ignore' ) { + # Output suppression used by <includeonly> etc. + # OT_WIKI will only respect <ignore> in substed templates. + # The other output types respect it unless NO_IGNORE is set. + # extractSections() sets NO_IGNORE and so never respects it. + if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) { + $out .= $contextNode->textContent; + } else { + $out .= ''; + } + } elseif ( $contextNode->nodeName == 'ext' ) { + # Extension tag + $xpath = new DOMXPath( $contextNode->ownerDocument ); + $names = $xpath->query( 'name', $contextNode ); + $attrs = $xpath->query( 'attr', $contextNode ); + $inners = $xpath->query( 'inner', $contextNode ); + $closes = $xpath->query( 'close', $contextNode ); + $params = array( + 'name' => new PPNode_DOM( $names->item( 0 ) ), + 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, + 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, + 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, + ); + $out .= $this->parser->extensionSubstitution( $params, $this ); + } elseif ( $contextNode->nodeName == 'h' ) { + # Heading + $s = $this->expand( $contextNode->childNodes, $flags ); + + # Insert a heading marker only for <h> children of <root> + # This is to stop extractSections from going over multiple tree levels + if ( $contextNode->parentNode->nodeName == 'root' + && $this->parser->ot['html'] ) + { + # Insert heading index marker + $headingIndex = $contextNode->getAttribute( 'i' ); + $titleText = $this->title->getPrefixedDBkey(); + $this->parser->mHeadings[] = array( $titleText, $headingIndex ); + $serial = count( $this->parser->mHeadings ) - 1; + $marker = "{$this->parser->mUniqPrefix}-h-$serial-{$this->parser->mMarkerSuffix}"; + $count = $contextNode->getAttribute( 'level' ); + $s = substr( $s, 0, $count ) . $marker . substr( $s, $count ); + $this->parser->mStripState->general->setPair( $marker, '' ); + } + $out .= $s; + } else { + # Generic recursive expansion + $newIterator = $contextNode->childNodes; + } + } else { + throw new MWException( __METHOD__.': Invalid parameter type' ); + } + + if ( $newIterator !== false ) { + if ( $newIterator instanceof PPNode_DOM ) { + $newIterator = $newIterator->node; + } + $outStack[] = ''; + $iteratorStack[] = $newIterator; + $indexStack[] = 0; + } elseif ( $iteratorStack[$level] === false ) { + // Return accumulated value to parent + // With tail recursion + while ( $iteratorStack[$level] === false && $level > 0 ) { + $outStack[$level - 1] .= $out; + array_pop( $outStack ); + array_pop( $iteratorStack ); + array_pop( $indexStack ); + $level--; + } + } + } + return $outStack[0]; + } + + function implodeWithFlags( $sep, $flags /*, ... */ ) { + $args = array_slice( func_get_args(), 2 ); + + $first = true; + $s = ''; + foreach ( $args as $root ) { + if ( $root instanceof PPNode_DOM ) $root = $root->node; + if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $s .= $sep; + } + $s .= $this->expand( $node, $flags ); + } + } + return $s; + } + + /** + * Implode with no flags specified + * This previously called implodeWithFlags but has now been inlined to reduce stack depth + */ + function implode( $sep /*, ... */ ) { + $args = array_slice( func_get_args(), 1 ); + + $first = true; + $s = ''; + foreach ( $args as $root ) { + if ( $root instanceof PPNode_DOM ) $root = $root->node; + if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $s .= $sep; + } + $s .= $this->expand( $node ); + } + } + return $s; + } + + /** + * Makes an object that, when expand()ed, will be the same as one obtained + * with implode() + */ + function virtualImplode( $sep /*, ... */ ) { + $args = array_slice( func_get_args(), 1 ); + $out = array(); + $first = true; + if ( $root instanceof PPNode_DOM ) $root = $root->node; + + foreach ( $args as $root ) { + if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $out[] = $sep; + } + $out[] = $node; + } + } + return $out; + } + + /** + * Virtual implode with brackets + */ + function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { + $args = array_slice( func_get_args(), 3 ); + $out = array( $start ); + $first = true; + + foreach ( $args as $root ) { + if ( $root instanceof PPNode_DOM ) $root = $root->node; + if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $out[] = $sep; + } + $out[] = $node; + } + } + $out[] = $end; + return $out; + } + + function __toString() { + return 'frame{}'; + } + + function getPDBK( $level = false ) { + if ( $level === false ) { + return $this->title->getPrefixedDBkey(); + } else { + return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; + } + } + + /** + * Returns true if there are no arguments in this frame + */ + function isEmpty() { + return true; + } + + function getArgument( $name ) { + return false; + } + + /** + * Returns true if the infinite loop check is OK, false if a loop is detected + */ + function loopCheck( $title ) { + return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); + } + + /** + * Return true if the frame is a template frame + */ + function isTemplate() { + return false; + } +} + +/** + * Expansion frame with template arguments + */ +class PPTemplateFrame_DOM extends PPFrame_DOM { + var $numberedArgs, $namedArgs, $parent; + var $numberedExpansionCache, $namedExpansionCache; + + function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { + $this->preprocessor = $preprocessor; + $this->parser = $preprocessor->parser; + $this->parent = $parent; + $this->numberedArgs = $numberedArgs; + $this->namedArgs = $namedArgs; + $this->title = $title; + $pdbk = $title ? $title->getPrefixedDBkey() : false; + $this->titleCache = $parent->titleCache; + $this->titleCache[] = $pdbk; + $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; + if ( $pdbk !== false ) { + $this->loopCheckHash[$pdbk] = true; + } + $this->depth = $parent->depth + 1; + $this->numberedExpansionCache = $this->namedExpansionCache = array(); + } + + function __toString() { + $s = 'tplframe{'; + $first = true; + $args = $this->numberedArgs + $this->namedArgs; + foreach ( $args as $name => $value ) { + if ( $first ) { + $first = false; + } else { + $s .= ', '; + } + $s .= "\"$name\":\"" . + str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"'; + } + $s .= '}'; + return $s; + } + /** + * Returns true if there are no arguments in this frame + */ + function isEmpty() { + return !count( $this->numberedArgs ) && !count( $this->namedArgs ); + } + + function getNumberedArgument( $index ) { + if ( !isset( $this->numberedArgs[$index] ) ) { + return false; + } + if ( !isset( $this->numberedExpansionCache[$index] ) ) { + # No trimming for unnamed arguments + $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS ); + } + return $this->numberedExpansionCache[$index]; + } + + function getNamedArgument( $name ) { + if ( !isset( $this->namedArgs[$name] ) ) { + return false; + } + if ( !isset( $this->namedExpansionCache[$name] ) ) { + # Trim named arguments post-expand, for backwards compatibility + $this->namedExpansionCache[$name] = trim( + $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) ); + } + return $this->namedExpansionCache[$name]; + } + + function getArgument( $name ) { + $text = $this->getNumberedArgument( $name ); + if ( $text === false ) { + $text = $this->getNamedArgument( $name ); + } + return $text; + } + + /** + * Return true if the frame is a template frame + */ + function isTemplate() { + return true; + } +} + +class PPNode_DOM implements PPNode { + var $node; + + function __construct( $node, $xpath = false ) { + $this->node = $node; + } + + function __get( $name ) { + if ( $name == 'xpath' ) { + $this->xpath = new DOMXPath( $this->node->ownerDocument ); + } + return $this->xpath; + } + + function __toString() { + if ( $this->node instanceof DOMNodeList ) { + $s = ''; + foreach ( $this->node as $node ) { + $s .= $node->ownerDocument->saveXML( $node ); + } + } else { + $s = $this->node->ownerDocument->saveXML( $this->node ); + } + return $s; + } + + function getChildren() { + return $this->node->childNodes ? new self( $this->node->childNodes ) : false; + } + + function getFirstChild() { + return $this->node->firstChild ? new self( $this->node->firstChild ) : false; + } + + function getNextSibling() { + return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; + } + + function getChildrenOfType( $type ) { + return new self( $this->xpath->query( $type, $this->node ) ); + } + + function getLength() { + if ( $this->node instanceof DOMNodeList ) { + return $this->node->length; + } else { + return false; + } + } + + function item( $i ) { + $item = $this->node->item( $i ); + return $item ? new self( $item ) : false; + } + + function getName() { + if ( $this->node instanceof DOMNodeList ) { + return '#nodelist'; + } else { + return $this->node->nodeName; + } + } + + /** + * Split a <part> node into an associative array containing: + * name PPNode name + * index String index + * value PPNode value + */ + function splitArg() { + $names = $this->xpath->query( 'name', $this->node ); + $values = $this->xpath->query( 'value', $this->node ); + if ( !$names->length || !$values->length ) { + throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); + } + $name = $names->item( 0 ); + $index = $name->getAttribute( 'index' ); + return array( + 'name' => new self( $name ), + 'index' => $index, + 'value' => new self( $values->item( 0 ) ) ); + } + + /** + * Split an <ext> node into an associative array containing name, attr, inner and close + * All values in the resulting array are PPNodes. Inner and close are optional. + */ + function splitExt() { + $names = $this->xpath->query( 'name', $this->node ); + $attrs = $this->xpath->query( 'attr', $this->node ); + $inners = $this->xpath->query( 'inner', $this->node ); + $closes = $this->xpath->query( 'close', $this->node ); + if ( !$names->length || !$attrs->length ) { + throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); + } + $parts = array( + 'name' => new self( $names->item( 0 ) ), + 'attr' => new self( $attrs->item( 0 ) ) ); + if ( $inners->length ) { + $parts['inner'] = new self( $inners->item( 0 ) ); + } + if ( $closes->length ) { + $parts['close'] = new self( $closes->item( 0 ) ); + } + return $parts; + } + + /** + * Split a <h> node + */ + function splitHeading() { + if ( !$this->nodeName == 'h' ) { + throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); + } + return array( + 'i' => $this->node->getAttribute( 'i' ), + 'level' => $this->node->getAttribute( 'level' ), + 'contents' => $this->getChildren() + ); + } +} diff --git a/includes/Preprocessor_Hash.php b/includes/Preprocessor_Hash.php new file mode 100644 index 00000000..2034278d --- /dev/null +++ b/includes/Preprocessor_Hash.php @@ -0,0 +1,1471 @@ +<?php + +/** + * Differences from DOM schema: + * * attribute nodes are children + * * <h> nodes that aren't at the top are replaced with <possible-h> + */ + +class Preprocessor_Hash implements Preprocessor { + var $parser; + + function __construct( $parser ) { + $this->parser = $parser; + } + + function newFrame() { + return new PPFrame_Hash( $this ); + } + + /** + * Preprocess some wikitext and return the document tree. + * This is the ghost of Parser::replace_variables(). + * + * @param string $text The text to parse + * @param integer flags Bitwise combination of: + * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * included. Default is to assume a direct page view. + * + * The generated DOM tree must depend only on the input text and the flags. + * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. + * + * Any flag added to the $flags parameter here, or any other parameter liable to cause a + * change in the DOM tree for a given text, must be passed through the section identifier + * in the section edit link and thus back to extractSections(). + * + * The output of this function is currently only cached in process memory, but a persistent + * cache may be implemented at a later date which takes further advantage of these strict + * dependency requirements. + * + * @private + */ + function preprocessToObj( $text, $flags = 0 ) { + wfDebug( __METHOD__."\n" . $text . "\n" ); + wfProfileIn( __METHOD__ ); + + $rules = array( + '{' => array( + 'end' => '}', + 'names' => array( + 2 => 'template', + 3 => 'tplarg', + ), + 'min' => 2, + 'max' => 3, + ), + '[' => array( + 'end' => ']', + 'names' => array( 2 => null ), + 'min' => 2, + 'max' => 2, + ) + ); + + $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; + + $xmlishElements = $this->parser->getStripList(); + $enableOnlyinclude = false; + if ( $forInclusion ) { + $ignoredTags = array( 'includeonly', '/includeonly' ); + $ignoredElements = array( 'noinclude' ); + $xmlishElements[] = 'noinclude'; + if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) { + $enableOnlyinclude = true; + } + } else { + $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); + $ignoredElements = array( 'includeonly' ); + $xmlishElements[] = 'includeonly'; + } + $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); + + // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset + $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; + + $stack = new PPDStack_Hash; + + $searchBase = "[{<\n"; + $revText = strrev( $text ); // For fast reverse searches + + $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start + $accum =& $stack->getAccum(); # Current accumulator + $findEquals = false; # True to find equals signs in arguments + $findPipe = false; # True to take notice of pipe characters + $headingIndex = 1; + $inHeading = false; # True if $i is inside a possible heading + $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i + $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude> + $fakeLineStart = true; # Do a line-start run without outputting an LF character + + while ( true ) { + //$this->memCheck(); + + if ( $findOnlyinclude ) { + // Ignore all input up to the next <onlyinclude> + $startPos = strpos( $text, '<onlyinclude>', $i ); + if ( $startPos === false ) { + // Ignored section runs to the end + $accum->addNodeWithText( 'ignore', substr( $text, $i ) ); + break; + } + $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end + $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) ); + $i = $tagEndPos; + $findOnlyinclude = false; + } + + if ( $fakeLineStart ) { + $found = 'line-start'; + $curChar = ''; + } else { + # Find next opening brace, closing brace or pipe + $search = $searchBase; + if ( $stack->top === false ) { + $currentClosing = ''; + } else { + $currentClosing = $stack->top->close; + $search .= $currentClosing; + } + if ( $findPipe ) { + $search .= '|'; + } + if ( $findEquals ) { + // First equals will be for the template + $search .= '='; + } + $rule = null; + # Output literal section, advance input counter + $literalLength = strcspn( $text, $search, $i ); + if ( $literalLength > 0 ) { + $accum->addLiteral( substr( $text, $i, $literalLength ) ); + $i += $literalLength; + } + if ( $i >= strlen( $text ) ) { + if ( $currentClosing == "\n" ) { + // Do a past-the-end run to finish off the heading + $curChar = ''; + $found = 'line-end'; + } else { + # All done + break; + } + } else { + $curChar = $text[$i]; + if ( $curChar == '|' ) { + $found = 'pipe'; + } elseif ( $curChar == '=' ) { + $found = 'equals'; + } elseif ( $curChar == '<' ) { + $found = 'angle'; + } elseif ( $curChar == "\n" ) { + if ( $inHeading ) { + $found = 'line-end'; + } else { + $found = 'line-start'; + } + } elseif ( $curChar == $currentClosing ) { + $found = 'close'; + } elseif ( isset( $rules[$curChar] ) ) { + $found = 'open'; + $rule = $rules[$curChar]; + } else { + # Some versions of PHP have a strcspn which stops on null characters + # Ignore and continue + ++$i; + continue; + } + } + } + + if ( $found == 'angle' ) { + $matches = false; + // Handle </onlyinclude> + if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) { + $findOnlyinclude = true; + continue; + } + + // Determine element name + if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { + // Element name missing or not listed + $accum->addLiteral( '<' ); + ++$i; + continue; + } + // Handle comments + if ( isset( $matches[2] ) && $matches[2] == '!--' ) { + // To avoid leaving blank lines, when a comment is both preceded + // and followed by a newline (ignoring spaces), trim leading and + // trailing spaces and one of the newlines. + + // Find the end + $endPos = strpos( $text, '-->', $i + 4 ); + if ( $endPos === false ) { + // Unclosed comment in input, runs to end + $inner = substr( $text, $i ); + $accum->addNodeWithText( 'comment', $inner ); + $i = strlen( $text ); + } else { + // Search backwards for leading whitespace + $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0; + // Search forwards for trailing whitespace + // $wsEnd will be the position of the last space + $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); + // Eat the line if possible + // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at + // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but + // it's a possible beneficial b/c break. + if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" + && substr( $text, $wsEnd + 1, 1 ) == "\n" ) + { + $startPos = $wsStart; + $endPos = $wsEnd + 1; + // Remove leading whitespace from the end of the accumulator + // Sanity check first though + $wsLength = $i - $wsStart; + if ( $wsLength > 0 + && $accum->lastNode instanceof PPNode_Hash_Text + && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) ) + { + $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength ); + } + // Do a line-start run next time to look for headings after the comment + $fakeLineStart = true; + } else { + // No line to eat, just take the comment itself + $startPos = $i; + $endPos += 2; + } + + if ( $stack->top ) { + $part = $stack->top->getCurrentPart(); + if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) { + // Comments abutting, no change in visual end + $part->commentEnd = $wsEnd; + } else { + $part->visualEnd = $wsStart; + $part->commentEnd = $endPos; + } + } + $i = $endPos + 1; + $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); + $accum->addNodeWithText( 'comment', $inner ); + } + continue; + } + $name = $matches[1]; + $attrStart = $i + strlen( $name ) + 1; + + // Find end of tag + $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); + if ( $tagEndPos === false ) { + // Infinite backtrack + // Disable tag search to prevent worst-case O(N^2) performance + $noMoreGT = true; + $accum->addLiteral( '<' ); + ++$i; + continue; + } + + // Handle ignored tags + if ( in_array( $name, $ignoredTags ) ) { + $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) ); + $i = $tagEndPos + 1; + continue; + } + + $tagStartPos = $i; + if ( $text[$tagEndPos-1] == '/' ) { + // Short end tag + $attrEnd = $tagEndPos - 1; + $inner = null; + $i = $tagEndPos + 1; + $close = null; + } else { + $attrEnd = $tagEndPos; + // Find closing tag + if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) { + $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); + $i = $matches[0][1] + strlen( $matches[0][0] ); + $close = $matches[0][0]; + } else { + // No end tag -- let it run out to the end of the text. + $inner = substr( $text, $tagEndPos + 1 ); + $i = strlen( $text ); + $close = null; + } + } + // <includeonly> and <noinclude> just become <ignore> tags + if ( in_array( $name, $ignoredElements ) ) { + $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) ); + continue; + } + + if ( $attrEnd <= $attrStart ) { + $attr = ''; + } else { + // Note that the attr element contains the whitespace between name and attribute, + // this is necessary for precise reconstruction during pre-save transform. + $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); + } + + $extNode = new PPNode_Hash_Tree( 'ext' ); + $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) ); + $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) ); + if ( $inner !== null ) { + $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) ); + } + if ( $close !== null ) { + $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) ); + } + $accum->addNode( $extNode ); + } + + elseif ( $found == 'line-start' ) { + // Is this the start of a heading? + // Line break belongs before the heading element in any case + if ( $fakeLineStart ) { + $fakeLineStart = false; + } else { + $accum->addLiteral( $curChar ); + $i++; + } + + $count = strspn( $text, '=', $i, 6 ); + if ( $count == 1 && $findEquals ) { + // DWIM: This looks kind of like a name/value separator + // Let's let the equals handler have it and break the potential heading + // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. + } elseif ( $count > 0 ) { + $piece = array( + 'open' => "\n", + 'close' => "\n", + 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ), + 'startPos' => $i, + 'count' => $count ); + $stack->push( $piece ); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + $i += $count; + } + } + + elseif ( $found == 'line-end' ) { + $piece = $stack->top; + // A heading must be open, otherwise \n wouldn't have been in the search list + assert( $piece->open == "\n" ); + $part = $piece->getCurrentPart(); + // Search back through the input to see if it has a proper close + // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient + $wsLength = strspn( $revText, " \t", strlen( $text ) - $i ); + $searchStart = $i - $wsLength; + if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { + // Comment found at line end + // Search for equals signs before the comment + $searchStart = $part->visualEnd; + $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart ); + } + $count = $piece->count; + $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart ); + if ( $equalsLength > 0 ) { + if ( $i - $equalsLength == $piece->startPos ) { + // This is just a single string of equals signs on its own line + // Replicate the doHeadings behaviour /={count}(.+)={count}/ + // First find out how many equals signs there really are (don't stop at 6) + $count = $equalsLength; + if ( $count < 3 ) { + $count = 0; + } else { + $count = min( 6, intval( ( $count - 1 ) / 2 ) ); + } + } else { + $count = min( $equalsLength, $count ); + } + if ( $count > 0 ) { + // Normal match, output <h> + $element = new PPNode_Hash_Tree( 'possible-h' ); + $element->addChild( new PPNode_Hash_Attr( 'level', $count ) ); + $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) ); + $element->lastChild->nextSibling = $accum->firstNode; + $element->lastChild = $accum->lastNode; + } else { + // Single equals sign on its own line, count=0 + $element = $accum; + } + } else { + // No match, no <h>, just pass down the inner text + $element = $accum; + } + // Unwind the stack + $stack->pop(); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + + // Append the result to the enclosing accumulator + if ( $element instanceof PPNode ) { + $accum->addNode( $element ); + } else { + $accum->addAccum( $element ); + } + // Note that we do NOT increment the input pointer. + // This is because the closing linebreak could be the opening linebreak of + // another heading. Infinite loops are avoided because the next iteration MUST + // hit the heading open case above, which unconditionally increments the + // input pointer. + } + + elseif ( $found == 'open' ) { + # count opening brace characters + $count = strspn( $text, $curChar, $i ); + + # we need to add to stack only if opening brace count is enough for one of the rules + if ( $count >= $rule['min'] ) { + # Add it to the stack + $piece = array( + 'open' => $curChar, + 'close' => $rule['end'], + 'count' => $count, + 'lineStart' => ($i > 0 && $text[$i-1] == "\n"), + ); + + $stack->push( $piece ); + $accum =& $stack->getAccum(); + extract( $stack->getFlags() ); + } else { + # Add literal brace(s) + $accum->addLiteral( str_repeat( $curChar, $count ) ); + } + $i += $count; + } + + elseif ( $found == 'close' ) { + $piece = $stack->top; + # lets check if there are enough characters for closing brace + $maxCount = $piece->count; + $count = strspn( $text, $curChar, $i, $maxCount ); + + # check for maximum matching characters (if there are 5 closing + # characters, we will probably need only 3 - depending on the rules) + $matchingCount = 0; + $rule = $rules[$piece->open]; + if ( $count > $rule['max'] ) { + # The specified maximum exists in the callback array, unless the caller + # has made an error + $matchingCount = $rule['max']; + } else { + # Count is less than the maximum + # Skip any gaps in the callback array to find the true largest match + # Need to use array_key_exists not isset because the callback can be null + $matchingCount = $count; + while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { + --$matchingCount; + } + } + + if ($matchingCount <= 0) { + # No matching element found in callback array + # Output a literal closing brace and continue + $accum->addLiteral( str_repeat( $curChar, $count ) ); + $i += $count; + continue; + } + $name = $rule['names'][$matchingCount]; + if ( $name === null ) { + // No element, just literal text + $element = $piece->breakSyntax( $matchingCount ); + $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) ); + } else { + # Create XML element + # Note: $parts is already XML, does not need to be encoded further + $parts = $piece->parts; + $titleAccum = $parts[0]->out; + unset( $parts[0] ); + + $element = new PPNode_Hash_Tree( $name ); + + # The invocation is at the start of the line if lineStart is set in + # the stack, and all opening brackets are used up. + if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { + $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) ); + } + $titleNode = new PPNode_Hash_Tree( 'title' ); + $titleNode->firstChild = $titleAccum->firstNode; + $titleNode->lastChild = $titleAccum->lastNode; + $element->addChild( $titleNode ); + $argIndex = 1; + foreach ( $parts as $partIndex => $part ) { + if ( isset( $part->eqpos ) ) { + // Find equals + $lastNode = false; + for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) { + if ( $node === $part->eqpos ) { + break; + } + $lastNode = $node; + } + if ( !$node ) { + throw new MWException( __METHOD__. ': eqpos not found' ); + } + if ( $node->name !== 'equals' ) { + throw new MWException( __METHOD__ .': eqpos is not equals' ); + } + $equalsNode = $node; + + // Construct name node + $nameNode = new PPNode_Hash_Tree( 'name' ); + if ( $lastNode !== false ) { + $lastNode->nextSibling = false; + $nameNode->firstChild = $part->out->firstNode; + $nameNode->lastChild = $lastNode; + } + + // Construct value node + $valueNode = new PPNode_Hash_Tree( 'value' ); + if ( $equalsNode->nextSibling !== false ) { + $valueNode->firstChild = $equalsNode->nextSibling; + $valueNode->lastChild = $part->out->lastNode; + } + $partNode = new PPNode_Hash_Tree( 'part' ); + $partNode->addChild( $nameNode ); + $partNode->addChild( $equalsNode->firstChild ); + $partNode->addChild( $valueNode ); + $element->addChild( $partNode ); + } else { + $partNode = new PPNode_Hash_Tree( 'part' ); + $nameNode = new PPNode_Hash_Tree( 'name' ); + $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) ); + $valueNode = new PPNode_Hash_Tree( 'value' ); + $valueNode->firstChild = $part->out->firstNode; + $valueNode->lastChild = $part->out->lastNode; + $partNode->addChild( $nameNode ); + $partNode->addChild( $valueNode ); + $element->addChild( $partNode ); + } + } + } + + # Advance input pointer + $i += $matchingCount; + + # Unwind the stack + $stack->pop(); + $accum =& $stack->getAccum(); + + # Re-add the old stack element if it still has unmatched opening characters remaining + if ($matchingCount < $piece->count) { + $piece->parts = array( new PPDPart_Hash ); + $piece->count -= $matchingCount; + # do we still qualify for any callback with remaining count? + $names = $rules[$piece->open]['names']; + $skippedBraces = 0; + $enclosingAccum =& $accum; + while ( $piece->count ) { + if ( array_key_exists( $piece->count, $names ) ) { + $stack->push( $piece ); + $accum =& $stack->getAccum(); + break; + } + --$piece->count; + $skippedBraces ++; + } + $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) ); + } + + extract( $stack->getFlags() ); + + # Add XML element to the enclosing accumulator + if ( $element instanceof PPNode ) { + $accum->addNode( $element ); + } else { + $accum->addAccum( $element ); + } + } + + elseif ( $found == 'pipe' ) { + $findEquals = true; // shortcut for getFlags() + $stack->addPart(); + $accum =& $stack->getAccum(); + ++$i; + } + + elseif ( $found == 'equals' ) { + $findEquals = false; // shortcut for getFlags() + $accum->addNodeWithText( 'equals', '=' ); + $stack->getCurrentPart()->eqpos = $accum->lastNode; + ++$i; + } + } + + # Output any remaining unclosed brackets + foreach ( $stack->stack as $piece ) { + $stack->rootAccum->addAccum( $piece->breakSyntax() ); + } + + # Enable top-level headings + for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) { + if ( isset( $node->name ) && $node->name === 'possible-h' ) { + $node->name = 'h'; + } + } + + $rootNode = new PPNode_Hash_Tree( 'root' ); + $rootNode->firstChild = $stack->rootAccum->firstNode; + $rootNode->lastChild = $stack->rootAccum->lastNode; + wfProfileOut( __METHOD__ ); + return $rootNode; + } +} + +/** + * Stack class to help Preprocessor::preprocessToObj() + */ +class PPDStack_Hash extends PPDStack { + function __construct() { + $this->elementClass = 'PPDStackElement_Hash'; + parent::__construct(); + $this->rootAccum = new PPDAccum_Hash; + } +} + +class PPDStackElement_Hash extends PPDStackElement { + function __construct( $data = array() ) { + $this->partClass = 'PPDPart_Hash'; + parent::__construct( $data ); + } + + /** + * Get the accumulator that would result if the close is not found. + */ + function breakSyntax( $openingCount = false ) { + if ( $this->open == "\n" ) { + $accum = $this->parts[0]->out; + } else { + if ( $openingCount === false ) { + $openingCount = $this->count; + } + $accum = new PPDAccum_Hash; + $accum->addLiteral( str_repeat( $this->open, $openingCount ) ); + $first = true; + foreach ( $this->parts as $part ) { + if ( $first ) { + $first = false; + } else { + $accum->addLiteral( '|' ); + } + $accum->addAccum( $part->out ); + } + } + return $accum; + } +} + +class PPDPart_Hash extends PPDPart { + function __construct( $out = '' ) { + $accum = new PPDAccum_Hash; + if ( $out !== '' ) { + $accum->addLiteral( $out ); + } + parent::__construct( $accum ); + } +} + +class PPDAccum_Hash { + var $firstNode, $lastNode; + + function __construct() { + $this->firstNode = $this->lastNode = false; + } + + /** + * Append a string literal + */ + function addLiteral( $s ) { + if ( $this->lastNode === false ) { + $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s ); + } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) { + $this->lastNode->value .= $s; + } else { + $this->lastNode->nextSibling = new PPNode_Hash_Text( $s ); + $this->lastNode = $this->lastNode->nextSibling; + } + } + + /** + * Append a PPNode + */ + function addNode( PPNode $node ) { + if ( $this->lastNode === false ) { + $this->firstNode = $this->lastNode = $node; + } else { + $this->lastNode->nextSibling = $node; + $this->lastNode = $node; + } + } + + /** + * Append a tree node with text contents + */ + function addNodeWithText( $name, $value ) { + $node = PPNode_Hash_Tree::newWithText( $name, $value ); + $this->addNode( $node ); + } + + /** + * Append a PPAccum_Hash + * Takes over ownership of the nodes in the source argument. These nodes may + * subsequently be modified, especially nextSibling. + */ + function addAccum( $accum ) { + if ( $accum->lastNode === false ) { + // nothing to add + } elseif ( $this->lastNode === false ) { + $this->firstNode = $accum->firstNode; + $this->lastNode = $accum->lastNode; + } else { + $this->lastNode->nextSibling = $accum->firstNode; + $this->lastNode = $accum->lastNode; + } + } +} + +/** + * An expansion frame, used as a context to expand the result of preprocessToObj() + */ +class PPFrame_Hash implements PPFrame { + var $preprocessor, $parser, $title; + var $titleCache; + + /** + * Hashtable listing templates which are disallowed for expansion in this frame, + * having been encountered previously in parent frames. + */ + var $loopCheckHash; + + /** + * Recursion depth of this frame, top = 0 + */ + var $depth; + + + /** + * Construct a new preprocessor frame. + * @param Preprocessor $preprocessor The parent preprocessor + */ + function __construct( $preprocessor ) { + $this->preprocessor = $preprocessor; + $this->parser = $preprocessor->parser; + $this->title = $this->parser->mTitle; + $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); + $this->loopCheckHash = array(); + $this->depth = 0; + } + + /** + * Create a new child frame + * $args is optionally a multi-root PPNode or array containing the template arguments + */ + function newChild( $args = false, $title = false ) { + $namedArgs = array(); + $numberedArgs = array(); + if ( $title === false ) { + $title = $this->title; + } + if ( $args !== false ) { + $xpath = false; + if ( $args instanceof PPNode_Hash_Array ) { + $args = $args->value; + } elseif ( !is_array( $args ) ) { + throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' ); + } + foreach ( $args as $arg ) { + $bits = $arg->splitArg(); + if ( $bits['index'] !== '' ) { + // Numbered parameter + $numberedArgs[$bits['index']] = $bits['value']; + unset( $namedArgs[$bits['index']] ); + } else { + // Named parameter + $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); + $namedArgs[$name] = $bits['value']; + unset( $numberedArgs[$name] ); + } + } + } + return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); + } + + function expand( $root, $flags = 0 ) { + if ( is_string( $root ) ) { + return $root; + } + + if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount ) + { + return '<span class="error">Node-count limit exceeded</span>'; + } + + $outStack = array( '', '' ); + $iteratorStack = array( false, $root ); + $indexStack = array( 0, 0 ); + + while ( count( $iteratorStack ) > 1 ) { + $level = count( $outStack ) - 1; + $iteratorNode =& $iteratorStack[ $level ]; + $out =& $outStack[$level]; + $index =& $indexStack[$level]; + + if ( is_array( $iteratorNode ) ) { + if ( $index >= count( $iteratorNode ) ) { + // All done with this iterator + $iteratorStack[$level] = false; + $contextNode = false; + } else { + $contextNode = $iteratorNode[$index]; + $index++; + } + } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) { + if ( $index >= $iteratorNode->getLength() ) { + // All done with this iterator + $iteratorStack[$level] = false; + $contextNode = false; + } else { + $contextNode = $iteratorNode->item( $index ); + $index++; + } + } else { + // Copy to $contextNode and then delete from iterator stack, + // because this is not an iterator but we do have to execute it once + $contextNode = $iteratorStack[$level]; + $iteratorStack[$level] = false; + } + + $newIterator = false; + + if ( $contextNode === false ) { + // nothing to do + } elseif ( is_string( $contextNode ) ) { + $out .= $contextNode; + } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) { + $newIterator = $contextNode; + } elseif ( $contextNode instanceof PPNode_Hash_Attr ) { + // No output + } elseif ( $contextNode instanceof PPNode_Hash_Text ) { + $out .= $contextNode->value; + } elseif ( $contextNode instanceof PPNode_Hash_Tree ) { + if ( $contextNode->name == 'template' ) { + # Double-brace expansion + $bits = $contextNode->splitTemplate(); + if ( $flags & self::NO_TEMPLATES ) { + $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] ); + } else { + $ret = $this->parser->braceSubstitution( $bits, $this ); + if ( isset( $ret['object'] ) ) { + $newIterator = $ret['object']; + } else { + $out .= $ret['text']; + } + } + } elseif ( $contextNode->name == 'tplarg' ) { + # Triple-brace expansion + $bits = $contextNode->splitTemplate(); + if ( $flags & self::NO_ARGS ) { + $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] ); + } else { + $ret = $this->parser->argSubstitution( $bits, $this ); + if ( isset( $ret['object'] ) ) { + $newIterator = $ret['object']; + } else { + $out .= $ret['text']; + } + } + } elseif ( $contextNode->name == 'comment' ) { + # HTML-style comment + # Remove it in HTML, pre+remove and STRIP_COMMENTS modes + if ( $this->parser->ot['html'] + || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) + || ( $flags & self::STRIP_COMMENTS ) ) + { + $out .= ''; + } + # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result + # Not in RECOVER_COMMENTS mode (extractSections) though + elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) { + $out .= $this->parser->insertStripItem( $contextNode->firstChild->value ); + } + # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove + else { + $out .= $contextNode->firstChild->value; + } + } elseif ( $contextNode->name == 'ignore' ) { + # Output suppression used by <includeonly> etc. + # OT_WIKI will only respect <ignore> in substed templates. + # The other output types respect it unless NO_IGNORE is set. + # extractSections() sets NO_IGNORE and so never respects it. + if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) { + $out .= $contextNode->firstChild->value; + } else { + //$out .= ''; + } + } elseif ( $contextNode->name == 'ext' ) { + # Extension tag + $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null ); + $out .= $this->parser->extensionSubstitution( $bits, $this ); + } elseif ( $contextNode->name == 'h' ) { + # Heading + if ( $this->parser->ot['html'] ) { + # Expand immediately and insert heading index marker + $s = ''; + for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) { + $s .= $this->expand( $node, $flags ); + } + + $bits = $contextNode->splitHeading(); + $titleText = $this->title->getPrefixedDBkey(); + $this->parser->mHeadings[] = array( $titleText, $bits['i'] ); + $serial = count( $this->parser->mHeadings ) - 1; + $marker = "{$this->parser->mUniqPrefix}-h-$serial-{$this->parser->mMarkerSuffix}"; + $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] ); + $this->parser->mStripState->general->setPair( $marker, '' ); + $out .= $s; + } else { + # Expand in virtual stack + $newIterator = $contextNode->getChildren(); + } + } else { + # Generic recursive expansion + $newIterator = $contextNode->getChildren(); + } + } else { + throw new MWException( __METHOD__.': Invalid parameter type' ); + } + + if ( $newIterator !== false ) { + $outStack[] = ''; + $iteratorStack[] = $newIterator; + $indexStack[] = 0; + } elseif ( $iteratorStack[$level] === false ) { + // Return accumulated value to parent + // With tail recursion + while ( $iteratorStack[$level] === false && $level > 0 ) { + $outStack[$level - 1] .= $out; + array_pop( $outStack ); + array_pop( $iteratorStack ); + array_pop( $indexStack ); + $level--; + } + } + } + return $outStack[0]; + } + + function implodeWithFlags( $sep, $flags /*, ... */ ) { + $args = array_slice( func_get_args(), 2 ); + + $first = true; + $s = ''; + foreach ( $args as $root ) { + if ( $root instanceof PPNode_Hash_Array ) { + $root = $root->value; + } + if ( !is_array( $root ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $s .= $sep; + } + $s .= $this->expand( $node, $flags ); + } + } + return $s; + } + + /** + * Implode with no flags specified + * This previously called implodeWithFlags but has now been inlined to reduce stack depth + */ + function implode( $sep /*, ... */ ) { + $args = array_slice( func_get_args(), 1 ); + + $first = true; + $s = ''; + foreach ( $args as $root ) { + if ( $root instanceof PPNode_Hash_Array ) { + $root = $root->value; + } + if ( !is_array( $root ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $s .= $sep; + } + $s .= $this->expand( $node ); + } + } + return $s; + } + + /** + * Makes an object that, when expand()ed, will be the same as one obtained + * with implode() + */ + function virtualImplode( $sep /*, ... */ ) { + $args = array_slice( func_get_args(), 1 ); + $out = array(); + $first = true; + + foreach ( $args as $root ) { + if ( $root instanceof PPNode_Hash_Array ) { + $root = $root->value; + } + if ( !is_array( $root ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $out[] = $sep; + } + $out[] = $node; + } + } + return new PPNode_Hash_Array( $out ); + } + + /** + * Virtual implode with brackets + */ + function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { + $args = array_slice( func_get_args(), 3 ); + $out = array( $start ); + $first = true; + + foreach ( $args as $root ) { + if ( $root instanceof PPNode_Hash_Array ) { + $root = $root->value; + } + if ( !is_array( $root ) ) { + $root = array( $root ); + } + foreach ( $root as $node ) { + if ( $first ) { + $first = false; + } else { + $out[] = $sep; + } + $out[] = $node; + } + } + $out[] = $end; + return new PPNode_Hash_Array( $out ); + } + + function __toString() { + return 'frame{}'; + } + + function getPDBK( $level = false ) { + if ( $level === false ) { + return $this->title->getPrefixedDBkey(); + } else { + return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; + } + } + + /** + * Returns true if there are no arguments in this frame + */ + function isEmpty() { + return true; + } + + function getArgument( $name ) { + return false; + } + + /** + * Returns true if the infinite loop check is OK, false if a loop is detected + */ + function loopCheck( $title ) { + return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); + } + + /** + * Return true if the frame is a template frame + */ + function isTemplate() { + return false; + } +} + +/** + * Expansion frame with template arguments + */ +class PPTemplateFrame_Hash extends PPFrame_Hash { + var $numberedArgs, $namedArgs, $parent; + var $numberedExpansionCache, $namedExpansionCache; + + function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { + $this->preprocessor = $preprocessor; + $this->parser = $preprocessor->parser; + $this->parent = $parent; + $this->numberedArgs = $numberedArgs; + $this->namedArgs = $namedArgs; + $this->title = $title; + $pdbk = $title ? $title->getPrefixedDBkey() : false; + $this->titleCache = $parent->titleCache; + $this->titleCache[] = $pdbk; + $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; + if ( $pdbk !== false ) { + $this->loopCheckHash[$pdbk] = true; + } + $this->depth = $parent->depth + 1; + $this->numberedExpansionCache = $this->namedExpansionCache = array(); + } + + function __toString() { + $s = 'tplframe{'; + $first = true; + $args = $this->numberedArgs + $this->namedArgs; + foreach ( $args as $name => $value ) { + if ( $first ) { + $first = false; + } else { + $s .= ', '; + } + $s .= "\"$name\":\"" . + str_replace( '"', '\\"', $value->__toString() ) . '"'; + } + $s .= '}'; + return $s; + } + /** + * Returns true if there are no arguments in this frame + */ + function isEmpty() { + return !count( $this->numberedArgs ) && !count( $this->namedArgs ); + } + + function getNumberedArgument( $index ) { + if ( !isset( $this->numberedArgs[$index] ) ) { + return false; + } + if ( !isset( $this->numberedExpansionCache[$index] ) ) { + # No trimming for unnamed arguments + $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS ); + } + return $this->numberedExpansionCache[$index]; + } + + function getNamedArgument( $name ) { + if ( !isset( $this->namedArgs[$name] ) ) { + return false; + } + if ( !isset( $this->namedExpansionCache[$name] ) ) { + # Trim named arguments post-expand, for backwards compatibility + $this->namedExpansionCache[$name] = trim( + $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) ); + } + return $this->namedExpansionCache[$name]; + } + + function getArgument( $name ) { + $text = $this->getNumberedArgument( $name ); + if ( $text === false ) { + $text = $this->getNamedArgument( $name ); + } + return $text; + } + + /** + * Return true if the frame is a template frame + */ + function isTemplate() { + return true; + } +} + +class PPNode_Hash_Tree implements PPNode { + var $name, $firstChild, $lastChild, $nextSibling; + + function __construct( $name ) { + $this->name = $name; + $this->firstChild = $this->lastChild = $this->nextSibling = false; + } + + function __toString() { + $inner = ''; + $attribs = ''; + for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) { + if ( $node instanceof PPNode_Hash_Attr ) { + $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"'; + } else { + $inner .= $node->__toString(); + } + } + if ( $inner === '' ) { + return "<{$this->name}$attribs/>"; + } else { + return "<{$this->name}$attribs>$inner</{$this->name}>"; + } + } + + function newWithText( $name, $text ) { + $obj = new self( $name ); + $obj->addChild( new PPNode_Hash_Text( $text ) ); + return $obj; + } + + function addChild( $node ) { + if ( $this->lastChild === false ) { + $this->firstChild = $this->lastChild = $node; + } else { + $this->lastChild->nextSibling = $node; + $this->lastChild = $node; + } + } + + function getChildren() { + $children = array(); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + $children[] = $child; + } + return new PPNode_Hash_Array( $children ); + } + + function getFirstChild() { + return $this->firstChild; + } + + function getNextSibling() { + return $this->nextSibling; + } + + function getChildrenOfType( $name ) { + $children = array(); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + if ( isset( $child->name ) && $child->name === $name ) { + $children[] = $name; + } + } + return $children; + } + + function getLength() { return false; } + function item( $i ) { return false; } + + function getName() { + return $this->name; + } + + /** + * Split a <part> node into an associative array containing: + * name PPNode name + * index String index + * value PPNode value + */ + function splitArg() { + $bits = array(); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + if ( !isset( $child->name ) ) { + continue; + } + if ( $child->name === 'name' ) { + $bits['name'] = $child; + if ( $child->firstChild instanceof PPNode_Hash_Attr + && $child->firstChild->name === 'index' ) + { + $bits['index'] = $child->firstChild->value; + } + } elseif ( $child->name === 'value' ) { + $bits['value'] = $child; + } + } + + if ( !isset( $bits['name'] ) ) { + throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); + } + if ( !isset( $bits['index'] ) ) { + $bits['index'] = ''; + } + return $bits; + } + + /** + * Split an <ext> node into an associative array containing name, attr, inner and close + * All values in the resulting array are PPNodes. Inner and close are optional. + */ + function splitExt() { + $bits = array(); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + if ( !isset( $child->name ) ) { + continue; + } + if ( $child->name == 'name' ) { + $bits['name'] = $child; + } elseif ( $child->name == 'attr' ) { + $bits['attr'] = $child; + } elseif ( $child->name == 'inner' ) { + $bits['inner'] = $child; + } elseif ( $child->name == 'close' ) { + $bits['close'] = $child; + } + } + if ( !isset( $bits['name'] ) ) { + throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); + } + return $bits; + } + + /** + * Split an <h> node + */ + function splitHeading() { + if ( $this->name !== 'h' ) { + throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); + } + $bits = array(); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + if ( !isset( $child->name ) ) { + continue; + } + if ( $child->name == 'i' ) { + $bits['i'] = $child->value; + } elseif ( $child->name == 'level' ) { + $bits['level'] = $child->value; + } + } + if ( !isset( $bits['i'] ) ) { + throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); + } + return $bits; + } + + /** + * Split a <template> or <tplarg> node + */ + function splitTemplate() { + wfDebug( 'Template: ' . var_export( $this, true ) ); + $parts = array(); + $bits = array( 'lineStart' => '' ); + for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { + wfDebug( 'Child: ' . var_export( $child, true ) ); + if ( !isset( $child->name ) ) { + continue; + } + if ( $child->name == 'title' ) { + $bits['title'] = $child; + } + if ( $child->name == 'part' ) { + $parts[] = $child; + } + if ( $child->name == 'lineStart' ) { + $bits['lineStart'] = '1'; + } + } + if ( !isset( $bits['title'] ) ) { + throw new MWException( 'Invalid node passed to ' . __METHOD__ ); + } + $bits['parts'] = new PPNode_Hash_Array( $parts ); + return $bits; + } +} + +class PPNode_Hash_Text implements PPNode { + var $value, $nextSibling; + + function __construct( $value ) { + if ( is_object( $value ) ) { + throw new MWException( __CLASS__ . ' given object instead of string' ); + } + $this->value = $value; + } + + function __toString() { + return htmlspecialchars( $this->value ); + } + + function getNextSibling() { + return $this->nextSibling; + } + + function getChildren() { return false; } + function getFirstChild() { return false; } + function getChildrenOfType( $name ) { return false; } + function getLength() { return false; } + function item( $i ) { return false; } + function getName() { return '#text'; } + function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } +} + +class PPNode_Hash_Array implements PPNode { + var $value, $nextSibling; + + function __construct( $value ) { + $this->value = $value; + } + + function __toString() { + return var_export( $this, true ); + } + + function getLength() { + return count( $this->value ); + } + + function item( $i ) { + return $this->value[$i]; + } + + function getName() { return '#nodelist'; } + + function getNextSibling() { + return $this->nextSibling; + } + + function getChildren() { return false; } + function getFirstChild() { return false; } + function getChildrenOfType( $name ) { return false; } + function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } +} + +class PPNode_Hash_Attr implements PPNode { + var $name, $value, $nextSibling; + + function __construct( $name, $value ) { + $this->name = $name; + $this->value = $value; + } + + function __toString() { + return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>"; + } + + function getName() { + return $this->name; + } + + function getNextSibling() { + return $this->nextSibling; + } + + function getChildren() { return false; } + function getFirstChild() { return false; } + function getChildrenOfType( $name ) { return false; } + function getLength() { return false; } + function item( $i ) { return false; } + function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } + function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } +} + diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index b07f2517..20ab99c0 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -72,10 +72,14 @@ class ProfilerSimple extends Profiler { $message = "Profile section ended by close(): {$ofname}"; $functionname = $ofname; $this->debug( "$message\n" ); + $this->mCollated[$message] = array( + 'real' => 0.0, 'count' => 1); } elseif ($ofname != $functionname) { $message = "Profiling error: in({$ofname}), out($functionname)"; $this->debug( "$message\n" ); + $this->mCollated[$message] = array( + 'real' => 0.0, 'count' => 1); } $entry =& $this->mCollated[$functionname]; $elapsedcpu = $this->getCpuTime() - $octime; diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index c249ec12..a5ff4f3e 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -28,17 +28,20 @@ class ProtectionForm { var $mReason = ''; var $mCascade = false; var $mExpiry = null; + var $mPermErrors = array(); + var $mApplicableTypes = array(); function __construct( &$article ) { global $wgRequest, $wgUser; global $wgRestrictionTypes, $wgRestrictionLevels; $this->mArticle =& $article; $this->mTitle =& $article->mTitle; + $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create'); if( $this->mTitle ) { $this->mTitle->loadRestrictions(); - foreach( $wgRestrictionTypes as $action ) { + foreach( $this->mApplicableTypes as $action ) { // Fixme: this form currently requires individual selections, // but the db allows multiples separated by commas. $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); @@ -56,7 +59,7 @@ class ProtectionForm { } // The form will be available in read-only to show levels. - $this->disabled = !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $wgUser->isBlocked(); + $this->disabled = wfReadOnly() || ($this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser)) != array(); $this->disabledAttrib = $this->disabled ? array( 'disabled' => 'disabled' ) : array(); @@ -66,7 +69,7 @@ class ProtectionForm { $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); $this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' ); - foreach( $wgRestrictionTypes as $action ) { + foreach( $this->mApplicableTypes as $action ) { $val = $wgRequest->getVal( "mwProtect-level-$action" ); if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { $this->mRestrictions[$action] = $val; @@ -94,7 +97,6 @@ class ProtectionForm { $wgOut->setRobotpolicy( 'noindex,nofollow' ); if( is_null( $this->mTitle ) || - !$this->mTitle->exists() || $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $wgOut->showFatalError( wfMsg( 'badarticleerror' ) ); return; @@ -114,9 +116,7 @@ class ProtectionForm { $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; } - $notice = wfMsgExt( 'protect-cascadeon', array('parsemag'), count($cascadeSources) ) . "\r\n$titles"; - - $wgOut->addWikiText( $notice ); + $wgOut->wrapWikiMsg( "$1\n$titles", array( 'protect-cascadeon', count($cascadeSources) ) ); } $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) ); @@ -125,22 +125,14 @@ class ProtectionForm { # Show an appropriate message if the user isn't allowed or able to change # the protection settings at this time if( $this->disabled ) { - if( $wgUser->isAllowed( 'protect' ) ) { - if( $wgUser->isBlocked() ) { - # Blocked - $message = 'protect-locked-blocked'; - } else { - # Database lock - $message = 'protect-locked-dblock'; - } - } else { - # Permission error - $message = 'protect-locked-access'; + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + } elseif( $this->mPermErrors ) { + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $this->mPermErrors ) ); } } else { - $message = 'protect-text'; + $wgOut->addWikiMsg( 'protect-text', $this->mTitle->getPrefixedText() ); } - $wgOut->addWikiText( wfMsg( $message, wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) ); $wgOut->addHTML( $this->buildForm() ); @@ -185,7 +177,22 @@ class ProtectionForm { } - $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry ); + # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied + # to a semi-protected page. + global $wgGroupPermissions; + + $edit_restriction = $this->mRestrictions['edit']; + + if ($this->mCascade && ($edit_restriction != 'protect') && + !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) ) + $this->mCascade = false; + + if ($this->mTitle->exists()) { + $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry ); + } else { + $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $this->mReason, $expiry ); + } + if( !$ok ) { throw new FatalError( "Unknown error at restriction save time." ); } @@ -222,6 +229,7 @@ class ProtectionForm { $out .= "<table id='mwProtectSet'>"; $out .= "<tbody>"; $out .= "<tr>\n"; + foreach( $this->mRestrictions as $action => $required ) { /* Not all languages have V_x <-> N_x relation */ $out .= "<th>" . wfMsgHtml( 'restriction-' . $action ) . "</th>\n"; @@ -244,7 +252,7 @@ class ProtectionForm { $out .= "<tbody>\n"; global $wgEnableCascadingProtection; - if( $wgEnableCascadingProtection ) + if( $wgEnableCascadingProtection && $this->mTitle->exists() ) $out .= '<tr><td></td><td>' . $this->buildCascadeInput() . "</td></tr>\n"; $out .= $this->buildExpiryInput(); @@ -311,6 +319,7 @@ class ProtectionForm { '</td><td>' . wfElement( 'input', array( 'size' => 60, + 'maxlength' => 255, 'name' => $id, 'id' => $id, 'value' => $this->mReason ) ); @@ -359,12 +368,12 @@ class ProtectionForm { $script = 'var wgCascadeableLevels='; $CascadeableLevels = array(); foreach( $wgRestrictionLevels as $key ) { - if ( isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect'] ) { + if ( (isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect']) || $key == 'protect' ) { $CascadeableLevels[]="'" . wfEscapeJsString($key) . "'"; } } $script .= "[" . implode(',',$CascadeableLevels) . "];\n"; - $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")'; + $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")'; return '<script type="text/javascript">' . $script . '</script>'; } @@ -383,4 +392,4 @@ class ProtectionForm { $logViewer->showList( $out ); } -}
\ No newline at end of file +} diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 06710b6d..eb4e71bf 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -308,20 +308,18 @@ class QueryPage { if( $tRow ) { $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true ); - $cacheNotice = wfMsg( 'perfcachedts', $updated ); $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp ); $wgOut->addInlineScript( "var dataCacheTime = '{$tRow->qci_timestamp}';" ); + $wgOut->addWikiMsg( 'perfcachedts', $updated ); } else { - $cacheNotice = wfMsg( 'perfcached' ); + $wgOut->addWikiMsg( 'perfcached' ); } - - $wgOut->addWikiText( $cacheNotice ); # If updates on this page have been disabled, let the user know # that the data set won't be refreshed for now global $wgDisableQueryPageUpdate; if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) { - $wgOut->addWikiText( wfMsg( 'querypage-no-updates' ) ); + $wgOut->addWikiMsg( 'querypage-no-updates' ); } } @@ -443,9 +441,8 @@ class QueryPage { /** * Do any necessary preprocessing of the result object. - * You should pass this by reference: &$db , &$res [although probably no longer necessary in PHP5] */ - function preprocessResults( &$db, &$res ) {} + function preprocessResults( $db, $res ) {} /** * Similar to above, but packaging in a syndicated feed instead of a web page diff --git a/includes/RawPage.php b/includes/RawPage.php index 9df94e50..909c300b 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -15,12 +15,12 @@ */ class RawPage { var $mArticle, $mTitle, $mRequest; - var $mOldId, $mGen, $mCharset; + var $mOldId, $mGen, $mCharset, $mSection; var $mSmaxage, $mMaxage; var $mContentType, $mExpandTemplates; function __construct( &$article, $request = false ) { - global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType; + global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgForcedRawSMaxage, $wgGroupPermissions; $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); $this->mArticle =& $article; @@ -35,10 +35,14 @@ class RawPage { $ctype = $this->mRequest->getVal( 'ctype' ); $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); + $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; $this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' ); + $this->mSection = $this->mRequest->getIntOrNull( 'section' ); + $oldid = $this->mRequest->getInt( 'oldid' ); + switch ( $wgRequest->getText( 'direction' ) ) { case 'next': # output next revision, or nothing if there isn't one @@ -78,12 +82,23 @@ class RawPage { $this->mGen = false; } $this->mCharset = $wgInputEncoding; - $this->mSmaxage = intval( $smaxage ); + + # Force caching for CSS and JS raw content, default: 5 minutes + if (is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType)) { + $this->mSmaxage = intval($wgForcedRawSMaxage); + } else { + $this->mSmaxage = intval( $smaxage ); + } $this->mMaxage = $maxage; - // Output may contain user-specific data; vary for open sessions - $this->mPrivateCache = ( $this->mSmaxage == 0 ) || - ( session_id() != '' ); + # Output may contain user-specific data; + # vary generated content for open sessions and private wikis + if ($this->mGen or !$wgGroupPermissions['*']['read']) { + $this->mPrivateCache = ( $this->mSmaxage == 0 ) || + ( session_id() != '' ); + } else { + $this->mPrivateCache = false; + } if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; @@ -111,8 +126,7 @@ class RawPage { $url = $_SERVER['PHP_SELF']; } - $ua = @$_SERVER['HTTP_USER_AGENT']; - if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) { + if( strcmp( $wgScript, $url ) ) { # Internet Explorer will ignore the Content-Type header if it # thinks it sees a file extension it recognizes. Make sure that # all raw requests are done through the script node, which will @@ -177,7 +191,12 @@ class RawPage { if ( $rev ) { $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); header( "Last-modified: $lastmod" ); - $text = $rev->getText(); + + if ( !is_null($this->mSection ) ) { + global $wgParser; + $text = $wgParser->getSection ( $rev->getText(), $this->mSection ); + } else + $text = $rev->getText(); $found = true; } } diff --git a/includes/RecentChange.php b/includes/RecentChange.php index 79f32d0c..750404a9 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -221,8 +221,7 @@ class RecentChange if( $wgUseEnotif ) { # this would be better as an extension hook global $wgUser; - include_once( "UserMailer.php" ); - $enotif = new EmailNotification(); + $enotif = new EmailNotification; $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); $enotif->notifyOnPageChange( $wgUser, $title, $this->mAttribs['rc_timestamp'], @@ -259,14 +258,9 @@ class RecentChange # Makes an entry in the database corresponding to an edit public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, - $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, + $oldId, $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0) { - - if ( $bot === 'default' ) { - $bot = $user->isAllowed( 'bot' ); - } - if ( !$ip ) { $ip = wfGetIP(); if ( !$ip ) { @@ -313,7 +307,7 @@ class RecentChange * Note: the title object must be loaded with the new id using resetArticleID() * @todo Document parameters and return */ - public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default', + public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot, $ip='', $size = 0, $newId = 0 ) { if ( !$ip ) { @@ -322,9 +316,6 @@ class RecentChange $ip = ''; } } - if ( $bot === 'default' ) { - $bot = $user->isAllowed( 'bot' ); - } $rc = new RecentChange; $rc->mAttribs = array( @@ -363,6 +354,8 @@ class RecentChange # Makes an entry in the database corresponding to a rename public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) { + global $wgRequest; + if ( !$ip ) { $ip = wfGetIP(); if ( !$ip ) { @@ -384,7 +377,7 @@ class RecentChange 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0, 'rc_moved_to_ns' => $newTitle->getNamespace(), 'rc_moved_to_title' => $newTitle->getDBkey(), 'rc_ip' => $ip, @@ -415,6 +408,8 @@ class RecentChange public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', $type, $action, $target, $logComment, $params ) { + global $wgRequest; + if ( !$ip ) { $ip = wfGetIP(); if ( !$ip ) { @@ -436,7 +431,7 @@ class RecentChange 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, @@ -626,3 +621,4 @@ class RecentChange } } + diff --git a/includes/Revision.php b/includes/Revision.php index 39470923..05a4a68a 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -47,7 +47,7 @@ class Revision { array( "rev_id=$matchId", 'page_id=rev_page', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + 'page_title' => $title->getDBkey() ) ); } /** @@ -110,7 +110,7 @@ class Revision { array( "rev_id=$matchId", 'page_id=rev_page', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + 'page_title' => $title->getDBkey() ) ); } /** @@ -131,7 +131,7 @@ class Revision { array( 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_id=rev_page', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + 'page_title' => $title->getDBkey() ) ); } /** @@ -190,7 +190,7 @@ class Revision { return Revision::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey(), + 'page_title' => $title->getDBkey(), 'page_id=rev_page' ) ); } @@ -209,7 +209,7 @@ class Revision { wfGetDB( DB_SLAVE ), array( 'rev_id=page_latest', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey(), + 'page_title' => $title->getDBkey(), 'page_id=rev_page' ) ); } @@ -794,9 +794,8 @@ class Revision { * @param bool $minor * @return Revision */ - function newNullRevision( &$dbw, $pageId, $summary, $minor ) { - $fname = 'Revision::newNullRevision'; - wfProfileIn( $fname ); + public static function newNullRevision( &$dbw, $pageId, $summary, $minor ) { + wfProfileIn( __METHOD__ ); $current = $dbw->selectRow( array( 'page', 'revision' ), @@ -805,7 +804,7 @@ class Revision { 'page_id' => $pageId, 'page_latest=rev_id', ), - $fname ); + __METHOD__ ); if( $current ) { $revision = new Revision( array( @@ -818,7 +817,7 @@ class Revision { $revision = null; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $revision; } diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index f2dcbf94..c1c8daf3 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -725,7 +725,7 @@ class Sanitizer { * @return HTML-encoded text fragment */ static function encodeAttribute( $text ) { - $encValue = htmlspecialchars( $text ); + $encValue = htmlspecialchars( $text, ENT_QUOTES ); // Whitespace is normalized during attribute decoding, // so if we've been passed non-spaces we must encode them diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 11fc3deb..c22e58d7 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -51,7 +51,7 @@ class SearchEngine { if($wgContLang->hasVariants()){ $allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm)); } - + foreach($allSearchTerms as $term){ # Exact match? No need to look further. @@ -102,6 +102,12 @@ class SearchEngine { return $title; } } + + // Give hooks a chance at better match variants + $title = null; + if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) { + return $title; + } } $title = Title::newFromText( $searchterm ); @@ -109,7 +115,7 @@ class SearchEngine { # Entering an IP address goes to the contributions page if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) || User::isIP( trim( $searchterm ) ) ) { - return SpecialPage::getTitleFor( 'Contributions', $title->getDbkey() ); + return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); } @@ -336,7 +342,16 @@ class SearchResultSet { /** * @addtogroup Search */ +class SearchResultTooMany { + ## Some search engines may bail out if too many matches are found +} + + +/** + * @addtogroup Search + */ class SearchResult { + function SearchResult( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); } diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php index 6d2bbfef..271dbe1d 100644 --- a/includes/SearchMySQL4.php +++ b/includes/SearchMySQL4.php @@ -32,7 +32,7 @@ class SearchMySQL4 extends SearchMySQL { /** @todo document */ function parseQuery( $filteredText, $fulltext ) { global $wgContLang; - $lc = SearchEngine::legalSearchChars(); + $lc = SearchEngine::legalSearchChars(); // Minus format chars $searchon = ''; $this->searchTerms = array(); @@ -47,12 +47,14 @@ class SearchMySQL4 extends SearchMySQL { } $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] ); if( !empty( $terms[3] ) ) { + // Match individual terms in result highlighting... $regexp = preg_quote( $terms[3], '/' ); if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+"; } else { + // Match the quoted term in result highlighting... $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' ); } - $this->searchTerms[] = $regexp; + $this->searchTerms[] = "\b$regexp\b"; } wfDebug( "Would search with '$searchon'\n" ); wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); @@ -64,5 +66,9 @@ class SearchMySQL4 extends SearchMySQL { $field = $this->getIndexField( $fulltext ); return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) "; } + + public static function legalSearchChars() { + return "\"*" . parent::legalSearchChars(); + } } diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index cf9e6981..59110a5a 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -37,11 +37,24 @@ class SearchPostgres extends SearchEngine { * @access public */ function searchTitle( $term ) { - $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term , 'titlevector', 'page_title' ))); + $q = $this->searchQuery( $term , 'titlevector', 'page_title' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + // Needed for "Query requires full scan, GIN doesn't support it" + return new SearchResultTooMany(); + } return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); } function searchText( $term ) { - $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term, 'textvector', 'old_text' ))); + $q = $this->searchQuery( $term, 'textvector', 'old_text' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + return new SearchResultTooMany(); + } return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); } @@ -122,11 +135,12 @@ class SearchPostgres extends SearchEngine { $this->db->getServerVersion(); $wgDBversion = $this->db->numeric_version; } + $prefix = $wgDBversion < 8.3 ? "'default'," : ''; $searchstring = $this->parseQuery( $term ); ## We need a separate query here so gin does not complain about empty searches - $SQL = "SELECT to_tsquery('default',$searchstring)"; + $SQL = "SELECT to_tsquery($prefix $searchstring)"; $res = $this->db->doQuery($SQL); if (!$res) { ## TODO: Better output (example to catch: one 'two) @@ -148,15 +162,16 @@ class SearchPostgres extends SearchEngine { } $rankscore = $wgDBversion > 8.2 ? 5 : 1; + $rank = $wgDBversion < 8.3 ? 'rank' : 'ts_rank'; $query = "SELECT page_id, page_namespace, page_title, ". - "rank($fulltext, to_tsquery('default',$searchstring), $rankscore) AS score ". + "$rank($fulltext, to_tsquery($prefix $searchstring), $rankscore) AS score ". "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . - "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery('default',$searchstring)"; + "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery($prefix $searchstring)"; } ## Redirects if (! $this->showRedirects) - $query .= ' AND page_is_redirect = 0'; ## IS FALSE + $query .= ' AND page_is_redirect = 0'; ## Namespaces - defaults to 0 if ( count($this->namespaces) < 1) diff --git a/includes/Setup.php b/includes/Setup.php index 66bae0a8..53e0b949 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -198,7 +198,7 @@ $wgCookiePrefix = strtr($wgCookiePrefix, "=,; +.\"'\\[", "__________"); # If session.auto_start is there, we can't touch session name # -if( !ini_get( 'session.auto_start' ) ) +if( !wfIniGetBool( 'session.auto_start' ) ) session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' ); if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) { @@ -235,7 +235,8 @@ $wgRequest->interpolateTitle(); $wgUser = new StubUser; $wgLang = new StubUserLang; $wgOut = new StubObject( 'wgOut', 'OutputPage' ); -$wgParser = new StubObject( 'wgParser', 'Parser' ); +$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', array( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) ); diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 353f5b3a..beeeaf15 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -19,34 +19,61 @@ class SiteConfiguration { var $localVHosts = array(); /** */ - function get( $setting, $wiki, $suffix, $params = array() ) { - if ( array_key_exists( $setting, $this->settings ) ) { - if ( array_key_exists( $wiki, $this->settings[$setting] ) ) { - $retval = $this->settings[$setting][$wiki]; - } elseif ( array_key_exists( $suffix, $this->settings[$setting] ) ) { - $retval = $this->settings[$setting][$suffix]; - } elseif ( array_key_exists( 'default', $this->settings[$setting] ) ) { - $retval = $this->settings[$setting]['default']; - } else { - $retval = NULL; - } + function get( $settingName, $wiki, $suffix, $params = array(), $wikiTags = array() ) { + if ( array_key_exists( $settingName, $this->settings ) ) { + $thisSetting =& $this->settings[$settingName]; + do { + if ( array_key_exists( $wiki, $thisSetting ) ) { + $retval = $thisSetting[$wiki]; + break; + } + foreach ( $wikiTags as $tag ) { + if ( array_key_exists( $tag, $thisSetting ) ) { + $retval = $thisSetting[$tag]; + break 2; + } + } + if ( array_key_exists( $suffix, $thisSetting ) ) { + $retval = $thisSetting[$suffix]; + break; + } + if ( array_key_exists( 'default', $thisSetting ) ) { + $retval = $thisSetting['default']; + break; + } + $retval = null; + } while ( false ); } else { $retval = NULL; } if ( !is_null( $retval ) && count( $params ) ) { foreach ( $params as $key => $value ) { - $retval = str_replace( '$' . $key, $value, $retval ); + $retval = $this->doReplace( '$' . $key, $value, $retval ); } } return $retval; } + + /** Type-safe string replace; won't do replacements on non-strings */ + function doReplace( $from, $to, $in ) { + if( is_string( $in ) ) { + return str_replace( $from, $to, $in ); + } elseif( is_array( $in ) ) { + foreach( $in as $key => $val ) { + $in[$key] = $this->doReplace( $from, $to, $val ); + } + return $in; + } else { + return $in; + } + } /** */ - function getAll( $wiki, $suffix, $params ) { + function getAll( $wiki, $suffix, $params, $wikiTags = array() ) { $localSettings = array(); foreach ( $this->settings as $varname => $stuff ) { - $value = $this->get( $varname, $wiki, $suffix, $params ); + $value = $this->get( $varname, $wiki, $suffix, $params, $wikiTags ); if ( !is_null( $value ) ) { $localSettings[$varname] = $value; } @@ -55,8 +82,8 @@ class SiteConfiguration { } /** */ - function getBool( $setting, $wiki, $suffix ) { - return (bool)($this->get( $setting, $wiki, $suffix )); + function getBool( $setting, $wiki, $suffix, $wikiTags = array() ) { + return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) ); } /** */ @@ -69,25 +96,25 @@ class SiteConfiguration { } /** */ - function extractVar( $setting, $wiki, $suffix, &$var, $params ) { - $value = $this->get( $setting, $wiki, $suffix, $params ); + function extractVar( $setting, $wiki, $suffix, &$var, $params, $wikiTags = array() ) { + $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags ); if ( !is_null( $value ) ) { $var = $value; } } /** */ - function extractGlobal( $setting, $wiki, $suffix, $params ) { - $value = $this->get( $setting, $wiki, $suffix, $params ); + function extractGlobal( $setting, $wiki, $suffix, $params, $wikiTags = array() ) { + $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags ); if ( !is_null( $value ) ) { $GLOBALS[$setting] = $value; } } /** */ - function extractAllGlobals( $wiki, $suffix, $params ) { + function extractAllGlobals( $wiki, $suffix, $params, $wikiTags = array() ) { foreach ( $this->settings as $varName => $setting ) { - $this->extractGlobal( $varName, $wiki, $suffix, $params ); + $this->extractGlobal( $varName, $wiki, $suffix, $params, $wikiTags ); } } diff --git a/includes/Skin.php b/includes/Skin.php index f9e17057..30d2c2bc 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -16,10 +16,6 @@ class Skin extends Linker { /**#@+ * @private */ - var $lastdate, $lastline; - var $rc_cache ; # Cache for Enhanced Recent Changes - var $rcCacheIndex ; # Recent Changes Cache Counter for visibility toggle - var $rcMoveIndex; var $mWatchLinkNum = 0; // Appended to end of watch link id's /**#@-*/ protected $mRevisionId; // The revision ID we're looking at, null if not applicable. @@ -97,8 +93,7 @@ class Skin extends Linker { if( isset( $skinNames[$key] ) ) { return $key; } else { - // The old built-in skin - return 'standard'; + return 'monobook'; } } @@ -115,23 +110,25 @@ class Skin extends Linker { $skinNames = Skin::getSkinNames(); $skinName = $skinNames[$key]; + $className = 'Skin'.ucfirst($key); # Grab the skin class and initialise it. - // Preload base classes to work around APC/PHP5 bug - $deps = "{$wgStyleDirectory}/{$skinName}.deps.php"; - if( file_exists( $deps ) ) include_once( $deps ); - require_once( "{$wgStyleDirectory}/{$skinName}.php" ); - - # Check if we got if not failback to default skin - $className = 'Skin'.$skinName; - if( !class_exists( $className ) ) { - # DO NOT die if the class isn't found. This breaks maintenance - # scripts and can cause a user account to be unrecoverable - # except by SQL manipulation if a previously valid skin name - # is no longer valid. - wfDebug( "Skin class does not exist: $className\n" ); - $className = 'SkinStandard'; - require_once( "{$wgStyleDirectory}/Standard.php" ); + if ( !class_exists( $className ) ) { + // Preload base classes to work around APC/PHP5 bug + $deps = "{$wgStyleDirectory}/{$skinName}.deps.php"; + if( file_exists( $deps ) ) include_once( $deps ); + require_once( "{$wgStyleDirectory}/{$skinName}.php" ); + + # Check if we got if not failback to default skin + if( !class_exists( $className ) ) { + # DO NOT die if the class isn't found. This breaks maintenance + # scripts and can cause a user account to be unrecoverable + # except by SQL manipulation if a previously valid skin name + # is no longer valid. + wfDebug( "Skin class does not exist: $className\n" ); + $className = 'SkinMonobook'; + require_once( "{$wgStyleDirectory}/MonoBook.php" ); + } } $skin = new $className; return $skin; @@ -156,21 +153,28 @@ class Skin extends Linker { } function initPage( &$out ) { - global $wgFavicon, $wgScriptPath, $wgSitename, $wgLanguageCode, $wgLanguageNames; + global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgSitename, $wgContLang, $wgScriptExtension; - $fname = 'Skin::initPage'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if( false !== $wgFavicon ) { $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); } + + if( false !== $wgAppleTouchIcon ) { + $out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) ); + } + + $code = $wgContLang->getCode(); + $name = $wgContLang->getLanguageName( $code ); + $langName = $name ? $name : $code; # OpenSearch description link $out->addLink( array( 'rel' => 'search', 'type' => 'application/opensearchdescription+xml', - 'href' => "$wgScriptPath/opensearch_desc.php", - 'title' => "$wgSitename ({$wgLanguageNames[$wgLanguageCode]})", + 'href' => "$wgScriptPath/opensearch_desc{$wgScriptExtension}", + 'title' => "$wgSitename ($langName)", )); $this->addMetadataLinks($out); @@ -179,7 +183,7 @@ class Skin extends Linker { $this->preloadExistence(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -298,6 +302,7 @@ class Skin extends Linker { global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle; global $wgBreakFrames, $wgRequest; global $wgUseAjax, $wgAjaxWatch; + global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI; $ns = $wgTitle->getNamespace(); $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText(); @@ -310,7 +315,7 @@ class Skin extends Linker { 'wgScript' => $wgScript, 'wgServer' => $wgServer, 'wgCanonicalNamespace' => $nsname, - 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ), + 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ), 'wgNamespaceNumber' => $wgTitle->getNamespace(), 'wgPageName' => $wgTitle->getPrefixedDBKey(), 'wgTitle' => $wgTitle->getText(), @@ -325,6 +330,9 @@ class Skin extends Linker { 'wgContentLanguage' => $wgContLang->getCode(), 'wgBreakFrames' => $wgBreakFrames, 'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0, + 'wgVersion' => $wgVersion, + 'wgEnableAPI' => $wgEnableAPI, + 'wgEnableWriteAPI' => $wgEnableWriteAPI, ); global $wgLivePreview; @@ -476,7 +484,7 @@ class Skin extends Linker { function reallyDoGetUserStyles() { global $wgUser; $s = ''; - if (($undopt = $wgUser->getOption("underline")) != 2) { + if (($undopt = $wgUser->getOption("underline")) < 2) { $underline = $undopt ? 'underline' : 'none'; $s .= "a { text-decoration: $underline; }\n"; } @@ -487,17 +495,14 @@ class Skin extends Linker { a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; - text-decoration: inherit; } a.new:after, #quickbar a.new:after { content: "?"; color: #CC2200; - text-decoration: $underline; } a.stub:after, #quickbar a.stub:after { content: "!"; color: #772233; - text-decoration: $underline; } END; } @@ -782,12 +787,12 @@ END; } function getUndeleteLink() { - global $wgUser, $wgTitle, $wgContLang, $action; + global $wgUser, $wgTitle, $wgContLang, $wgLang, $action; if( $wgUser->isAllowed( 'deletedhistory' ) && (($wgTitle->getArticleId() == 0) || ($action == "history")) && ($n = $wgTitle->isDeleted() ) ) { - if ( $wgUser->isAllowed( 'delete' ) ) { + if ( $wgUser->isAllowed( 'undelete' ) ) { $msg = 'thisisdeleted'; } else { $msg = 'viewdeleted'; @@ -795,7 +800,7 @@ END; return wfMsg( $msg, $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete', $wgTitle->getPrefixedDBkey() ), - wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $n ) ) ); + wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ) ) ); } return ''; } @@ -836,8 +841,11 @@ END; } function subPageSubtitle() { - global $wgOut,$wgTitle,$wgNamespacesWithSubpages; $subpages = ''; + if(!wfRunHooks('SkinSubPageSubtitle', array(&$subpages))) + return $retval; + + global $wgOut, $wgTitle, $wgNamespacesWithSubpages; if($wgOut->isArticle() && !empty($wgNamespacesWithSubpages[$wgTitle->getNamespace()])) { $ptext=$wgTitle->getPrefixedText(); if(preg_match('/\//',$ptext)) { @@ -1072,11 +1080,14 @@ END; $dbr = wfGetDB( DB_SLAVE ); $watchlist = $dbr->tableName( 'watchlist' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist - WHERE wl_title='" . $dbr->strencode($wgTitle->getDBKey()) . + WHERE wl_title='" . $dbr->strencode($wgTitle->getDBkey()) . "' AND wl_namespace=" . $wgTitle->getNamespace() ; $res = $dbr->query( $sql, 'Skin::pageStats'); $x = $dbr->fetchObject( $res ); - $s .= ' ' . wfMsg('number_of_watching_users_pageview', $x->n ); + + $s .= ' ' . wfMsgExt( 'number_of_watching_users_pageview', + array( 'parseinline' ), $wgLang->formatNum($x->n) + ); } return $s . ' ' . $this->getCopyright(); @@ -1222,7 +1233,8 @@ END; // Otherwise, we display the link for the user, described in their // language (which may or may not be the same as the default language), // but we make the link target be the one site-wide page. - return $this->makeKnownLink( wfMsgForContent( $page ), wfMsg( $desc ) ); + return $this->makeKnownLink( wfMsgForContent( $page ), + wfMsgExt( $desc, array( 'parsemag', 'escapenoentities' ) ) ); } } @@ -1495,11 +1507,11 @@ END; # If it's present, the link points to this page, otherwise # it points to the talk page if( $wgTitle->isTalkPage() ) { - $title =& $wgTitle; + $title = $wgTitle; } elseif( $wgOut->showNewSectionLink() ) { - $title =& $wgTitle; + $title = $wgTitle; } else { - $title =& $wgTitle->getTalkPage(); + $title = $wgTitle->getTalkPage(); } return $this->makeKnownLinkObj( $title, wfMsg( 'postcomment' ), 'action=edit§ion=new' ); @@ -1590,7 +1602,7 @@ END; * @private */ function buildSidebar() { - global $parserMemc, $wgEnableSidebarCache; + global $parserMemc, $wgEnableSidebarCache, $wgSidebarCacheExpiry; global $wgLang, $wgContLang; $fname = 'SkinTemplate::buildSidebar'; @@ -1611,6 +1623,7 @@ END; $bar = array(); $lines = explode( "\n", wfMsgForContent( 'sidebar' ) ); + $heading = ''; foreach ($lines as $line) { if (strpos($line, '*') !== 0) continue; @@ -1650,9 +1663,9 @@ END; } } if ($cacheSidebar) - $parserMemc->set( $key, $bar, 86400 ); + $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry ); wfProfileOut( $fname ); return $bar; } -}
\ No newline at end of file +} diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 6ce40606..0178b866 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -219,17 +219,10 @@ class SkinTemplate extends Skin { $tpl->set( 'catlinks', $this->getCategories()); if( $wgOut->isSyndicated() ) { $feeds = array(); - foreach( $wgFeedClasses as $format => $class ) { - $linktext = $format; - if ( $format == "atom" ) { - $linktext = wfMsg( 'feed-atom' ); - } else if ( $format == "rss" ) { - $linktext = wfMsg( 'feed-rss' ); - } + foreach( $wgOut->getSyndicationLinks() as $format => $link ) { $feeds[$format] = array( - 'text' => $linktext, - 'href' => $wgRequest->appendQuery( "feed=$format" ) - ); + 'text' => wfMsg( "feed-$format" ), + 'href' => $link ); } $tpl->setRef( 'feeds', $feeds ); } else { @@ -351,13 +344,16 @@ class SkinTemplate extends Skin { $dbr = wfGetDB( DB_SLAVE ); $watchlist = $dbr->tableName( 'watchlist' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist - WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBKey()) . + WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBkey()) . "' AND wl_namespace=" . $this->mTitle->getNamespace() ; $res = $dbr->query( $sql, 'SkinTemplate::outputPage'); $x = $dbr->fetchObject( $res ); $numberofwatchingusers = $x->n; if ($numberofwatchingusers > 0) { - $tpl->set('numberofwatchingusers', wfMsg('number_of_watching_users_pageview', $numberofwatchingusers)); + $tpl->set('numberofwatchingusers', + wfMsgExt('number_of_watching_users_pageview', array('parseinline'), + $wgLang->formatNum($numberofwatchingusers)) + ); } else { $tpl->set('numberofwatchingusers', false); } @@ -602,6 +598,13 @@ class SkinTemplate extends Skin { global $wgContLang; $text = $wgContLang->getFormattedNsText( Namespace::getSubject( $title->getNamespace() ) ); } + + $result = array(); + if( !wfRunHooks('SkinTemplateTabAction', array(&$this, + $title, $message, $selected, $checkEdit, + &$classes, &$query, &$text, &$result)) ) { + return $result; + } return array( 'class' => implode( ' ', $classes ), @@ -638,7 +641,7 @@ class SkinTemplate extends Skin { * @private */ function buildContentActionUrls () { - global $wgContLang, $wgOut; + global $wgContLang, $wgLang, $wgOut; $fname = 'SkinTemplate::buildContentActionUrls'; wfProfileIn( $fname ); @@ -685,7 +688,7 @@ class SkinTemplate extends Skin { 'href' => $this->mTitle->getLocalUrl( 'action=edit§ion=new' ) ); } - } else { + } elseif ( $this->mTitle->exists() || $this->mTitle->isAlwaysKnown() ) { $content_actions['viewsource'] = array( 'class' => ($action == 'edit') ? 'selected' : false, 'text' => wfMsg('viewsource'), @@ -703,6 +706,22 @@ class SkinTemplate extends Skin { 'href' => $this->mTitle->getLocalUrl( 'action=history') ); + if($wgUser->isAllowed('delete')){ + $content_actions['delete'] = array( + 'class' => ($action == 'delete') ? 'selected' : false, + 'text' => wfMsg('delete'), + 'href' => $this->mTitle->getLocalUrl( 'action=delete' ) + ); + } + if ( $this->mTitle->quickUserCan( 'move' ) ) { + $moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage ); + $content_actions['move'] = array( + 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false, + 'text' => wfMsg('move'), + 'href' => $moveTitle->getLocalUrl() + ); + } + if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) { if(!$this->mTitle->isProtected()){ $content_actions['protect'] = array( @@ -719,35 +738,38 @@ class SkinTemplate extends Skin { ); } } - if($wgUser->isAllowed('delete')){ - $content_actions['delete'] = array( - 'class' => ($action == 'delete') ? 'selected' : false, - 'text' => wfMsg('delete'), - 'href' => $this->mTitle->getLocalUrl( 'action=delete' ) - ); - } - if ( $this->mTitle->quickUserCan( 'move' ) ) { - $moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage ); - $content_actions['move'] = array( - 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false, - 'text' => wfMsg('move'), - 'href' => $moveTitle->getLocalUrl() - ); - } } else { //article doesn't exist or is deleted - if( $wgUser->isAllowed( 'delete' ) ) { + if( $wgUser->isAllowed( 'deletedhistory' ) && $wgUser->isAllowed( 'undelete' ) ) { if( $n = $this->mTitle->isDeleted() ) { $undelTitle = SpecialPage::getTitleFor( 'Undelete' ); $content_actions['undelete'] = array( 'class' => false, - 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $n ), + 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $wgLang->formatNum($n) ), 'href' => $undelTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) ) #'href' => self::makeSpecialUrl( "Undelete/$this->thispage" ) ); } } + + if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) { + if( !$this->mTitle->getRestrictions( 'create' ) ) { + $content_actions['protect'] = array( + 'class' => ($action == 'protect') ? 'selected' : false, + 'text' => wfMsg('protect'), + 'href' => $this->mTitle->getLocalUrl( 'action=protect' ) + ); + + } else { + $content_actions['unprotect'] = array( + 'class' => ($action == 'unprotect') ? 'selected' : false, + 'text' => wfMsg('unprotect'), + 'href' => $this->mTitle->getLocalUrl( 'action=unprotect' ) + ); + } + } } + wfProfileOut( "$fname-live" ); if( $this->loggedin ) { @@ -1186,3 +1208,6 @@ class QuickTemplate { } } + + + diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php index 4ba01e29..ee97b48e 100644 --- a/includes/SpecialAllmessages.php +++ b/includes/SpecialAllmessages.php @@ -13,7 +13,7 @@ function wfSpecialAllmessages() { # The page isn't much use if the MediaWiki namespace is not being used if( !$wgUseDatabaseMessages ) { - $wgOut->addWikiText( wfMsg( 'allmessagesnotsupportedDB' ) ); + $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' ); return; } @@ -44,25 +44,44 @@ function wfSpecialAllmessages() { wfProfileIn( __METHOD__ . '-output' ); if ( $ot == 'php' ) { - $navText .= makePhp( $messages ); - $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a><pre>' . htmlspecialchars( $navText ) . '</pre>' ); + $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' ); + $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | ' . + 'HTML | <a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' ); $wgOut->addWikiText( $navText ); - $wgOut->addHTML( makeHTMLText( $messages ) ); + $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 makePhp( $messages ) { +function wfAllMessagesMakePhp( $messages ) { global $wgLang; $txt = "\n\n\$messages = array(\n"; foreach( $messages as $key => $m ) { @@ -85,7 +104,7 @@ function makePhp( $messages ) { * @param $messages Messages array. * @return The HTML list of messages. */ -function makeHTMLText( $messages ) { +function wfAllMessagesMakeHTMLText( $messages ) { global $wgLang, $wgContLang, $wgUser; wfProfileIn( __METHOD__ ); diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php index 07ff120b..9f5cf834 100644 --- a/includes/SpecialAllpages.php +++ b/includes/SpecialAllpages.php @@ -38,11 +38,21 @@ function wfSpecialAllpages( $par=NULL, $specialPage ) { * @addtogroup SpecialPage */ class SpecialAllpages { - var $maxPerPage=960; - var $topLevelMax=50; - var $name='Allpages'; - # Determines, which message describes the input field 'nsfrom' (->SpecialPrefixindex.php) - var $nsfromMsg='allpagesfrom'; + /** + * 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 @@ -63,7 +73,7 @@ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) . "</td> <td>" . - Xml::input( 'from', 20, htmlspecialchars ( $from ), array( 'id' => 'nsfrom' ) ) . + Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) . "</td> </tr> <tr> @@ -86,7 +96,6 @@ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { */ function showToplevel ( $namespace = NS_MAIN, $including = false ) { global $wgOut, $wgContLang; - $fname = "indexShowToplevel"; $align = $wgContLang->isRtl() ? 'left' : 'right'; # TODO: Either make this *much* faster or cache the title index points @@ -105,7 +114,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { if ( ! $dbr->implicitOrderby() ) { $options['ORDER BY'] = 'page_title'; } - $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, $options ); + $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options ); $lastTitle = $firstTitle; # This array is going to hold the page_titles in order. @@ -122,7 +131,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { 'page', /* FROM */ 'page_title', /* WHAT */ $where + array( $chunk), - $fname, + __METHOD__, array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') ); if ( $s = $dbr->fetchObject( $res ) ) { @@ -133,7 +142,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { array( 'page_namespace' => $namespace, $chunk - ), $fname ); + ), __METHOD__ ); array_push( $lines, $endTitle ); $done = true; } @@ -214,7 +223,6 @@ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { function showChunk( $namespace = NS_MAIN, $from, $including = false ) { global $wgOut, $wgUser, $wgContLang; - $fname = 'indexShowChunk'; $sk = $wgUser->getSkin(); $fromList = $this->getNamespaceKeyAndText($namespace, $from); @@ -239,7 +247,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { 'page_namespace' => $namespace, 'page_title >= ' . $dbr->addQuotes( $fromKey ) ), - $fname, + __METHOD__, array( 'ORDER BY' => 'page_title', 'LIMIT' => $this->maxPerPage + 1, @@ -261,7 +269,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { if( $n % 3 == 0 ) { $out .= '<tr>'; } - $out .= "<td>$link</td>"; + $out .= "<td width=\"33%\">$link</td>"; $n++; if( $n % 3 == 0 ) { $out .= '</tr>'; @@ -286,7 +294,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { 'page', 'page_title', array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ), - $fname, + __METHOD__, array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) ) ); @@ -301,7 +309,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { if ( ! $dbr->implicitOrderby() ) { $options['ORDER BY'] = 'page_title'; } - $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, $options ); + $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 ); diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php index 942ebe8b..cfbef1b3 100644 --- a/includes/SpecialBlockip.php +++ b/includes/SpecialBlockip.php @@ -70,19 +70,18 @@ class IPBlockForm { global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; $wgOut->setPagetitle( wfMsg( 'blockip' ) ); - $wgOut->addWikiText( wfMsg( 'blockiptext' ) ); + $wgOut->addWikiMsg( 'blockiptext' ); if($wgSysopUserBans) { $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' ); } else { - $mIpaddress = Xml::label( wfMsg( 'ipadress' ), 'mw-bi-target' ); + $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' ); } $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' ); $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' ); $mIpbothertime = wfMsgHtml( 'ipbotheroption' ); $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' ); $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' ); - $mIpbreasonotherlist = wfMsgHtml( 'ipbreasonotherlist' ); $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $action = $titleObj->escapeLocalURL( "action=submit" ); @@ -111,38 +110,9 @@ class IPBlockForm { $blockExpiryFormOptions .= "<option value=\"$value\"$selected>$show</option>"; } - $scBlockReasonList = wfMsgForContent( 'ipbreason-dropdown' ); - $blockReasonList = ''; - if ( $scBlockReasonList != '' && $scBlockReasonList != '-' ) { - $blockReasonList = "<option value=\"other\">$mIpbreasonotherlist</option>"; - $optgroup = ""; - foreach ( explode( "\n", $scBlockReasonList ) as $option) { - $value = trim( htmlspecialchars($option) ); - if ( $value == '' ) { - continue; - } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) { - // A new group is starting ... - $value = trim( substr( $value, 1 ) ); - $blockReasonList .= "$optgroup<optgroup label=\"$value\">"; - $optgroup = "</optgroup>"; - } elseif ( substr( $value, 0, 2) == '**' ) { - // groupmember - $selected = ""; - $value = trim( substr( $value, 2 ) ); - if ( $this->BlockReasonList === $value) - $selected = ' selected="selected"'; - $blockReasonList .= "<option value=\"$value\"$selected>$value</option>"; - } else { - // groupless block reason - $selected = ""; - if ( $this->BlockReasonList === $value) - $selected = ' selected="selected"'; - $blockReasonList .= "$optgroup<option value=\"$value\"$selected>$value</option>"; - $optgroup = ""; - } - } - $blockReasonList .= $optgroup; - } + $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList', + wfMsgForContent( 'ipbreason-dropdown' ), + wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 ); $token = $wgUser->editToken(); @@ -182,17 +152,13 @@ class IPBlockForm { array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . " </td> </tr>"); - if ( $blockReasonList != '' ) { - $wgOut->addHTML(" - <tr> - <td align=\"$alignRight\">{$mIpbreasonother}</td> - <td> - <select tabindex='4' id=\"wpBlockReasonList\" name=\"wpBlockReasonList\"> - $blockReasonList - </select> - </td> - </tr>"); - } + $wgOut->addHTML(" + <tr> + <td align=\"$alignRight\">{$mIpbreasonother}</td> + <td> + $reasonDropDown + </td> + </tr>"); $wgOut->addHTML(" <tr id=\"wpBlockReason\"> <td align=\"$alignRight\">{$mIpbreason}</td> @@ -227,34 +193,35 @@ class IPBlockForm { </td> </tr> "); - // Allow some users to hide name from block log, blocklist and listusers - if ( $wgUser->isAllowed( 'hideuser' ) ) { + + global $wgSysopEmailBans; + if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { $wgOut->addHTML(" - <tr> + <tr id='wpEnableEmailBan'> <td> </td> <td> - " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ), - 'wpHideName', 'wpHideName', $this->BlockHideName, - array( 'tabindex' => '9' ) ) . " + " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ), + 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, + array( 'tabindex' => '10' )) . " </td> </tr> "); } - global $wgSysopEmailBans; - - if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { + // Allow some users to hide name from block log, blocklist and listusers + if ( $wgUser->isAllowed( 'hideuser' ) ) { $wgOut->addHTML(" - <tr id='wpEnableEmailBan'> + <tr id='wpEnableHideUser'> <td> </td> <td> - " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ), - 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, - array( 'tabindex' => '10' )) . " + " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ), + 'wpHideName', 'wpHideName', $this->BlockHideName, + array( 'tabindex' => '9' ) ) . " </td> </tr> "); } + $wgOut->addHTML(" <tr> <td style='padding-top: 1em'> </td> @@ -281,8 +248,14 @@ class IPBlockForm { } } - function doSubmit() { - global $wgOut, $wgUser, $wgSysopUserBans, $wgSysopRangeBans; + /** + * 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 @@ -299,27 +272,23 @@ class IPBlockForm { # IPv4 if ( $wgSysopRangeBans ) { if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) { - $this->showForm( wfMsg( 'ip_range_invalid' ) ); - return; + return array('ip_range_invalid'); } $this->BlockAddress = Block::normaliseRange( $this->BlockAddress ); } else { # Range block illegal - $this->showForm( wfMsg( 'range_block_disabled' ) ); - return; + 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 ) { - $this->showForm( wfMsg( 'ip_range_invalid' ) ); - return; + return array('ip_range_invalid'); } $this->BlockAddress = Block::normaliseRange( $this->BlockAddress ); } else { # Range block illegal - $this->showForm( wfMsg( 'range_block_disabled' ) ); - return; + return array('range_block_disabled'); } } else { # Username block @@ -327,15 +296,13 @@ class IPBlockForm { $user = User::newFromName( $this->BlockAddress ); if( !is_null( $user ) && $user->getID() ) { # Use canonical name - $this->BlockAddress = $user->getName(); $userId = $user->getID(); + $this->BlockAddress = $user->getName(); } else { - $this->showForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->BlockAddress ) ) ); - return; + return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) ); } } else { - $this->showForm( wfMsg( 'badipaddress' ) ); - return; + return array('badipaddress'); } } } @@ -353,8 +320,7 @@ class IPBlockForm { $expirestr = $this->BlockOther; if (strlen($expirestr) == 0) { - $this->showForm( wfMsg( 'ipb_expiry_invalid' ) ); - return; + return array('ipb_expiry_invalid'); } if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) { @@ -364,8 +330,7 @@ class IPBlockForm { $expiry = strtotime( $expirestr ); if ( $expiry < 0 || $expiry === false ) { - $this->showForm( wfMsg( 'ipb_expiry_invalid' ) ); - return; + return array('ipb_expiry_invalid'); } $expiry = wfTimestamp( TS_MW, $expiry ); @@ -381,9 +346,7 @@ class IPBlockForm { if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { if ( !$block->insert() ) { - $this->showForm( wfMsg( 'ipb_already_blocked', - htmlspecialchars( $this->BlockAddress ) ) ); - return; + return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress)); } wfRunHooks('BlockIpComplete', array($block, $wgUser)); @@ -400,10 +363,28 @@ class IPBlockForm { $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() { @@ -411,8 +392,8 @@ class IPBlockForm { $wgOut->setPagetitle( wfMsg( 'blockip' ) ); $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) ); - $text = wfMsg( 'blockipsuccesstext', $this->BlockAddress ); - $wgOut->addWikiText( $text ); + $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress ); + $wgOut->addHtml( $text ); } function showLogFragment( $out, $title ) { @@ -450,7 +431,7 @@ class IPBlockForm { private function getConvenienceLinks() { global $wgUser; $skin = $wgUser->getSkin(); - $links[] = $skin->makeLink ( 'MediaWiki:ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) ); + $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>'; diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php index da2757ac..6c9dea06 100644 --- a/includes/SpecialBlockme.php +++ b/includes/SpecialBlockme.php @@ -13,13 +13,12 @@ function wfSpecialBlockme() { $ip = wfGetIP(); if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) { - $wgOut->addWikiText( wfMsg( 'disabled' ) ); + $wgOut->addWikiMsg( 'proxyblocker-disabled' ); return; } $blockerName = wfMsg( "proxyblocker" ); $reason = wfMsg( "proxyblockreason" ); - $success = wfMsg( "proxyblocksuccess" ); $u = User::newFromName( $blockerName ); $id = $u->idForName(); @@ -34,6 +33,6 @@ function wfSpecialBlockme() { $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() ); $block->insert(); - $wgOut->addWikiText( $success ); + $wgOut->addWikiMsg( "proxyblocksuccess" ); } diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php index 5f103495..af258872 100644 --- a/includes/SpecialBooksources.php +++ b/includes/SpecialBooksources.php @@ -31,7 +31,7 @@ class SpecialBookSources extends SpecialPage { global $wgOut, $wgRequest; $this->setHeaders(); $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'booksources-summary' ) ); + $wgOut->addWikiMsg( 'booksources-summary' ); $wgOut->addHtml( $this->makeForm() ); if( strlen( $this->isbn ) > 0 ) $this->showList(); @@ -87,7 +87,7 @@ class SpecialBookSources extends SpecialPage { } # Fall back to the defaults given in the language file - $wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) ); + $wgOut->addWikiMsg( 'booksources-text' ); $wgOut->addHtml( '<ul>' ); $items = $wgContLang->getBookstoreList(); foreach( $items as $label => $url ) diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php index 1fb48350..f6887741 100644 --- a/includes/SpecialBrokenRedirects.php +++ b/includes/SpecialBrokenRedirects.php @@ -51,7 +51,7 @@ class BrokenRedirectsPage extends PageQueryPage { if ( isset( $result->rd_title ) ) { $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title ); } else { - $blinks = $fromObj->getBrokenLinksFrom(); + $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links if ( $blinks ) { $toObj = $blinks[0]; } else { diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php index 596569ed..efe65a78 100644 --- a/includes/SpecialCategories.php +++ b/includes/SpecialCategories.php @@ -9,7 +9,7 @@ function wfSpecialCategories() { $cap = new CategoryPager(); $wgOut->addHTML( - wfMsgWikiHtml( 'categoriespagetext' ) . + wfMsgExt( 'categoriespagetext', array( 'parse' ) ) . $cap->getNavigationBar() . '<ul>' . $cap->getBody() . '</ul>' . $cap->getNavigationBar() @@ -54,7 +54,7 @@ class CategoryPager extends AlphabeticPager { global $wgLang; $title = Title::makeTitle( NS_CATEGORY, $result->cl_to ); $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) ); - $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), + $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->count ) ); return Xml::tags('li', null, "$titleText ($count)" ) . "\n"; } diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php index ba419f25..c3aa53c2 100644 --- a/includes/SpecialConfirmemail.php +++ b/includes/SpecialConfirmemail.php @@ -30,7 +30,7 @@ class EmailConfirmation extends UnlistedSpecialPage { if( User::isValidEmailAddr( $wgUser->getEmail() ) ) { $this->showRequestForm(); } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_noemail' ) ); + $wgOut->addWikiMsg( 'confirmemail_noemail' ); } } else { $title = SpecialPage::getTitleFor( 'Userlogin' ); @@ -52,19 +52,19 @@ class EmailConfirmation extends UnlistedSpecialPage { if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) { $ok = $wgUser->sendConfirmationMail(); if ( WikiError::isError( $ok ) ) { - $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $ok->toString() ) ); + $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() ); } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_sent' ) ); + $wgOut->addWikiMsg( 'confirmemail_sent' ); } } else { if( $wgUser->isEmailConfirmed() ) { $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true ); - $wgOut->addWikiText( wfMsg( 'emailauthenticated', $time ) ); + $wgOut->addWikiMsg( 'emailauthenticated', $time ); } if( $wgUser->isEmailConfirmationPending() ) { - $wgOut->addWikiText( wfMsg( 'confirmemail_pending' ) ); + $wgOut->addWikiMsg( 'confirmemail_pending' ); } - $wgOut->addWikiText( wfMsg( 'confirmemail_text' ) ); + $wgOut->addWikiMsg( 'confirmemail_text' ); $self = SpecialPage::getTitleFor( 'Confirmemail' ); $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); $form .= wfHidden( 'token', $wgUser->editToken() ); @@ -86,16 +86,16 @@ class EmailConfirmation extends UnlistedSpecialPage { if( is_object( $user ) ) { if( $user->confirmEmail() ) { $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success'; - $wgOut->addWikiText( wfMsg( $message ) ); + $wgOut->addWikiMsg( $message ); if( !$wgUser->isLoggedIn() ) { $title = SpecialPage::getTitleFor( 'Userlogin' ); $wgOut->returnToMain( true, $title->getPrefixedText() ); } } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) ); + $wgOut->addWikiMsg( 'confirmemail_error' ); } } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) ); + $wgOut->addWikiMsg( 'confirmemail_invalid' ); } } diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php index cc1b2e6f..6bed7905 100644 --- a/includes/SpecialContributions.php +++ b/includes/SpecialContributions.php @@ -4,7 +4,7 @@ * @addtogroup SpecialPage */ -class ContribsPager extends IndexPager { +class ContribsPager extends ReverseChronologicalPager { public $mDefaultDirection = true; var $messages, $target; var $namespace = '', $mDb; @@ -110,33 +110,13 @@ class ContribsPager extends IndexPager { return "</ul>\n"; } - function getNavigationBar() { - if ( isset( $this->mNavigationBar ) ) { - return $this->mNavigationBar; - } - $linkTexts = array( - 'prev' => wfMsgHtml( "sp-contributions-newer", $this->mLimit ), - 'next' => wfMsgHtml( 'sp-contributions-older', $this->mLimit ), - 'first' => wfMsgHtml('sp-contributions-newest'), - 'last' => wfMsgHtml( 'sp-contributions-oldest' ) - ); - - $pagingLinks = $this->getPagingLinks( $linkTexts ); - $limitLinks = $this->getLimitLinks(); - $limits = implode( ' | ', $limitLinks ); - - $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . - wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); - return $this->mNavigationBar; - } - /** * 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 sysop - * privileges. The rollback link restores the most recent version that was not - * written by the target user. + * 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. */ @@ -159,7 +139,8 @@ class ContribsPager extends IndexPager { $difftext .= $this->messages['newarticle']; } - if( $wgUser->isAllowed( 'rollback' ) ) { + if( !$page->getUserPermissionsErrors( 'rollback', $wgUser ) + && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) { $topmarktext .= ' '.$sk->generateRollback( $rev ); } @@ -265,7 +246,7 @@ function wfSpecialContributions( $par = null ) { } else { $options['namespace'] = ''; } - if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) { + if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) { $options['bot'] = '1'; } @@ -302,7 +283,7 @@ function wfSpecialContributions( $par = null ) { $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] ); if ( !$pager->getNumRows() ) { - $wgOut->addWikiText( wfMsg( 'nocontribs' ) ); + $wgOut->addWikiMsg( 'nocontribs' ); return; } @@ -323,7 +304,7 @@ function wfSpecialContributions( $par = null ) { : 'sp-contributions-footer'; - $text = wfMsg( $message, $target ); + $text = wfMsgNoTrans( $message, $target ); if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { $wgOut->addHtml( '<div class="mw-contributions-footer">' ); $wgOut->addWikiText( $text ); @@ -426,13 +407,20 @@ function contributionsForm( $options ) { 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']) . ' '. - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + '<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) ) . ' '. + 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' ); diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php index 6d46fc50..7e4ec360 100644 --- a/includes/SpecialDoubleRedirects.php +++ b/includes/SpecialDoubleRedirects.php @@ -63,7 +63,6 @@ class DoubleRedirectsPage extends PageQueryPage { $fname = 'DoubleRedirectsPage::formatResult'; $titleA = Title::makeTitle( $result->namespace, $result->title ); - $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' ); if ( $result && !isset( $result->nsb ) ) { $dbr = wfGetDB( DB_SLAVE ); @@ -75,12 +74,13 @@ class DoubleRedirectsPage extends PageQueryPage { } } if ( !$result ) { - return "<s>{$linkA}</s>\n"; + 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 ); diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php index 1d5a9647..7de89dce 100644 --- a/includes/SpecialEmailuser.php +++ b/includes/SpecialEmailuser.php @@ -4,8 +4,6 @@ * @addtogroup SpecialPage */ -require_once('UserMailer.php'); - /** * @todo document */ @@ -96,7 +94,7 @@ class EmailUserForm { global $wgOut, $wgUser; $wgOut->setPagetitle( wfMsg( "emailpage" ) ); - $wgOut->addWikiText( wfMsg( "emailpagetext" ) ); + $wgOut->addWikiMsg( "emailpagetext" ); if ( $this->subject === "" ) { $this->subject = wfMsg( "defemailsubject" ); @@ -133,7 +131,7 @@ class EmailUserForm { </tr> </table> <span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span> -<textarea name=\"wpText\" rows='20' cols='80' wrap='virtual' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) . +<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}\" /> @@ -143,18 +141,47 @@ class EmailUserForm { } function doSubmit() { - global $wgOut, $wgUser; + global $wgOut, $wgUser, $wgUserEmailUseReplyTo; $to = new MailAddress( $this->target ); $from = new MailAddress( $wgUser ); $subject = $this->subject; 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( $to, $from, $subject, $this->text ); + $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo ); if( WikiError::isError( $mailResult ) ) { - $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult); + $wgOut->addHTML( wfMsg( "usermailererror" ) . + ' ' . htmlspecialchars( $mailResult->getMessage() ) ); } else { // if the user requested a copy of this mail, do this now, @@ -162,14 +189,15 @@ class EmailUserForm { 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( $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. - $wgOut->addHTML( wfMsg( "usermailererror" ) . $ccResult); + $wgOut->addHTML( wfMsg( "usermailererror" ) . + ' ' . htmlspecialchars( $ccResult->getMessage() ) ); return; } } @@ -192,4 +220,3 @@ class EmailUserForm { $wgOut->returnToMain( false, $user->getUserPage() ); } } - diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php index 12bd4d5c..1fe2e44b 100644 --- a/includes/SpecialExport.php +++ b/includes/SpecialExport.php @@ -24,7 +24,7 @@ function wfExportGetPagesFromCategory( $title ) { global $wgContLang; - $name = $title->getDBKey(); + $name = $title->getDBkey(); $dbr = wfGetDB( DB_SLAVE ); @@ -50,6 +50,68 @@ function wfExportGetPagesFromCategory( $title ) { } /** + * 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 = '' ) { @@ -66,6 +128,11 @@ function wfSpecialExport( $page = '' ) { 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 ); } @@ -123,7 +190,7 @@ function wfSpecialExport( $page = '' ) { $list_authors = $wgRequest->getCheck( 'listauthors' ); if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ; - + if ( $doexport ) { $wgOut->disable(); @@ -136,7 +203,25 @@ function wfSpecialExport( $page = '' ) { $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' ); $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" ); } - $pages = explode( "\n", $page ); + + /* 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 ); @@ -160,7 +245,7 @@ function wfSpecialExport( $page = '' ) { #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->userCan( 'read' ) ) continue; #TODO: perhaps output an <error> tag or something. + if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something. $exporter->pageByTitle( $title ); } @@ -188,6 +273,9 @@ function wfSpecialExport( $page = '' ) { } 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' ) ); diff --git a/includes/SpecialFilepath.php b/includes/SpecialFilepath.php new file mode 100644 index 00000000..4ba8fdb0 --- /dev/null +++ b/includes/SpecialFilepath.php @@ -0,0 +1,69 @@ +<?php + +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(); + } + } +} + +class FilepathForm { + var $mTitle; + + function FilepathForm( &$title ) { + $this->mTitle =& $title; + } + + function execute() { + global $wgOut, $wgTitle, $wgScript; + + $wgOut->addHTML( + wfElement( 'form', + array( + 'id' => 'specialfilepath', + 'method' => 'get', + 'action' => $wgScript, + ), + null + ) . + wfHidden( 'title', $wgTitle->getPrefixedText() ) . + wfOpenElement( 'label' ) . + wfMsgHtml( 'filepath-page' ) . + ' ' . + wfElement( 'input', + array( + 'type' => 'text', + 'size' => 25, + 'name' => 'file', + 'value' => is_object( $this->mTitle ) ? $this->mTitle->getText() : '' + ), + '' + ) . + ' ' . + wfElement( 'input', + array( + 'type' => 'submit', + 'value' => wfMsgHtml( 'filepath-submit' ) + ), + '' + ) . + wfCloseElement( 'label' ) . + wfCloseElement( 'form' ) + ); + } +} diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php index ad5d8e64..7a2e6221 100644 --- a/includes/SpecialImport.php +++ b/includes/SpecialImport.php @@ -34,6 +34,11 @@ function wfSpecialImport( $page = '' ) { $frompage = ''; $history = true; + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') { $isUpload = false; $namespace = $wgRequest->getIntOrNull( 'namespace' ); @@ -61,9 +66,9 @@ function wfSpecialImport( $page = '' ) { } if( WikiError::isError( $source ) ) { - $wgOut->addWikiText( wfEscapeWikiText( $source->getMessage() ) ); + $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) ); } else { - $wgOut->addWikiText( wfMsg( "importstart" ) ); + $wgOut->addWikiMsg( "importstart" ); $importer = new WikiImporter( $source ); if( !is_null( $namespace ) ) { @@ -73,88 +78,93 @@ function wfSpecialImport( $page = '' ) { $reporter->open(); $result = $importer->doImport(); - $reporter->close(); + $resultCount = $reporter->close(); if( WikiError::isError( $result ) ) { - $wgOut->addWikiText( wfMsg( "importfailed", - wfEscapeWikiText( $result->getMessage() ) ) ); + # 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->addWikiText( wfMsg( "importsuccess" ) ); + $wgOut->addWikiMsg( 'importsuccess' ); } + $wgOut->addWikiText( '<hr />' ); } } $action = $wgTitle->getLocalUrl( 'action=submit' ); if( $wgUser->isAllowed( 'importupload' ) ) { - $wgOut->addWikiText( wfMsg( "importtext" ) ); - $wgOut->addHTML( " -<fieldset> - <legend>" . wfMsgHtml('upload') . "</legend> - <form enctype='multipart/form-data' method='post' action=\"$action\"> - <input type='hidden' name='action' value='submit' /> - <input type='hidden' name='source' value='upload' /> - <input type='hidden' name='MAX_FILE_SIZE' value='2000000' /> - <input type='file' name='xmlimport' value='' size='30' /> - <input type='submit' value=\"" . wfMsgHtml( "uploadbtn" ) . "\" /> - </form> -</fieldset> -" ); + $wgOut->addWikiMsg( "importtext" ); + $wgOut->addHTML( + Xml::openElement( 'fieldset' ). + Xml::element( 'legend', null, wfMsg( 'upload' ) ) . + Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) . + Xml::hidden( 'action', 'submit' ) . + Xml::hidden( 'source', 'upload' ) . + "<input type='file' name='xmlimport' value='' size='30' />" . // No Xml function for type=file? Todo? + Xml::submitButton( wfMsg( 'uploadbtn' ) ) . + Xml::closeElement( 'form' ) . + Xml::closeElement( 'fieldset' ) + ); } else { if( empty( $wgImportSources ) ) { - $wgOut->addWikiText( wfMsg( 'importnosources' ) ); + $wgOut->addWikiMsg( 'importnosources' ); } } if( !empty( $wgImportSources ) ) { - $wgOut->addHTML( " -<fieldset> - <legend>" . wfMsgHtml('importinterwiki') . "</legend> - <form method='post' action=\"$action\">" . - $wgOut->parse( wfMsg( 'import-interwiki-text' ) ) . " - <input type='hidden' name='action' value='submit' /> - <input type='hidden' name='source' value='interwiki' /> - <table> - <tr> - <td> - <select name='interwiki'>" ); + $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' ) . + "<tr> + <td>" . + Xml::openElement( 'select', array( 'name' => 'interwiki' ) ) + ); foreach( $wgImportSources as $prefix ) { - $iw = htmlspecialchars( $prefix ); - $selected = ($interwiki === $prefix) ? ' selected="selected"' : ''; - $wgOut->addHTML( "<option value=\"$iw\"$selected>$iw</option>\n" ); + $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : ''; + $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) ); } - $wgOut->addHTML( " - </select> - </td> + $wgOut->addHTML( + Xml::closeElement( 'select' ) . + "</td> <td>" . - wfInput( 'frompage', 50, $frompage ) . + Xml::input( 'frompage', 50, $frompage ) . "</td> </tr> <tr> - <td></td> + <td> + </td> <td>" . - wfCheckLabel( wfMsg( 'import-interwiki-history' ), - 'interwikiHistory', 'interwikiHistory', $history ) . + Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) . "</td> </tr> <tr> - <td></td> <td> - " . wfMsgHtml( 'import-interwiki-namespace' ) . " " . - HTMLnamespaceselector( $namespace, '' ) . " </td> + <td>" . + Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) . + Xml::namespaceSelector( $namespace, '' ) . + "</td> </tr> <tr> - <td></td> + <td> + </td> <td>" . - wfSubmitButton( wfMsg( 'import-interwiki-submit' ) ) . + Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) . "</td> - </tr> - </table> - </form> -</fieldset> -" ); + </tr>" . + Xml::closeElement( 'table' ). + Xml::closeElement( 'form' ) . + Xml::closeElement( 'fieldset' ) + ); } } @@ -185,21 +195,21 @@ class ImportReporter { $localCount = $wgLang->formatNum( $successCount ); $contentCount = $wgContLang->formatNum( $successCount ); - $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . - " " . - wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . - "</li>\n" ); - 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 = wfMsgForContent( 'import-logentry-upload-detail', + $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ), $contentCount ); $log->addEntry( 'upload', $title, $detail ); } else { $interwiki = '[[:' . $this->mInterwiki . ':' . $origTitle->getPrefixedText() . ']]'; - $detail = wfMsgForContent( 'import-logentry-interwiki-detail', + $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ), $contentCount, $interwiki ); $log->addEntry( 'interwiki', $title, $detail ); } @@ -212,15 +222,20 @@ class ImportReporter { # Update page record $article = new Article( $title ); $article->updateRevisionOn( $dbw, $nullRevision ); + } else { + $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' ); } } function close() { global $wgOut; if( $this->mPageCount == 0 ) { - $wgOut->addHtml( "<li>" . wfMsgHtml( 'importnopages' ) . "</li>\n" ); + $wgOut->addHtml( "</ul>\n" ); + return new WikiErrorMsg( "importnopages" ); } $wgOut->addHtml( "</ul>\n" ); + + return $this->mPageCount; } } @@ -428,7 +443,7 @@ class WikiImporter { $chunk = $this->mSource->readChunk(); if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) { wfDebug( "WikiImporter::doImport encountered XML parsing error\n" ); - return new WikiXmlError( $parser, 'XML import parse failure', $chunk, $offset ); + return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset ); } $offset += strlen( $chunk ); } while( $chunk !== false && !$this->mSource->atEnd() ); @@ -683,7 +698,7 @@ class WikiImporter { $this->origTitle = Title::newFromText( $this->workTitle ); if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) { $this->pageTitle = Title::makeTitle( $this->mTargetNamespace, - $this->origTitle->getDbKey() ); + $this->origTitle->getDBkey() ); } else { $this->pageTitle = Title::newFromText( $this->workTitle ); } @@ -850,7 +865,18 @@ class ImportStreamSource { return new WikiErrorMsg( 'importnofile' ); } if( !empty( $upload['error'] ) ) { - return new WikiErrorMsg( 'importuploaderror', $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 ) ) { @@ -879,6 +905,9 @@ class ImportStreamSource { } 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' ); @@ -890,6 +919,3 @@ class ImportStreamSource { } } } - - - diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php index 4f093dcb..c2de9e2f 100644 --- a/includes/SpecialIpblocklist.php +++ b/includes/SpecialIpblocklist.php @@ -80,7 +80,7 @@ class IPUnblockForm { global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; $wgOut->setPagetitle( wfMsg( 'unblockip' ) ); - $wgOut->addWikiText( wfMsg( 'unblockiptext' ) ); + $wgOut->addWikiMsg( 'unblockiptext' ); $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ); $ipr = wfMsgHtml( 'ipbreason' ); @@ -91,7 +91,7 @@ class IPUnblockForm { if ( "" != $err ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); - $wgOut->addWikitext( "<span class='error'>{$err}</span>\n" ); + $wgOut->addWikiText( "<span class='error'>{$err}</span>\n" ); } $token = htmlspecialchars( $wgUser->editToken() ); @@ -140,49 +140,78 @@ class IPUnblockForm { } - function doSubmit() { - global $wgOut; + 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 - if ( $this->id ) { - $block = Block::newFromID( $this->id ); - if ( $block ) { - $this->ip = $block->getRedactedName(); + /** + * 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(); - $this->ip = trim( $this->ip ); - if ( substr( $this->ip, 0, 1 ) == "#" ) { - $id = substr( $this->ip, 1 ); + $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( $this->ip ); + $block = Block::newFromDB( $ip ); if ( !$block ) { - $block = null; + 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); } } } - $success = false; - if ( $block ) { - # Delete block - if ( $block->delete() ) { - # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); - $success = true; - } + // Yes, this is really necessary + $id = $block->mId; + + # Delete block + if ( !$block->delete() ) { + return array('ipb_cant_unblock', htmlspecialchars($id)); } - if ( $success ) { - # Report to the user - $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); - $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) ); - $wgOut->redirect( $success ); - } else { - if ( !$this->ip && $this->id ) { - $this->ip = '#' . $this->id; - } - $this->showForm( wfMsg( 'ipb_cant_unblock', htmlspecialchars( $this->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 ) { @@ -234,9 +263,9 @@ class IPUnblockForm { ); } elseif ( $this->ip != '') { $wgOut->addHTML( $this->searchForm() ); - $wgOut->addWikiText( wfMsg( 'ipblocklist-no-results' ) ); + $wgOut->addWikiMsg( 'ipblocklist-no-results' ); } else { - $wgOut->addWikiText( wfMsg( 'ipblocklist-empty' ) ); + $wgOut->addWikiMsg( 'ipblocklist-empty' ); } } @@ -268,13 +297,12 @@ class IPUnblockForm { $sk = $wgUser->getSkin(); if( is_null( $msg ) ) { $msg = array(); - $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', + $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' ); foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } $msg['blocklistline'] = wfMsg( 'blocklistline' ); - $msg['contribslink'] = wfMsg( 'contribslink' ); } # Prepare links to the blocker's user and talk pages diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php index 581ea55b..92bd66e4 100644 --- a/includes/SpecialListredirects.php +++ b/includes/SpecialListredirects.php @@ -30,8 +30,7 @@ class ListredirectsPage extends QueryPage { # Make a link to the redirect itself $rd_title = Title::makeTitle( $result->namespace, $result->title ); - $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); - $rd_link = $skin->makeKnownLinkObj( $rd_title, '', 'redirect=no' ); + $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' ); # Find out where the redirect leads $revision = Revision::newFromTitle( $rd_title ); @@ -39,19 +38,15 @@ class ListredirectsPage extends QueryPage { # 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 { - /** @todo Put in some decent error display here */ - $targetLink = '*'; + return "<s>$rd_link</s>"; } } else { - /** @todo Put in some decent error display here */ - $targetLink = '*'; + return "<s>$rd_link</s>"; } - - # Format the whole thing and return it - return "$rd_link $arr $targetLink"; - } } diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php index e57717e2..b523591c 100644 --- a/includes/SpecialLockdb.php +++ b/includes/SpecialLockdb.php @@ -51,7 +51,7 @@ class DBLockForm { global $wgOut, $wgUser; $wgOut->setPagetitle( wfMsg( 'lockdb' ) ); - $wgOut->addWikiText( wfMsg( 'lockdbtext' ) ); + $wgOut->addWikiMsg( 'lockdbtext' ); if ( "" != $err ) { $wgOut->setSubtitle( wfMsg( 'formerror' ) ); @@ -121,7 +121,7 @@ END $wgOut->setPagetitle( wfMsg( 'lockdb' ) ); $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) ); - $wgOut->addWikiText( wfMsg( 'lockdbsuccesstext' ) ); + $wgOut->addWikiMsg( 'lockdbsuccesstext' ); } public static function notWritable() { diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php index f0794eb5..5c28340f 100644 --- a/includes/SpecialLog.php +++ b/includes/SpecialLog.php @@ -123,6 +123,7 @@ class LogReader { */ function limitTitle( $page , $pattern ) { global $wgMiserMode; + $title = Title::newFromText( $page ); if( strlen( $page ) == 0 || !$title instanceof Title ) @@ -182,7 +183,7 @@ class LogReader { * @return ResultWrapper result object to return the relevant rows */ function getRows() { - $res = $this->db->query( $this->getQuery(), 'LogReader::getRows' ); + $res = $this->db->query( $this->getQuery(), __METHOD__ ); return $this->db->resultObject( $res ); } @@ -341,7 +342,7 @@ class LogViewer { } function showError( &$out ) { - $out->addWikiText( wfMsg( 'logempty' ) ); + $out->addWikiMsg( 'logempty' ); } /** @@ -370,7 +371,7 @@ class LogViewer { $revert = ''; // show revertmove link if ( !( $this->flags & self::NO_ACTION_LINK ) ) { - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { + if ( $s->log_type == 'move' && isset( $paramArray[0] ) && $wgUser->isAllowed( 'move' ) ) { $destTitle = Title::newFromText( $paramArray[0] ); if ( $destTitle ) { $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), @@ -383,9 +384,8 @@ class LogViewer { // show undelete link } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), - wfMsg( 'undeletebtn' ) , + wfMsg( 'undeletelink' ) , 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; - // show unblock link } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), @@ -394,21 +394,21 @@ class LogViewer { // show change protection link } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) { $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')'; - // show user tool links for self created users - // TODO: The extension should be handling this, get it out of core! - } elseif ( $s->log_action == 'create2' ) { - if( isset( $paramArray[0] ) ) { - $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true ); - } else { - # Fall back to a blue contributions link - $revert = $this->skin->userToolLinks( 1, $s->log_title ); - } - # Suppress $comment from old entries, not needed and can contain incorrect links - $comment = ''; + // Show unmerge link + } elseif ( $s->log_action == 'merge' ) { + $merge = SpecialPage::getTitleFor( 'Mergehistory' ); + $revert = '(' . $this->skin->makeKnownLinkObj( $merge, wfMsg('revertmerge'), + wfArrayToCGI( + array('target' => $paramArray[0], 'dest' => $title->getPrefixedText(), 'mergepoint' => $paramArray[1] ) + ) + ) . ')'; + } elseif ( wfRunHooks( 'LogLine', array( $s->log_type, $s->log_action, $title, $paramArray, &$comment, &$revert, $s->log_timestamp ) ) ) { + // wfDebug( "Invoked LogLine hook for " $s->log_type . ", " . $s->log_action . "\n" ); + // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters. } } - $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true ); + $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true ); $out = "<li>$time $userLink $action $comment $revert</li>\n"; return $out; } @@ -525,6 +525,3 @@ class LogViewer { $out->addHTML( '<p>' . $html . '</p>' ); } } - - - diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php index c89c1af6..70e44750 100644 --- a/includes/SpecialMIMEsearch.php +++ b/includes/SpecialMIMEsearch.php @@ -91,7 +91,7 @@ function wfSpecialMIMEsearch( $par = null ) { array( 'id' => 'specialmimesearch', 'method' => 'get', - 'action' => $wgTitle->escapeLocalUrl() + 'action' => $wgTitle->getLocalUrl() ) ) . Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . diff --git a/includes/SpecialMergeHistory.php b/includes/SpecialMergeHistory.php new file mode 100644 index 00000000..c7f42fe9 --- /dev/null +++ b/includes/SpecialMergeHistory.php @@ -0,0 +1,423 @@ +<?php + +/** + * Special page allowing users with the appropriate permissions to + * merge article histories, with some restrictions + * + * @addtogroup 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. + * @addtogroup 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" ); + $logViewer = new LogViewer( + new LogReader( + new FauxRequest( + array( 'page' => $this->mTargetObj->getPrefixedText(), + 'type' => 'merge' ) ) ) ); + $logViewer->showList( $wgOut ); + + # Slip in the hidden controls here + # 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)) { + if($size == 0) + $stxt = wfMsgHtml('historyempty'); + else + $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $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) : ''; + + $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; + } + # Leave the latest version no matter what + $lasttime = $dbw->selectField( array('page','revision'), + 'rev_timestamp', + array('page_id' => $this->mTargetID, 'page_latest = rev_id' ), + __METHOD__ ); + # Take the most restrictive of the twain + $maxtimestamp = ($lasttime < $maxtimestamp) ? $lasttime : $maxtimestamp; + // $this->mTimestamp must be less 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,$maxtimestamp); + } + + $dbw->update( 'revision', + array( 'rev_page' => $this->mDestID ), + array( 'rev_page' => $this->mTargetID, + $timewhere ), + __METHOD__ ); + # Check if this did anything + if( !$count = $dbw->affectedRows() ) { + $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(), $title, $title2 ) { + $this->mForm = $form; + $this->mConds = $conds; + $this->title = $title; + $this->articleID = $title->getArticleID(); + + $dbr = wfGetDB( DB_SLAVE ); + $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)', + array('rev_page' => $title2->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}"; + # Skip the latest one, as that could cause problems + if( $page = $this->title->getLatestRevID() ) + $conds[] = "rev_id != {$page}"; + + 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/SpecialMostlinked.php b/includes/SpecialMostlinked.php index b4de0a0e..916f219b 100644 --- a/includes/SpecialMostlinked.php +++ b/includes/SpecialMostlinked.php @@ -39,7 +39,7 @@ class MostlinkedPage extends QueryPage { /** * Pre-fill the link cache */ - function preprocessResults( &$db, &$res ) { + function preprocessResults( $db, $res ) { if( $db->numRows( $res ) > 0 ) { $linkBatch = new LinkBatch(); while( $row = $db->fetchObject( $res ) ) diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php index d0a99b3b..c357c8f4 100644 --- a/includes/SpecialMostlinkedcategories.php +++ b/includes/SpecialMostlinkedcategories.php @@ -35,7 +35,7 @@ class MostlinkedCategoriesPage extends QueryPage { /** * Fetch user page links and cache their existence */ - function preprocessResults( &$db, &$res ) { + function preprocessResults( $db, $res ) { $batch = new LinkBatch; while ( $row = $db->fetchObject( $res ) ) $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); diff --git a/includes/SpecialMostlinkedtemplates.php b/includes/SpecialMostlinkedtemplates.php index e7e7afcc..b0f1b196 100644 --- a/includes/SpecialMostlinkedtemplates.php +++ b/includes/SpecialMostlinkedtemplates.php @@ -17,7 +17,7 @@ class SpecialMostlinkedtemplates extends QueryPage { public function getName() { return 'Mostlinkedtemplates'; } - + /** * Is this report expensive, i.e should it be cached? * @@ -26,7 +26,7 @@ class SpecialMostlinkedtemplates extends QueryPage { public function isExpensive() { return true; } - + /** * Is there a feed available? * @@ -44,7 +44,7 @@ class SpecialMostlinkedtemplates extends QueryPage { public function sortDescending() { return true; } - + /** * Generate SQL for the report * @@ -60,9 +60,9 @@ class SpecialMostlinkedtemplates extends QueryPage { COUNT(*) AS value FROM {$templatelinks} WHERE tl_namespace = " . NS_TEMPLATE . " - GROUP BY 1, 2, 3"; + GROUP BY 1, 2, 3"; } - + /** * Pre-cache page existence to speed up link generation * @@ -79,7 +79,7 @@ class SpecialMostlinkedtemplates extends QueryPage { if( $dbr->numRows( $res ) > 0 ) $dbr->dataSeek( $res, 0 ); } - + /** * Format a result row * @@ -99,7 +99,7 @@ class SpecialMostlinkedtemplates extends QueryPage { return "Invalid title in result set; {$tsafe}"; } } - + /** * Make a "what links here" link for a given title * @@ -115,7 +115,6 @@ class SpecialMostlinkedtemplates extends QueryPage { $wgLang->formatNum( $result->value ) ); return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() ); } - } /** @@ -128,4 +127,3 @@ function wfSpecialMostlinkedtemplates( $par = false ) { $mlt = new SpecialMostlinkedtemplates(); $mlt->doQuery( $offset, $limit ); } - diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php index cfc434ae..e0a89bc2 100644 --- a/includes/SpecialMovepage.php +++ b/includes/SpecialMovepage.php @@ -65,7 +65,7 @@ class MovePageForm { $this->watch = $wgRequest->getCheck( 'wpWatch' ); } - function showForm( $err ) { + function showForm( $err, $hookErr = '' ) { global $wgOut, $wgUser, $wgContLang; $start = $wgContLang->isRTL() ? 'right' : 'left'; @@ -78,12 +78,15 @@ class MovePageForm { $wgOut->showErrorPage( 'notargettitle', 'notargettext' ); return; } + $sk = $wgUser->getSkin(); + $oldTitleLink = $sk->makeLinkObj( $ot ); $oldTitle = $ot->getPrefixedText(); $encOldTitle = htmlspecialchars( $oldTitle ); if( $this->newTitle == '' ) { # Show the current title as a default # when the form is first opened. + $newTitle = $oldTitle; $encNewTitle = $encOldTitle; } else { if( $err == '' ) { @@ -98,12 +101,13 @@ class MovePageForm { } } } - $encNewTitle = htmlspecialchars( $this->newTitle ); + $newTitle = $this->newTitle; + $encNewTitle = htmlspecialchars( $newTitle ); } $encReason = htmlspecialchars( $this->reason ); if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) { - $wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) ); + $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle ); $movepagebtn = wfMsgHtml( 'delete_and_move' ); $submitVar = 'wpDeleteAndMove'; $confirm = " @@ -112,7 +116,7 @@ class MovePageForm { </tr>"; $err = ''; } else { - $wgOut->addWikiText( wfMsg( 'movepagetext' ) ); + $wgOut->addWikiMsg( 'movepagetext' ); $movepagebtn = wfMsgHtml( 'movepagebtn' ); $submitVar = 'wpMove'; $confirm = false; @@ -122,7 +126,7 @@ class MovePageForm { $considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() ); if ( $considerTalk ) { - $wgOut->addWikiText( wfMsg( 'movepagetalktext' ) ); + $wgOut->addWikiMsg( 'movepagetalktext' ); } $movearticle = wfMsgHtml( 'movearticle' ); @@ -135,7 +139,13 @@ class MovePageForm { if ( $err != '' ) { $wgOut->setSubtitle( wfMsg( 'formerror' ) ); - $wgOut->addWikiText( '<p class="error">' . wfMsg($err) . "</p>\n" ); + $errMsg = ""; + if( $err == 'hookaborted' ) { + $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n"; + } else { + $errMsg = '<p><strong class="error">' . wfMsgWikiHtml( $err ) . "</strong></p>\n"; + } + $wgOut->addHTML( $errMsg ); } $moveTalkChecked = $this->moveTalk ? ' checked="checked"' : ''; @@ -145,7 +155,7 @@ class MovePageForm { <table border='0'> <tr> <td align='$end'>{$movearticle}</td> - <td align='$start'><strong>{$oldTitle}</strong></td> + <td align='$start'><strong>{$oldTitleLink}</strong></td> </tr> <tr> <td align='$end'><label for='wpNewTitle'>{$newtitle}</label></td> @@ -186,7 +196,7 @@ class MovePageForm { <input type='hidden' name='wpEditToken' value=\"{$token}\" /> </form>\n" ); - $this->showLogFragment( $ot, $wgOut ); + $this->showLogFragment( $ot, $wgOut ); } @@ -216,6 +226,12 @@ class MovePageForm { return; } + $hookErr = null; + if( !wfRunHooks( 'AbortMove', array( $ot, $nt, $wgUser, &$hookErr ) ) ) { + $this->showForm( 'hookaborted', $hookErr ); + return; + } + $error = $ot->moveTo( $nt, true, $this->reason ); if ( $error !== true ) { $this->showForm( $error ); @@ -280,21 +296,21 @@ class MovePageForm { $talkmoved = $wgRequest->getVal( 'talkmoved' ); $oldUrl = $old->getFullUrl( 'redirect=no' ); - $newUrl = $new->getFullURl(); + $newUrl = $new->getFullUrl(); $oldText = $old->getPrefixedText(); $newText = $new->getPrefixedText(); $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>"; $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>"; - $s = wfMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText ); + $s = wfMsgNoTrans( 'movepage-moved', $oldLink, $newLink, $oldText, $newText ); if ( $talkmoved == 1 ) { - $s .= "\n\n" . wfMsg( 'talkpagemoved' ); + $s .= "\n\n" . wfMsgNoTrans( 'talkpagemoved' ); } elseif( 'articleexists' == $talkmoved ) { - $s .= "\n\n" . wfMsg( 'talkexists' ); + $s .= "\n\n" . wfMsgNoTrans( 'talkexists' ); } else { if( !$old->isTalkPage() && $talkmoved != 'notalkpage' ) { - $s .= "\n\n" . wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ); + $s .= "\n\n" . wfMsgNoTrans( 'talkpagenotmoved', wfMsgNoTrans( $talkmoved ) ); } } $wgOut->addWikiText( $s ); diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php index f81a70f4..013b0986 100644 --- a/includes/SpecialNewimages.php +++ b/includes/SpecialNewimages.php @@ -200,7 +200,7 @@ function wfSpecialNewimages( $par, $specialPage ) { if ($shownav) $wgOut->addHTML( $prevnext ); } else { - $wgOut->addWikiText( wfMsg( 'noimages' ) ); + $wgOut->addWikiMsg( 'noimages' ); } } diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php index abd5e018..1c3bee84 100644 --- a/includes/SpecialNewpages.php +++ b/includes/SpecialNewpages.php @@ -4,18 +4,116 @@ * @addtogroup SpecialPage */ + +/** + * Start point + */ +function wfSpecialNewPages( $par, $specialPage ) { + $page = new NewPagesPage( $specialPage ); + $page->execute( $par ); +} + /** * implements Special:Newpages * @addtogroup SpecialPage */ class NewPagesPage extends QueryPage { - var $namespace; - var $username = ''; + protected $options = array(); + protected $nondefaults = array(); + protected $specialPage; + + public function __construct( $specialPage=null ) { + $this->specialPage = $specialPage; + } + + public function execute( $par ) { + global $wgRequest, $wgLang; + + $shownavigation = is_object( $this->specialPage ) && !$this->specialPage->including(); + + $defaults = array( + /* bool */ 'hideliu' => false, + /* bool */ 'hidepatrolled' => false, + /* bool */ 'hidebots' => false, + /* text */ 'namespace' => "0", + /* text */ 'username' => '', + /* int */ 'offset' => 0, + /* int */ 'limit' => 50, + ); + + $options = $defaults; + + if ( $par ) { + $bits = preg_split( '/\s*,\s*/', trim( $par ) ); + foreach ( $bits as $bit ) { + if ( 'shownav' == $bit ) + $shownavigation = true; + if ( 'hideliu' === $bit ) + $options['hideliu'] = true; + if ( 'hidepatrolled' == $bit ) + $options['hidepatrolled'] = true; + if ( 'hidebots' == $bit ) + $options['hidebots'] = true; + if ( is_numeric( $bit ) ) + $options['limit'] = intval( $bit ); + + $m = array(); + if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) + $options['limit'] = intval($m[1]); + if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) ) + $options['offset'] = intval($m[1]); + if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) { + $ns = $wgLang->getNsIndex( $m[1] ); + if( $ns !== false ) { + $options['namespace'] = $ns; + } + } + } + } + + // Override all values from requests, if specified + foreach ( $defaults as $v => $t ) { + if ( is_bool($t) ) { + $options[$v] = $wgRequest->getBool( $v, $options[$v] ); + } elseif( is_int($t) ) { + $options[$v] = $wgRequest->getInt( $v, $options[$v] ); + } elseif( is_string($t) ) { + $options[$v] = $wgRequest->getText( $v, $options[$v] ); + } + } + + // Validate limit and offset params + if ( $options['limit'] <= 0 ) { + $options['limit'] = $defaults['limit']; + } + + if ( $options['offset'] < 0 ) { + $options['offset'] = $defaults['offset']; + } + + $nondefaults = array(); + foreach ( $options as $v => $t ) { + if ( $v === 'offset' ) continue; # Reset offset if parameters change + wfAppendToArrayIfNotDefault( $v, $t, $defaults, $nondefaults ); + } + + # bind to class + $this->options = $options; + $this->nondefaults = $nondefaults; + + if ( !$this->doFeed( $wgRequest->getVal( 'feed' ), $options['limit'] ) ) { + $this->doQuery( $options['offset'], $options['limit'], $shownavigation ); + } + } - function NewPagesPage( $namespace = NS_MAIN, $username = '' ) { - $this->namespace = $namespace; - $this->username = $username; + function linkParameters() { + $nondefaults = $this->nondefaults; + // QueryPage seems to handle limit and offset itself + if ( isset( $nondefaults['limit'] ) ) { + unset($nondefaults['limit']); + } + return $nondefaults; } function getName() { @@ -27,29 +125,40 @@ class NewPagesPage extends QueryPage { return false; } - function makeUserWhere( &$dbo ) { - $title = Title::makeTitleSafe( NS_USER, $this->username ); - if( $title ) { - return ' AND rc_user_text = ' . $dbo->addQuotes( $title->getText() ); + function makeUserWhere( $db ) { + global $wgGroupPermissions; + $conds = array(); + if ($this->options['hidepatrolled']) { + $conds['rc_patrolled'] = 0; + } + if ($this->options['hidebots']) { + $conds['rc_bot'] = 0; + } + if ($wgGroupPermissions['*']['createpage'] == true && $this->options['hideliu']) { + $conds['rc_user'] = 0; } else { - return ''; + $title = Title::makeTitleSafe( NS_USER, $this->options['username'] ); + if( $title ) { + $conds['rc_user_text'] = $title->getText(); + } } - } - - private function makeNamespaceWhere() { - return $this->namespace !== 'all' - ? ' AND rc_namespace = ' . intval( $this->namespace ) - : ''; + return $conds; } function getSQL() { - global $wgUser, $wgUseRCPatrol; - $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0; + global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol; + $usepatrol = ( $wgUseNPPatrol || $wgUseRCPatrol ) ? 1 : 0; $dbr = wfGetDB( DB_SLAVE ); list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' ); - $nsfilter = $this->makeNamespaceWhere(); - $uwhere = $this->makeUserWhere( $dbr ); + $conds = array(); + $conds['rc_new'] = 1; + if ( $this->options['namespace'] !== 'all' ) { + $conds['rc_namespace'] = intval( $this->options['namespace'] ); + } + $conds['page_is_redirect'] = 0; + $conds += $this->makeUserWhere( $dbr ); + $condstext = $dbr->makeList( $conds, LIST_AND ); # FIXME: text will break with compression return @@ -68,23 +177,20 @@ class NewPagesPage extends QueryPage { page_len as length, page_latest as rev_id FROM $recentchanges,$page - WHERE rc_cur_id=page_id AND rc_new=1 - {$nsfilter} - AND page_is_redirect = 0 - {$uwhere}"; + WHERE rc_cur_id=page_id AND $condstext"; } - - function preprocessResults( &$dbo, &$res ) { + + function preprocessResults( $db, $res ) { # Do a batch existence check on the user and talk pages $linkBatch = new LinkBatch(); - while( $row = $dbo->fetchObject( $res ) ) { - $linkBatch->addObj( Title::makeTitleSafe( NS_USER, $row->user_text ) ); - $linkBatch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_text ) ); + while( $row = $db->fetchObject( $res ) ) { + $linkBatch->add( NS_USER, $row->user_text ); + $linkBatch->add( NS_USER_TALK, $row->user_text ); } $linkBatch->execute(); # Seek to start - if( $dbo->numRows( $res ) > 0 ) - $dbo->dataSeek( $res, 0 ); + if( $db->numRows( $res ) > 0 ) + $db->dataSeek( $res, 0 ); } /** @@ -116,8 +222,10 @@ class NewPagesPage extends QueryPage { * @return bool */ function patrollable( $result ) { - global $wgUser, $wgUseRCPatrol; - return $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) && !$result->patrolled; + global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; + return ( $wgUseRCPatrol || $wgUseNPPatrol ) + && $wgUser->isAllowed( 'patrol' ) + && !$result->patrolled; } function feedItemDesc( $row ) { @@ -131,82 +239,79 @@ class NewPagesPage extends QueryPage { } return parent::feedItemDesc( $row ); } - + /** * Show a form for filtering namespace and username * * @return string - */ + */ function getPageHeader() { - global $wgScript; + global $wgScript, $wgContLang, $wgGroupPermissions, $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; + $sk = $wgUser->getSkin(); + $align = $wgContLang->isRTL() ? 'left' : 'right'; $self = SpecialPage::getTitleFor( $this->getName() ); - $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - $form .= Xml::hidden( 'title', $self->getPrefixedDBkey() ); - # Namespace selector - $form .= '<table><tr><td align="right">' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '</td>'; - $form .= '<td>' . Xml::namespaceSelector( $this->namespace, 'all' ) . '</td></tr>'; - # Username filter - $form .= '<tr><td align="right">' . Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . '</td>'; - $form .= '<td>' . Xml::input( 'username', 30, $this->username, array( 'id' => 'mw-np-username' ) ) . '</td></tr>'; - - $form .= '<tr><td></td><td>' . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</td></tr></table>'; - $form .= Xml::hidden( 'offset', $this->offset ) . Xml::hidden( 'limit', $this->limit ) . '</form>'; - return $form; - } - - /** - * Link parameters - * - * @return array - */ - function linkParameters() { - return( array( 'namespace' => $this->namespace, 'username' => $this->username ) ); - } - -} -/** - * constructor - */ -function wfSpecialNewpages($par, $specialPage) { - global $wgRequest, $wgContLang; - - list( $limit, $offset ) = wfCheckLimits(); - $namespace = NS_MAIN; - $username = ''; - - if ( $par ) { - $bits = preg_split( '/\s*,\s*/', trim( $par ) ); - foreach ( $bits as $bit ) { - if ( 'shownav' == $bit ) - $shownavigation = true; - if ( is_numeric( $bit ) ) - $limit = $bit; - - $m = array(); - if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) - $limit = intval($m[1]); - if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) ) - $offset = intval($m[1]); - if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) { - $ns = $wgContLang->getNsIndex( $m[1] ); - if( $ns !== false ) { - $namespace = $ns; - } - } + // show/hide links + $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' )); + + $hidelinks = array(); + + if ( $wgGroupPermissions['*']['createpage'] === true ) { + $hidelinks['hideliu'] = 'rcshowhideliu'; } - } else { - if( $ns = $wgRequest->getText( 'namespace', NS_MAIN ) ) - $namespace = $ns; - if( $un = $wgRequest->getText( 'username' ) ) - $username = $un; - } - - if ( ! isset( $shownavigation ) ) - $shownavigation = ! $specialPage->including(); + if ( $wgUseNPPatrol || $wgUseRCPatrol ) { + $hidelinks['hidepatrolled'] = 'rcshowhidepatr'; + } + $hidelinks['hidebots'] = 'rcshowhidebots'; - $npp = new NewPagesPage( $namespace, $username ); + $links = array(); + foreach ( $hidelinks as $key => $msg ) { + $reversed = 1-$this->options[$key]; + $link = $sk->makeKnownLinkObj( $self, $showhide[$reversed], + wfArrayToCGI( array( $key => $reversed ), $this->nondefaults ) + ); + $links[$key] = wfMsgHtml( $msg, $link ); + } + + $hl = implode( ' | ', $links ); + + // Store query values in hidden fields so that form submission doesn't lose them + $hidden = array(); + foreach ( $this->nondefaults as $key => $value ) { + if ( $key === 'namespace' ) continue; + if ( $key === 'username' ) continue; + $hidden[] = Xml::hidden( $key, $value ); + } + $hidden = implode( "\n", $hidden ); - if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) ) - $npp->doQuery( $offset, $limit, $shownavigation ); -}
\ No newline at end of file + $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . + Xml::hidden( 'title', $self->getPrefixedDBkey() ) . + Xml::openElement( 'table' ) . + "<tr> + <td align=\"$align\">" . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + "</td> + <td>" . + Xml::namespaceSelector( $this->options['namespace'], 'all' ) . + "</td> + </tr> + <tr> + <td align=\"$align\">" . + Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . + "</td> + <td>" . + Xml::input( 'username', 30, $this->options['username'], array( 'id' => 'mw-np-username' ) ) . + "</td> + </tr> + <tr> <td></td> + <td>" . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + "</td> + </tr>" . + "<tr><td></td><td>" . $hl . "</td></tr>" . + Xml::closeElement( 'table' ) . + $hidden . + Xml::closeElement( 'form' ); + return $form; + } +} diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 89fd15bb..c9037ea7 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -80,6 +80,7 @@ class SpecialPage 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), + 'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ), 'Preferences' => array( 'SpecialPage', 'Preferences' ), 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), @@ -89,36 +90,37 @@ class SpecialPage 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), 'Listusers' => array( 'SpecialPage', 'Listusers' ), 'Statistics' => array( 'SpecialPage', 'Statistics' ), - 'Randompage' => array( 'SpecialPage', 'Randompage' ), + 'Randompage' => 'Randompage', 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), - 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), + 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), - 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), + 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), - 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ), + 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ), 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), 'Longpages' => array( 'SpecialPage', 'Longpages' ), 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ), 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), 'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ), + 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ), 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ), 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) , 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), 'Contributions' => array( 'SpecialPage', 'Contributions' ), 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), - 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ), + 'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ), 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ), 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), @@ -131,23 +133,26 @@ class SpecialPage 'Log' => array( 'SpecialPage', 'Log' ), 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), - 'Import' => array( 'SpecialPage', "Import", 'import' ), + 'Import' => array( 'SpecialPage', 'Import', 'import' ), 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), - 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ), + 'Userrights' => 'UserrightsPage', 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), - 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ), + 'Revisiondelete' => array( 'UnlistedSpecialPage', 'Revisiondelete', 'deleterevision' ), 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), - 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ), - 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), + 'Randomredirect' => 'SpecialRandomredirect', + 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), + 'Filepath' => array( 'SpecialPage', 'Filepath' ), 'Mypage' => array( 'SpecialMypage' ), 'Mytalk' => array( 'SpecialMytalk' ), 'Mycontributions' => array( 'SpecialMycontributions' ), 'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ), - ); + 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ), + 'Listbots' => array( 'SpecialRedirectToSpecial', 'Listbots', 'Listusers', 'bot' ), + ); static public $mAliases; static public $mListInitialised = false; @@ -349,7 +354,7 @@ class SpecialPage foreach ( self::$mList as $name => $rec ) { $page = self::getPage( $name ); - if ( $page->isListed() && $page->getRestriction() == '' ) { + if ( $page->isListed() && !$page->isRestricted() ) { $pages[$name] = $page; } } @@ -370,11 +375,12 @@ class SpecialPage foreach ( self::$mList as $name => $rec ) { $page = self::getPage( $name ); - if ( $page->isListed() ) { - $restriction = $page->getRestriction(); - if ( $restriction != '' && $wgUser->isAllowed( $restriction ) ) { - $pages[$name] = $page; - } + if ( + $page->isListed() + and $page->isRestricted() + and $page->userCanExecute( $wgUser ) + ) { + $pages[$name] = $page; } } return $pages; @@ -404,7 +410,6 @@ class SpecialPage $par = $bits[1]; } $page = SpecialPage::getPageByAlias( $name ); - # Nonexistent? if ( !$page ) { if ( !$including ) { @@ -486,6 +491,11 @@ class SpecialPage /** * Get the local name for a specified canonical name + * + * @param $name + * @param mixed $subpage Boolean false, or string + * + * @return string */ static function getLocalNameFor( $name, $subpage = false ) { global $wgContLang; @@ -603,10 +613,25 @@ class SpecialPage } /** + * Can be overridden by subclasses with more complicated permissions + * schemes. + * + * @return bool Should the page be displayed with the restricted-access + * pages? + */ + public function isRestricted() { + return $this->mRestriction != ''; + } + + /** * Checks if the given user (identified by an object) can execute this - * special page (as defined by $mRestriction) + * special page (as defined by $mRestriction). Can be overridden by sub- + * classes with more complicated permissions schemes. + * + * @param User $user The user to check + * @return bool Does the user have permission to view the page? */ - function userCanExecute( &$user ) { + public function userCanExecute( $user ) { return $user->isAllowed( $this->mRestriction ); } @@ -642,7 +667,7 @@ class SpecialPage if ( $this->userCanExecute( $wgUser ) ) { $func = $this->mFunction; // only load file if the function does not exist - if(!function_exists($func) and $this->mFile) { + if(!is_callable($func) and $this->mFile) { require_once( $this->mFile ); } # FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage. @@ -650,7 +675,7 @@ class SpecialPage $this->outputHeader(); if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) ) return; - $func( $par, $this ); + call_user_func( $func, $par, $this ); if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) ) return; } else { @@ -662,9 +687,10 @@ class SpecialPage global $wgOut, $wgContLang; $msg = $wgContLang->lc( $this->name() ) . '-summary'; - $out = wfMsg( $msg ); - if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) + $out = wfMsgNoTrans( $msg ); + if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) { $wgOut->addWikiText( $out ); + } } @@ -776,7 +802,7 @@ class SpecialRedirectToSpecial extends UnlistedSpecialPage { class SpecialMypage extends UnlistedSpecialPage { function __construct() { parent::__construct( 'Mypage' ); - $this->mAllowedRedirectParams = array( 'action' ); + $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', 'section' ); } function getRedirect( $subpage ) { @@ -796,7 +822,7 @@ class SpecialMypage extends UnlistedSpecialPage { class SpecialMytalk extends UnlistedSpecialPage { function __construct() { parent::__construct( 'Mytalk' ); - $this->mAllowedRedirectParams = array( 'action' ); + $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', 'section' ); } function getRedirect( $subpage ) { @@ -823,5 +849,3 @@ class SpecialMycontributions extends UnlistedSpecialPage { return SpecialPage::getTitleFor( 'Contributions', $wgUser->getName() ); } } - - diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php index a36be289..ca163a6a 100644 --- a/includes/SpecialPreferences.php +++ b/includes/SpecialPreferences.php @@ -24,7 +24,7 @@ class PreferencesForm { var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick; var $mUserLanguage, $mUserVariant; var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; - var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize; + var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize; var $mUnderline, $mWatchlistEdits; /** @@ -65,6 +65,7 @@ class PreferencesForm { $this->mSuccess = $request->getCheck( 'success' ); $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' ); $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' ); + $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' ); $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) && $this->mPosted && @@ -98,7 +99,7 @@ class PreferencesForm { $this->mUserLanguage = 'nolanguage'; } - wfRunHooks( "InitPreferencesForm", array( $this, $request ) ); + wfRunHooks( 'InitPreferencesForm', array( $this, $request ) ); } function execute() { @@ -207,29 +208,29 @@ class PreferencesForm { function savePreferences() { global $wgUser, $wgOut, $wgParser; global $wgEnableUserEmail, $wgEnableEmail; - global $wgEmailAuthentication; - global $wgAuth; + global $wgEmailAuthentication, $wgRCMaxAge; + global $wgAuth, $wgEmailConfirmToEdit; if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) { if ( $this->mNewpass != $this->mRetypePass ) { - wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'badretype' ) ); + 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' ) ); + 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' ) ); + wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) ); $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; } catch( PasswordError $e ) { - wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'error' ) ); + wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) ); $this->mainPrefsForm( 'error', $e->getMessage() ); return; } @@ -250,7 +251,7 @@ class PreferencesForm { wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) ); return; } elseif( $this->mToggles['fancysig'] ) { - if( Parser::validateSig( $this->mNick ) !== false ) { + if( $wgParser->validateSig( $this->mNick ) !== false ) { $this->mNick = $wgParser->cleanSig( $this->mNick ); } else { $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) ); @@ -275,7 +276,7 @@ class PreferencesForm { $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, 7 ) ); + $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 ) ); @@ -285,6 +286,7 @@ class PreferencesForm { $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 ); # Set search namespace options foreach( $this->mSearchNs as $i => $value ) { @@ -299,20 +301,6 @@ class PreferencesForm { foreach ( $this->mToggles as $tname => $tvalue ) { $wgUser->setOption( $tname, $tvalue ); } - if (!$wgAuth->updateExternalDB($wgUser)) { - $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); - return; - } - - $msg = ''; - if ( !wfRunHooks( "SavePreferences", array( $this, $wgUser, &$msg ) ) ) { - print "(($msg))"; - $this->mainPrefsForm( 'error', $msg ); - return; - } - - $wgUser->setCookies(); - $wgUser->saveSettings(); $error = false; if( $wgEnableEmail ) { @@ -323,7 +311,6 @@ class PreferencesForm { if( $wgUser->isValidEmailAddr( $newadr ) ) { $wgUser->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record $wgUser->mEmailAuthenticated = null; # but flag as "dirty" = unauthenticated - $wgUser->saveSettings(); if ($wgEmailAuthentication) { # Mail a temporary password to the dirty address. # User can come back through the confirmation URL to re-enable email. @@ -338,17 +325,34 @@ class PreferencesForm { $error = wfMsg( 'invalidemailaddress' ); } } else { + if( $wgEmailConfirmToEdit && empty( $newadr ) ) { + $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) ); + return; + } $wgUser->setEmail( $this->mUserEmail ); - $wgUser->setCookies(); - $wgUser->saveSettings(); } if( $oldadr != $newadr ) { - wfRunHooks( "PrefsEmailAudit", array( $wgUser, $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 ) ) ) { + print "(($msg))"; + $this->mainPrefsForm( 'error', $msg ); + return; + } + + $wgUser->setCookies(); + $wgUser->saveSettings(); + if( $needRedirect && $error === false ) { - $title =& SpecialPage::getTitleFor( "Preferences" ); + $title = SpecialPage::getTitleFor( 'Preferences' ); $wgOut->redirect($title->getFullURL('success')); return; } @@ -393,6 +397,7 @@ class PreferencesForm { $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' ); $this->mUnderline = $wgUser->getOption( 'underline' ); $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' ); + $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' ); $togs = User::getToggles(); foreach ( $togs as $tname ) { @@ -406,7 +411,7 @@ class PreferencesForm { } } - wfRunHooks( "ResetPreferences", array( $this, $wgUser ) ); + wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) ); } /** @@ -510,6 +515,7 @@ class PreferencesForm { global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress; global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication; global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth; + global $wgEmailConfirmToEdit, $wgAjaxSearch; $wgOut->setPageTitle( wfMsg( 'preferences' ) ); $wgOut->setArticleRelated( false ); @@ -518,11 +524,11 @@ class PreferencesForm { $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. if ( $this->mSuccess || 'success' == $status ) { - $wgOut->addWikitext( '<div class="successbox"><strong>'. wfMsg( 'savedprefs' ) . '</strong></div>' ); + $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' ); } else if ( 'error' == $status ) { - $wgOut->addWikitext( '<div class="errorbox"><strong>' . $message . '</strong></div>' ); + $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message . '</strong></div>' ); } else if ( '' != $status ) { - $wgOut->addWikitext( $message . "\n----" ); + $wgOut->addWikiText( $message . "\n----" ); } $qbs = $wgLang->getQuickbarSettings(); @@ -619,7 +625,7 @@ class PreferencesForm { Xml::label( wfMsg('youremail'), 'wpUserEmail' ), Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ), Xml::tags('div', array( 'class' => 'prefsectiontip' ), - wfMsgExt( 'prefs-help-email', 'parseinline' ) + wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' ) ) ) ); @@ -967,7 +973,13 @@ class PreferencesForm { $wgOut->addHtml( '</fieldset>' ); # Search + $ajaxsearch = $wgAjaxSearch ? + $this->addRow( + wfLabel( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ), + wfCheck( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) ) + ) : ''; $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' . + $ajaxsearch . $this->addRow( wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ), wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) ) @@ -1010,7 +1022,7 @@ class PreferencesForm { } $wgOut->addHTML( '</fieldset>' ); - wfRunHooks( "RenderPreferencesForm", array( $this, $wgOut ) ); + wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) ); $token = htmlspecialchars( $wgUser->editToken() ); $skin = $wgUser->getSkin(); diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php index 6bb26d67..bfab21b6 100644 --- a/includes/SpecialPrefixindex.php +++ b/includes/SpecialPrefixindex.php @@ -40,11 +40,11 @@ function wfSpecialPrefixIndex( $par=NULL, $specialPage ) { * @addtogroup SpecialPage */ class SpecialPrefixindex extends SpecialAllpages { - var $maxPerPage=960; - var $topLevelMax=50; - var $name='Prefixindex'; - # Determines, which message describes the input field 'nsfrom', used in function namespaceForm (see superclass SpecialAllpages) - var $nsfromMsg='allpagesprefix'; + // Inherit $maxPerPage + + // Define other properties + protected $name = 'Prefixindex'; + protected $nsfromMsg = 'allpagesprefix'; /** * @param integer $namespace (Default NS_MAIN) diff --git a/includes/SpecialProtectedpages.php b/includes/SpecialProtectedpages.php index 122ca8fc..60a8d602 100644 --- a/includes/SpecialProtectedpages.php +++ b/includes/SpecialProtectedpages.php @@ -52,7 +52,7 @@ class ProtectedPagesForm { * Callback function to output a restriction */ function formatRow( $row ) { - global $wgUser, $wgLang; + global $wgUser, $wgLang, $wgContLang; wfProfileIn( __METHOD__ ); @@ -89,6 +89,7 @@ class ProtectedPagesForm { $stxt = ' <small>' . wfMsgHtml('historyempty') . '</small>'; else $stxt = ' <small>' . wfMsgHtml('historysize', $wgLang->formatNum( $size ) ) . '</small>'; + $stxt = $wgContLang->getDirMark() . $stxt; } wfProfileOut( __METHOD__ ); diff --git a/includes/SpecialProtectedtitles.php b/includes/SpecialProtectedtitles.php new file mode 100755 index 00000000..4bc303bb --- /dev/null +++ b/includes/SpecialProtectedtitles.php @@ -0,0 +1,219 @@ +<?php +/** + * + * @addtogroup SpecialPage + */ + +/** + * @todo document + * @addtogroup 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 + * @addtogroup 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/SpecialRandompage.php b/includes/SpecialRandompage.php index 42734274..9f324bd0 100644 --- a/includes/SpecialRandompage.php +++ b/includes/SpecialRandompage.php @@ -9,56 +9,56 @@ */ /** - * Main execution point - * @param $par Namespace to select the page from - */ -function wfSpecialRandompage( $par = null ) { - global $wgOut, $wgContLang; - - $rnd = new RandomPage(); - $rnd->setNamespace( $wgContLang->getNsIndex( $par ) ); - $rnd->setRedirect( false ); - - $title = $rnd->getRandomTitle(); - - if( is_null( $title ) ) { - $wgOut->addWikiText( wfMsg( 'randompage-nopages' ) ); - return; - } - - $wgOut->reportTime(); - $wgOut->redirect( $title->getFullUrl() ); -} - - -/** * Special page to direct the user to a random page * * @addtogroup SpecialPage */ -class RandomPage { +class RandomPage extends SpecialPage { private $namespace = NS_MAIN; // namespace to select pages from - private $redirect = false; // select redirects instead of normal pages? - public function getNamespace ( ) { + 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; } - public function getRedirect ( ) { - return $this->redirect; + + // select redirects instead of normal pages? + // Overriden by SpecialRandomredirect + public function isRedirect(){ + return false; } - public function setRedirect ( $redirect ) { - $this->redirect = $redirect; + + 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 ( ) { + public function getRandomTitle() { $randstr = wfRandom(); $row = $this->selectRandomPageFromDB( $randstr ); @@ -78,7 +78,7 @@ class RandomPage { return null; } - private function selectRandomPageFromDB ( $randstr ) { + private function selectRandomPageFromDB( $randstr ) { global $wgExtraRandompageSQL; $fname = 'RandomPage::selectRandomPageFromDB'; @@ -88,7 +88,7 @@ class RandomPage { $page = $dbr->tableName( 'page' ); $ns = (int) $this->namespace; - $redirect = $this->redirect ? 1 : 0; + $redirect = $this->isRedirect() ? 1 : 0; $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : ""; $sql = "SELECT page_title diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php index b7aa3e49..ccf5cbcd 100644 --- a/includes/SpecialRandomredirect.php +++ b/includes/SpecialRandomredirect.php @@ -7,27 +7,14 @@ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen * @license GNU General Public Licence 2.0 or later */ - -/** - * Main execution point - * @param $par Namespace to select the redirect from - */ -function wfSpecialRandomredirect( $par = null ) { - global $wgOut, $wgContLang; - - $rnd = new RandomPage(); - $rnd->setNamespace( $wgContLang->getNsIndex( $par ) ); - $rnd->setRedirect( true ); - - $title = $rnd->getRandomTitle(); - - if( is_null( $title ) ) { - $wgOut->addWikiText( wfMsg( 'randomredirect-nopages' ) ); - return; +class SpecialRandomredirect extends RandomPage { + function __construct(){ + parent::__construct( 'Randomredirect' ); } - $wgOut->reportTime(); - $wgOut->redirect( $title->getFullUrl( 'redirect=no' ) ); + // Override parent::isRedirect() + public function isRedirect(){ + return true; + } } - diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php index 7565481b..60a04e00 100644 --- a/includes/SpecialRecentchanges.php +++ b/includes/SpecialRecentchanges.php @@ -172,13 +172,9 @@ function wfSpecialRecentchanges( $par, $specialPage ) { while( $row = $dbr->fetchObject( $res ) ){ $rows[] = $row; if ( !$feedFormat ) { - // User page link - $title = Title::makeTitleSafe( NS_USER, $row->rc_user_text ); - $batch->addObj( $title ); - - // User talk - $title = Title::makeTitleSafe( NS_USER_TALK, $row->rc_user_text ); - $batch->addObj( $title ); + // User page and talk links + $batch->add( NS_USER, $row->rc_user_text ); + $batch->add( NS_USER_TALK, $row->rc_user_text ); } } @@ -221,7 +217,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) { // And now for the content $wgOut->setSyndicated( true ); - + $list = ChangesList::newFromUser( $wgUser ); if ( $wgAllowCategorizedRecentChanges ) { @@ -233,6 +229,10 @@ function wfSpecialRecentchanges( $par, $specialPage ) { $s = $list->beginRecentChangesList(); $counter = 1; + + $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' ); + $watcherCache = array(); + foreach( $rows as $obj ){ if( $limit == 0) { break; @@ -251,13 +251,19 @@ function wfSpecialRecentchanges( $par, $specialPage ) { $rc->notificationtimestamp = false; } - if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) { - $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" . $dbr->strencode($obj->rc_title) ."' AND wl_namespace=$obj->rc_namespace" ; - $res3 = $dbr->query( $sql3, 'wfSpecialRecentChanges'); - $x = $dbr->fetchObject( $res3 ); - $rc->numberofWatchingusers = $x->n; - } else { - $rc->numberofWatchingusers = 0; + $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; @@ -269,6 +275,10 @@ function wfSpecialRecentchanges( $par, $specialPage ) { } function rcFilterByCategories ( &$rows , $categories , $any ) { + if( empty( $categories ) ) { + return; + } + # Filter categories $cats = array () ; foreach ( $categories AS $cat ) { @@ -281,7 +291,7 @@ function rcFilterByCategories ( &$rows , $categories , $any ) { $articles = array () ; $a2r = array () ; foreach ( $rows AS $k => $r ) { - $nt = Title::newFromText ( $r->rc_title , $r->rc_namespace ) ; + $nt = Title::makeTitle( $r->rc_title , $r->rc_namespace ); $id = $nt->getArticleID() ; if ( $id == 0 ) continue ; # Page might have been deleted... if ( !in_array ( $id , $articles ) ) { @@ -332,6 +342,14 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ), $wgTitle->getFullUrl() ); + //purge cache if requested + global $wgRequest, $wgUser; + $purge = $wgRequest->getVal( 'action' ) == 'purge'; + if ( $purge && $wgUser->isAllowed('purge') ) { + $messageMemc->delete( $timekey ); + $messageMemc->delete( $key ); + } + /** * Bumping around loading up diffs can be pretty slow, so where * possible we want to cache the feed output so the next visitor @@ -373,9 +391,12 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { return true; } +/** + * @todo document + * @param $rows Database resource with recentchanges rows + */ function rcDoOutputFeed( $rows, &$feed ) { - $fname = 'rcDoOutputFeed'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $feed->outHeader(); @@ -400,7 +421,7 @@ function rcDoOutputFeed( $rows, &$feed ) { $item = new FeedItem( $title->getPrefixedText(), rcFormatDiff( $obj ), - $title->getFullURL(), + $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ), $obj->rc_timestamp, $obj->rc_user_text, $talkpage->getFullURL() @@ -408,7 +429,7 @@ function rcDoOutputFeed( $rows, &$feed ) { $feed->outItem( $item ); } $feed->outFooter(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -622,7 +643,13 @@ function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) { $skin = $wgUser->getSkin(); $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n"; - if( $title->getNamespace() >= 0 && $title->userCan( 'read' ) ) { + //NOTE: Check permissions for anonymous users, not current user. + // No "privileged" version should end up in the cache. + // Most feed readers will not log in anway. + $anon = new User(); + $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true ); + + if( $title->getNamespace() >= 0 && !$accErrors ) { if( $oldid ) { wfProfileIn( "$fname-dodiff" ); diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php index 2a8ac32d..bc6bbf4a 100644 --- a/includes/SpecialRecentchangeslinked.php +++ b/includes/SpecialRecentchangeslinked.php @@ -14,7 +14,7 @@ require_once( 'SpecialRecentchanges.php' ); * @param string $par parent page we will look at */ function wfSpecialRecentchangeslinked( $par = NULL ) { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest; + global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle; $fname = 'wfSpecialRecentchangeslinked'; $days = $wgRequest->getInt( 'days' ); @@ -36,7 +36,8 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { $id = $nt->getArticleId(); $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) ); - $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) ); + $wgOut->setSyndicated(); + $wgOut->setFeedAppendQuery( "target=" . urlencode( $target ) ); if ( ! $days ) { $days = (int)$wgUser->getOption( 'rcdays', 7 ); @@ -75,7 +76,7 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { // If target is a Category, use categorylinks and invert from and to if( $nt->getNamespace() == NS_CATEGORY ) { - $catkey = $dbr->addQuotes( $nt->getDBKey() ); + $catkey = $dbr->addQuotes( $nt->getDBkey() ); $sql = "SELECT /* wfSpecialRecentchangeslinked */ rc_id, rc_cur_id, @@ -152,6 +153,7 @@ $GROUPBY $s = $list->beginRecentChangesList(); $count = $dbr->numRows( $res ); + $rchanges = array(); if ( $count ) { $counter = 1; while ( $limit ) { @@ -162,14 +164,27 @@ $GROUPBY $rc->counter = $counter++; $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) ); --$limit; + $rchanges[] = $obj; } } else { - $wgOut->addWikiText( wfMsg('recentchangeslinked-noresult') ); + $wgOut->addWikiMsg('recentchangeslinked-noresult'); } $s .= $list->endRecentChangesList(); $dbr->freeResult( $res ); $wgOut->addHTML( $s ); + + global $wgSitename, $wgFeedClasses, $wgContLanguageCode; + $feedFormat = $wgRequest->getVal( 'feed' ); + if( $feedFormat && isset( $wgFeedClasses[$feedFormat] ) ) { + $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchangeslinked-title', $nt->getPrefixedText() ) . ' [' . $wgContLanguageCode . ']'; + $feed = new $wgFeedClasses[$feedFormat]( $feedTitle, + htmlspecialchars( wfMsgForContent('recentchangeslinked') ), $wgTitle->getFullUrl() ); + + require_once( "SpecialRecentchanges.php" ); + $wgOut->disable(); + rcDoOutputFeed( $rchanges, $feed ); + } } diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php index 281a78b6..2ecd15b0 100644 --- a/includes/SpecialResetpass.php +++ b/includes/SpecialResetpass.php @@ -25,7 +25,7 @@ class PasswordResetForm extends SpecialPage { /** * Main execution point */ - function execute( $par='' ) { + function execute( $par ) { global $wgUser, $wgAuth, $wgOut, $wgRequest; if( !$wgAuth->allowPasswordChange() ) { @@ -43,7 +43,7 @@ class PasswordResetForm extends SpecialPage { $retype = $wgRequest->getVal( 'wpRetype' ); try { $this->attemptReset( $newpass, $retype ); - $wgOut->addWikiText( wfMsg( 'resetpass_success' ) ); + $wgOut->addWikiMsg( 'resetpass_success' ); $data = array( 'action' => 'submitlogin', diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php index 34e9dfbc..b6ca7e14 100644 --- a/includes/SpecialRevisiondelete.php +++ b/includes/SpecialRevisiondelete.php @@ -66,7 +66,7 @@ class RevisionDeleteForm { function show( $request ) { global $wgOut, $wgUser; - $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) ); + $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText() ); $wgOut->addHtml( "<ul>" ); foreach( $this->revisions as $revid ) { @@ -80,7 +80,7 @@ class RevisionDeleteForm { } $wgOut->addHtml( "</ul>" ); - $wgOut->addWikiText( wfMsg( 'revdelete-text' ) ); + $wgOut->addWikiMsg( 'revdelete-text' ); $items = array( wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ), diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php index 3fc8bab4..dcbbb903 100644 --- a/includes/SpecialSearch.php +++ b/includes/SpecialSearch.php @@ -103,7 +103,11 @@ class SpecialSearch { return; } } - $wgOut->addWikiText( wfMsg( 'noexactmatch', wfEscapeWikiText( $term ) ) ); + if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) { + $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) ); + } else { + $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) ); + } return $this->showResults( $term ); } @@ -119,12 +123,13 @@ class SpecialSearch { $this->setupPage( $term ); global $wgOut; - $wgOut->addWikiText( wfMsg( 'searchresulttext' ) ); + $wgOut->addWikiMsg( 'searchresulttext' ); - #if ( !$this->parseQuery() ) { if( '' === trim( $term ) ) { + // Empty query -- straight view of search form $wgOut->setSubtitle( '' ); $wgOut->addHTML( $this->powerSearchBox( $term ) ); + $wgOut->addHTML( $this->powerSearchFocus() ); wfProfileOut( $fname ); return; } @@ -155,6 +160,15 @@ class SpecialSearch { $search->setNamespaces( $this->namespaces ); $search->showRedirects = $this->searchRedirects; $titleMatches = $search->searchTitle( $term ); + + // Sometimes the search engine knows there are too many hits + if ($titleMatches instanceof SearchResultTooMany) { + $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); + $wgOut->addHTML( $this->powerSearchBox( $term ) ); + $wgOut->addHTML( $this->powerSearchFocus() ); + wfProfileOut( $fname ); + return; + } $textMatches = $search->searchText( $term ); $num = ( $titleMatches ? $titleMatches->numRows() : 0 ) @@ -180,27 +194,27 @@ class SpecialSearch { if( $titleMatches ) { if( $titleMatches->numRows() ) { - $wgOut->addWikiText( '==' . wfMsg( 'titlematches' ) . "==\n" ); + $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); $wgOut->addHTML( $this->showMatches( $titleMatches ) ); } else { - $wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" ); + $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' ); } $titleMatches->free(); } if( $textMatches ) { if( $textMatches->numRows() ) { - $wgOut->addWikiText( '==' . wfMsg( 'textmatches' ) . "==\n" ); + $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); $wgOut->addHTML( $this->showMatches( $textMatches ) ); } elseif( $num == 0 ) { # Don't show the 'no text matches' if we received title matches - $wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" ); + $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); } $textMatches->free(); } if ( $num == 0 ) { - $wgOut->addWikiText( wfMsg( 'nonefound' ) ); + $wgOut->addWikiMsg( 'nonefound' ); } if( $num || $this->offset ) { $wgOut->addHTML( "<p>{$prevnext}</p>\n" ); @@ -387,8 +401,9 @@ class SpecialSearch { if( '' == $name ) { $name = wfMsg( 'blanknamespace' ); } + $encName = htmlspecialchars( $name ); $namespaces .= " <label><input type='checkbox' value=\"1\" name=\"" . - "ns{$ns}\"{$checked} />{$name}</label>\n"; + "ns{$ns}\"{$checked} />{$encName}</label>\n"; } $checked = $this->searchRedirects @@ -396,7 +411,7 @@ class SpecialSearch { : ''; $redirect = "<input type='checkbox' value='1' name=\"redirs\"{$checked} />\n"; - $searchField = '<input type="text" name="search" value="' . + $searchField = '<input type="text" id="powerSearchText" name="search" value="' . htmlspecialchars( $term ) ."\" size=\"16\" />\n"; $searchButton = '<input type="submit" name="searchx" value="' . @@ -412,6 +427,12 @@ class SpecialSearch { return "<br /><br />\n<form id=\"powersearch\" method=\"get\" " . "action=\"$action\">\n{$ret}\n</form>\n"; } + + function powerSearchFocus() { + return "<script type='text/javascript'>" . + "document.getElementById('powerSearchText').focus();" . + "</script>"; + } } diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php index 973656dd..5aa36386 100644 --- a/includes/SpecialShortpages.php +++ b/includes/SpecialShortpages.php @@ -41,7 +41,7 @@ class ShortPagesPage extends QueryPage { WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0"; } - function preprocessResults( &$db, &$res ) { + 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() ) { diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php index a893966c..4ea956b8 100644 --- a/includes/SpecialSpecialpages.php +++ b/includes/SpecialSpecialpages.php @@ -12,7 +12,7 @@ function wfSpecialSpecialpages() { $wgMessageCache->loadAllMessages(); - $wgOut->setRobotpolicy( 'index,nofollow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed? $sk = $wgUser->getSkin(); /** Pages available to all */ diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php index a29811da..983dc896 100644 --- a/includes/SpecialStatistics.php +++ b/includes/SpecialStatistics.php @@ -32,7 +32,7 @@ function wfSpecialStatistics( $par = '' ) { return; } else { $text = "__NOTOC__\n"; - $text .= '==' . wfMsg( 'sitestats' ) . "==\n"; + $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n"; $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ), $wgLang->formatNum( $total ), $wgLang->formatNum( $good ), @@ -44,7 +44,7 @@ function wfSpecialStatistics( $par = '' ) { $wgLang->formatNum( $images ) )."\n"; - $text .= "==" . wfMsg( 'userstats' ) . "==\n"; + $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n"; $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ), $wgLang->formatNum( $users ), $wgLang->formatNum( $admins ), @@ -73,7 +73,7 @@ function wfSpecialStatistics( $par = '' ) { ) ); if( $res->numRows() > 0 ) { - $text .= "==" . wfMsg( 'statistics-mostpopular' ) . "==\n"; + $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n"; while( $row = $res->fetchObject() ) { $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); if( $title instanceof Title ) @@ -83,11 +83,11 @@ function wfSpecialStatistics( $par = '' ) { } } - $footer = wfMsg( 'statistics-footer' ); + $footer = wfMsgNoTrans( 'statistics-footer' ); if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' ) $text .= "\n" . $footer; $wgOut->addWikiText( $text ); } -}
\ No newline at end of file +} diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 5678a81e..e6f6298c 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -57,7 +57,7 @@ class PageArchive { $title = Title::newFromText( $prefix ); if( $title ) { $ns = $title->getNamespace(); - $encPrefix = $dbr->escapeLike( $title->getDbKey() ); + $encPrefix = $dbr->escapeLike( $title->getDBkey() ); } else { // Prolly won't work too good // @todo handle bare namespace names cleanly? @@ -132,7 +132,7 @@ class PageArchive { 'fa_user', 'fa_user_text', 'fa_timestamp' ), - array( 'fa_name' => $this->title->getDbKey() ), + array( 'fa_name' => $this->title->getDBkey() ), __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); $ret = $dbr->resultObject( $res ); @@ -174,7 +174,7 @@ class PageArchive { 'ar_text_id', 'ar_len' ), array( 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDbkey(), + 'ar_title' => $this->title->getDBkey(), 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), __METHOD__ ); if( $row ) { @@ -194,6 +194,59 @@ class PageArchive { 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 @@ -259,7 +312,8 @@ class PageArchive { * @param string $comment * @param array $fileVersions * - * @return true on success. + * @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() ) { // If both the set of text revisions and file revisions are empty, @@ -279,6 +333,8 @@ class PageArchive { if( $restoreText ) { $textRestored = $this->undeleteRevisions( $timestamps ); + if($textRestored === false) // It must be one of UNDELETE_* + return false; } else { $textRestored = 0; } @@ -288,14 +344,14 @@ class PageArchive { $log = new LogPage( 'delete' ); if( $textRestored && $filesRestored ) { - $reason = wfMsgForContent( 'undeletedrevisions-files', + $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ), $wgContLang->formatNum( $textRestored ), $wgContLang->formatNum( $filesRestored ) ); } elseif( $textRestored ) { - $reason = wfMsgForContent( 'undeletedrevisions', + $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ), $wgContLang->formatNum( $textRestored ) ); } elseif( $filesRestored ) { - $reason = wfMsgForContent( 'undeletedfiles', + $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ), $wgContLang->formatNum( $filesRestored ) ); } else { wfDebug( "Undelete: nothing undeleted...\n" ); @@ -306,11 +362,7 @@ class PageArchive { $reason .= ": {$comment}"; $log->addEntry( 'restore', $this->title, $reason ); - if ( $this->fileStatus && !$this->fileStatus->ok ) { - return false; - } else { - return true; - } + return array($textRestored, $filesRestored, $reason); } /** @@ -322,9 +374,12 @@ class PageArchive { * @param string $comment * @param array $fileVersions * - * @return int number of revisions restored + * @return mixed number of revisions restored or false on failure */ private function undeleteRevisions( $timestamps ) { + if ( wfReadOnly() ) + return false; + $restoreAll = empty( $timestamps ); $dbw = wfGetDB( DB_MASTER ); @@ -376,6 +431,7 @@ class PageArchive { 'ar_minor_edit', 'ar_flags', 'ar_text_id', + 'ar_page_id', 'ar_len' ), /* WHERE */ array( 'ar_namespace' => $this->title->getNamespace(), @@ -420,7 +476,12 @@ class PageArchive { ) ); $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... @@ -438,8 +499,14 @@ class PageArchive { 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 { - # Something went terribly wrong! + // 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 @@ -478,12 +545,13 @@ class UndeleteForm { $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' ); if( $par != "" ) { $this->mTarget = $par; } - if ( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) { + if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) { $this->mAllowed = true; } else { $this->mAllowed = false; @@ -546,7 +614,7 @@ class UndeleteForm { function showSearchForm() { global $wgOut, $wgScript; - $wgOut->addWikiText( wfMsg( 'undelete-header' ) ); + $wgOut->addWikiMsg( 'undelete-header' ); $wgOut->addHtml( Xml::openElement( 'form', array( @@ -569,11 +637,11 @@ class UndeleteForm { global $wgLang, $wgContLang, $wgUser, $wgOut; if( $result->numRows() == 0 ) { - $wgOut->addWikiText( wfMsg( 'undelete-no-results' ) ); + $wgOut->addWikiMsg( 'undelete-no-results' ); return; } - $wgOut->addWikiText( wfMsg( "undeletepagetext" ) ); + $wgOut->addWikiMsg( "undeletepagetext" ); $sk = $wgUser->getSkin(); $undelete = SpecialPage::getTitleFor( 'Undelete' ); @@ -604,21 +672,34 @@ class UndeleteForm { $rev = $archive->getRevision( $timestamp ); if( !$rev ) { - $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) ); + $wgOut->addWikiMsg( 'undeleterevision-missing' ); return; } $wgOut->setPageTitle( wfMsg( 'undeletepage' ) ); $link = $skin->makeKnownLinkObj( - $self, - htmlspecialchars( $this->mTargetObj->getPrefixedText() ), - 'target=' . $this->mTargetObj->getPrefixedUrl() + SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ), + htmlspecialchars( $this->mTargetObj->getPrefixedText() ) ); - $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp ) ); + $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) ); $user = $skin->userLink( $rev->getUser(), $rev->getUserText() ) . $skin->userToolLinks( $rev->getUser(), $rev->getUserText() ); - + + 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 ) ); @@ -651,17 +732,84 @@ class UndeleteForm { 'name' => 'wpEditToken', 'value' => $wgUser->editToken() ) ) . wfElement( 'input', array( - 'type' => 'hidden', + 'type' => 'submit', 'name' => 'preview', - 'value' => '1' ) ) . + 'value' => wfMsg( 'showpreview' ) ) ) . wfElement( 'input', array( + 'name' => 'diff', 'type' => 'submit', - 'value' => wfMsg( 'showpreview' ) ) ) . + '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. */ function showFile( $key ) { @@ -694,14 +842,14 @@ class UndeleteForm { /* $text = $archive->getLastRevisionText(); if( is_null( $text ) ) { - $wgOut->addWikiText( wfMsg( "nohistory" ) ); + $wgOut->addWikiMsg( "nohistory" ); return; } */ if ( $this->mAllowed ) { - $wgOut->addWikiText( wfMsg( "undeletehistory" ) ); + $wgOut->addWikiMsg( "undeletehistory" ); } else { - $wgOut->addWikiText( wfMsg( "undeletehistorynoadmin" ) ); + $wgOut->addWikiMsg( "undeletehistorynoadmin" ); } # List all stored revisions @@ -792,16 +940,32 @@ class UndeleteForm { # 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--; $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); if ( $this->mAllowed ) { $checkBox = Xml::check( "ts$ts" ); $pageLink = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target×tamp=$ts" ); + if( ($remaining > 0) || + ($earliestLiveTime && $ts > $earliestLiveTime ) ) { + $diffLink = '(' . + $sk->makeKnownLinkObj( $titleObj, + wfMsgHtml( 'diff' ), + "target=$target×tamp=$ts&diff=prev" ) . + ')'; + } else { + // No older revision to diff against + $diffLink = ''; + } } else { $checkBox = ''; $pageLink = $wgLang->timeanddate( $ts, true ); + $diffLink = ''; } $userLink = $sk->userLink( $row->ar_user, $row->ar_user_text ) . $sk->userToolLinks( $row->ar_user, $row->ar_user_text ); $stxt = ''; @@ -813,13 +977,13 @@ class UndeleteForm { } } $comment = $sk->commentBlock( $row->ar_comment ); - $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $stxt $comment</li>\n" ); + $wgOut->addHTML( "<li>$checkBox $pageLink $diffLink . . $userLink $stxt $comment</li>\n" ); } $revisions->free(); $wgOut->addHTML("</ul>"); } else { - $wgOut->addWikiText( wfMsg( "nohistory" ) ); + $wgOut->addWikiMsg( "nohistory" ); } if( $haveFiles ) { @@ -863,9 +1027,25 @@ class UndeleteForm { return true; } + + 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; + } function undelete() { global $wgOut, $wgUser; + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } if( !is_null( $this->mTargetObj ) ) { $archive = new PageArchive( $this->mTargetObj ); @@ -874,7 +1054,7 @@ class UndeleteForm { $this->mComment, $this->mFileVersions ); - if( $ok ) { + if( is_array($ok) ) { $skin = $wgUser->getSkin(); $link = $skin->makeKnownLinkObj( $this->mTargetObj ); $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); @@ -893,5 +1073,3 @@ class UndeleteForm { return false; } } - - diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php index 52025e53..74b794dd 100644 --- a/includes/SpecialUnlockdb.php +++ b/includes/SpecialUnlockdb.php @@ -39,12 +39,12 @@ class DBUnlockForm { global $wgReadOnlyFile; if( !file_exists( $wgReadOnlyFile ) ) { - $wgOut->addWikiText( wfMsg( 'databasenotlocked' ) ); + $wgOut->addWikiMsg( 'databasenotlocked' ); return; } $wgOut->setPagetitle( wfMsg( "unlockdb" ) ); - $wgOut->addWikiText( wfMsg( "unlockdbtext" ) ); + $wgOut->addWikiMsg( "unlockdbtext" ); if ( "" != $err ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); @@ -103,7 +103,7 @@ END $wgOut->setPagetitle( wfMsg( "unlockdb" ) ); $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) ); - $wgOut->addWikiText( wfMsg( "unlockdbsuccesstext", $ip ) ); + $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip ); } } diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php index 52aa19d2..623137c0 100644 --- a/includes/SpecialUnusedimages.php +++ b/includes/SpecialUnusedimages.php @@ -9,7 +9,9 @@ * @addtogroup SpecialPage */ class UnusedimagesPage extends ImageQueryPage { - + + function isExpensive() { return true; } + function getName() { return 'Unusedimages'; } @@ -26,21 +28,23 @@ class UnusedimagesPage extends ImageQueryPage { if ( $wgCountCategorizedImagesAsUsed ) { list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' ); - return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, 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'; + 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 img_name as title, img_user, img_user_text, img_timestamp as value, img_description' . - ' FROM '.$image.' LEFT JOIN '.$imagelinks.' ON img_name=il_to WHERE il_to IS NULL '; + 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 wfMsg( "unusedimagestext" ); + return wfMsgExt( 'unusedimagestext', array( 'parse') ); } } diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index 18c6dd9e..36bae4f7 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -19,6 +19,21 @@ function wfSpecialUpload() { * @addtogroup 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 */ @@ -126,7 +141,8 @@ class UploadForm { $this->mTempPath = $local_file; $this->mFileSize = 0; # Will be set by curlCopy $this->mCurlError = $this->curlCopy( $url, $local_file ); - $this->mSrcName = array_pop( explode( '/', $url ) ); + $pathParts = explode( '/', $url ); + $this->mSrcName = array_pop( $pathParts ); $this->mSessionKey = false; $this->mStashed = false; @@ -253,34 +269,123 @@ class UploadForm { $this->cleanupTempFile(); } - /* -------------------------------------------------------------- */ + /** + * Do the upload + * Checks are made in SpecialUpload::execute() + * + * @access private + */ + function processUpload(){ + global $wgUser, $wgOut, $wgFileExtensions; + $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: + $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) ); + break; + + case self::OVERWRITE_EXISTING_FILE: + $errorText = $details['overwrite']; + $overwrite = new WikiError( $wgOut->parse( $errorText ) ); + $this->uploadError( $overwrite->toString() ); + 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 + ) + ) + ); + 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 processUpload() { - global $wgUser, $wgOut; + function internalProcessUpload( &$resultDetails ) { + global $wgUser; if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) { wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." ); - return false; + return self::BEFORE_PROCESSING; } /* Check for PHP error if any, requires php 4.2 or newer */ if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { - $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); - return; + return self::LARGE_FILE_SERVER; } /** * If there was no filename or a zero size given, give up quick. */ if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { - $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); - return; + return self::EMPTY_FILE; } # Chop off any directories in the given filename @@ -311,8 +416,7 @@ class UploadForm { } if( strlen( $partname ) < 1 ) { - $this->mainUploadForm( wfMsgHtml( 'minlength1' ) ); - return; + return self::MIN_LENGHT_PARTNAME; } /** @@ -322,8 +426,8 @@ class UploadForm { $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered ); $nt = Title::makeTitleSafe( NS_IMAGE, $filtered ); if( is_null( $nt ) ) { - $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) ); - return; + $resultDetails = array( 'filtered' => $filtered ); + return self::ILLEGAL_FILENAME; } $this->mLocalFile = wfLocalFile( $nt ); $this->mDestName = $this->mLocalFile->getName(); @@ -332,27 +436,28 @@ class UploadForm { * If the image is protected, non-sysop users won't be able * to modify it by uploading a new revision. */ - if( !$nt->userCan( 'edit' ) ) { - return $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) ); + if( !$nt->userCan( 'edit' ) || !$nt->userCan( 'create' ) ) { + return self::PROTECTED_PAGE; } /** * In some cases we may forbid overwriting of existing files. */ $overwrite = $this->checkOverwrite( $this->mDestName ); - if( WikiError::isError( $overwrite ) ) { - return $this->uploadError( $overwrite->toString() ); + if( $overwrite !== true ) { + $resultDetails = array( 'overwrite' => $overwrite ); + return self::OVERWRITE_EXISTING_FILE; } /* Don't allow users to override the blacklist (check file extension) */ global $wgStrictFileExtensions; global $wgFileExtensions, $wgFileBlacklist; if ($finalExt == '') { - return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) ); + return self::FILETYPE_MISSING; } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), - htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) ); + $resultDetails = array( 'finalExt' => $finalExt ); + return self::FILETYPE_BADTYPE; } /** @@ -366,7 +471,8 @@ class UploadForm { $veri = $this->verify( $this->mTempPath, $finalExt ); if( $veri !== true ) { //it's a wiki error... - return $this->uploadError( $veri->toString() ); + $resultDetails = array( 'veri' => $veri ); + return self::VERIFICATION_ERROR; } /** @@ -375,7 +481,8 @@ class UploadForm { $error = ''; if( !wfRunHooks( 'UploadVerification', array( $this->mDestName, $this->mTempPath, &$error ) ) ) { - return $this->uploadError( $error ); + $resultDetails = array( 'error' => $error ); + return self::UPLOAD_VERIFICATION_ERROR; } } @@ -396,9 +503,16 @@ class UploadForm { global $wgCheckFileExtensions; if ( $wgCheckFileExtensions ) { - if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - $warning .= '<li>'.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), - htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'</li>'; + if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { + $warning .= '<li>' . + wfMsgExt( 'filetype-unwanted-type', + array( 'parseinline' ), + htmlspecialchars( $finalExt ), + implode( + wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ), + $wgFileExtensions + ) + ) . '</li>'; } } @@ -421,7 +535,8 @@ class UploadForm { * Stash the file in a temporary location; the user can choose * to let it through and we'll complete the upload then. */ - return $this->uploadWarning( $warning ); + $resultDetails = array( 'warning' => $warning ); + return self::UPLOAD_WARNING; } } @@ -432,19 +547,20 @@ class UploadForm { $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, $this->mCopyrightStatus, $this->mCopyrightSource ); - $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, + $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, File::DELETE_SOURCE, $this->mFileProps ); if ( !$status->isGood() ) { - $this->showError( $status->getWikiText() ); + $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 - $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere? - wfRunHooks( 'UploadComplete', array( &$img ) ); + wfRunHooks( 'UploadComplete', array( &$this ) ); + return self::SUCCESS; } } @@ -455,11 +571,12 @@ class UploadForm { * Returns an empty string if there is no warning */ static function getExistsWarning( $file ) { - global $wgUser; + 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 = ''; @@ -483,28 +600,31 @@ class UploadForm { if( $file->exists() ) { $dlink = $sk->makeKnownLinkObj( $file->getTitle() ); if ( $file->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), - $file->getName(), 'right', array(), false, true ); + $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:right" id="mw-media-icon">' . + $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>'; } else { $dlink2 = ''; } - $warning .= '<li>' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '</li>' . $dlink2; + $warning .= '<li>' . wfMsgExt( 'fileexists', array(), $dlink ) . '</li>' . $dlink2; + } elseif( $file->getTitle()->getArticleID() ) { + $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' ); + $warning .= '<li>' . wfMsgExt( 'filepageexists', array(), $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', $dlink ), - $nt_lc->getText(), 'right', array(), false, true ); + $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:right" id="mw-media-icon">' . + $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>'; } else { $dlink2 = ''; @@ -523,11 +643,11 @@ class UploadForm { $dlink = $sk->makeKnownLinkObj( $nt_thb); if ( $file_thb->allowInlineDisplay() ) { $dlink2 = $sk->makeImageLinkObj( $nt_thb, - wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), - $nt_thb->getText(), 'right', array(), false, true ); + 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:right" id="mw-media-icon">' . + $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>'; } else { @@ -542,9 +662,19 @@ class UploadForm { substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>'; } } - if ( $file->wasDeleted() ) { + + $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 image exists now, however + # 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() ); @@ -553,6 +683,12 @@ class UploadForm { 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 ) { @@ -590,6 +726,33 @@ class UploadForm { } /** + * 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. * @@ -752,10 +915,10 @@ class UploadForm { $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview; - + $adc = wfBoolToStr( $useAjaxDestCheck ); $alp = wfBoolToStr( $useAjaxLicensePreview ); - + $wgOut->addScript( "<script type=\"text/javascript\"> wgAjaxUploadDestCheck = {$adc}; wgAjaxLicensePreview = {$alp}; @@ -768,26 +931,35 @@ wgAjaxLicensePreview = {$alp}; wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); return false; } - - if( $this->mDesiredDestName && $wgUser->isAllowed( 'deletedhistory' ) ) { + + if( $this->mDesiredDestName ) { $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName ); - if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 ) { + // 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() ), - wfMsgHtml( 'restorelink', $count ) + 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' )); - $ew = $wgUser->getOption( 'editwidth' ); - if ( $ew ) $ew = " style=\"width:100%\""; - else $ew = ''; + + if( $wgUser->getOption( 'editwidth' ) ) { + $width = " style=\"width:100%\""; + } else { + $width = ''; + } if ( '' != $msg ) { $sub = wfMsgHtml( 'uploaderror' ); @@ -795,8 +967,34 @@ wgAjaxLicensePreview = {$alp}; "<span class='error'>{$msg}</span>\n" ); } $wgOut->addHTML( '<div id="uploadtext">' ); - $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) ); - $wgOut->addHTML( '</div>' ); + $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; + if( $wgCheckFileExtensions ) { + $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ); + if( $wgStrictFileExtensions ) { + # Everything not permitted is banned + $wgOut->addHTML( + '<div id="mw-upload-permitted">' . + wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) . + "</div>\n" + ); + } else { + # We have to list both preferred and prohibited + $wgOut->addHTML( + '<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" + ); + } + } $sourcefilename = wfMsgHtml( 'sourcefilename' ); $destfilename = wfMsgHtml( 'destfilename' ); @@ -881,7 +1079,7 @@ wgAjaxLicensePreview = {$alp}; <td align='$align1'><label for='wpUploadDescription'>{$summary}</label></td> <td align='$align2'> <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' - cols='{$cols}'{$ew}>$encComment</textarea> + cols='{$cols}'{$width}>$encComment</textarea> {$this->uploadFormTextAfterSummary} </td> </tr> @@ -1353,7 +1551,7 @@ EOT if( $error ) { $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) ); - return new WikiError( $wgOut->parse( $errorText ) ); + return $errorText; } // Rockin', go ahead and upload @@ -1420,4 +1618,29 @@ EOT } 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 ) { + $reader = new LogReader( + new FauxRequest( + array( + 'page' => $filename, + 'type' => 'delete', + ) + ) + ); + if( $reader->hasRows() ) { + $out->addHtml( '<div id="mw-upload-deleted-warn">' ); + $out->addWikiMsg( 'upload-wasdeleted' ); + $viewer = new LogViewer( $reader ); + $viewer->showList( $out ); + $out->addHtml( '</div>' ); + } + } } diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index f358c1fd..3651fdc8 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -7,13 +7,13 @@ /** * constructor */ -function wfSpecialUserlogin() { +function wfSpecialUserlogin( $par = '' ) { global $wgRequest; if( session_id() == '' ) { wfSetupSession(); } - $form = new LoginForm( $wgRequest ); + $form = new LoginForm( $wgRequest, $par ); $form->execute(); } @@ -41,11 +41,11 @@ class LoginForm { * Constructor * @param WebRequest $request A WebRequest object passed by reference */ - function LoginForm( &$request ) { + function LoginForm( &$request, $par = '' ) { global $wgLang, $wgAllowRealName, $wgEnableEmail; global $wgAuth; - $this->mType = $request->getText( 'type' ); + $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' ); @@ -123,9 +123,9 @@ class LoginForm { // Wipe the initial password and mail a temporary one $u->setPassword( null ); $u->saveSettings(); - $result = $this->mailPasswordInternal( $u, false ); + $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' ); - wfRunHooks( 'AddNewAccount', array( $u ) ); + wfRunHooks( 'AddNewAccount', array( $u, true ) ); $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -134,7 +134,7 @@ class LoginForm { if( WikiError::isError( $result ) ) { $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { - $wgOut->addWikiText( wfMsg( 'accmailtext', $u->getName(), $u->getEmail() ) ); + $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() ); $wgOut->returnToMain( false ); } $u = 0; @@ -164,9 +164,9 @@ class LoginForm { global $wgOut; $error = $u->sendConfirmationMail(); if( WikiError::isError( $error ) ) { - $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $error->getMessage() ) ); + $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() ); } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_oncreate' ) ); + $wgOut->addWikiMsg( 'confirmemail_oncreate' ); } } @@ -189,7 +189,7 @@ class LoginForm { $wgOut->setArticleRelated( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) ); - $wgOut->returnToMain( $self->getPrefixedText() ); + $wgOut->returnToMain( false, $self ); wfRunHooks( 'AddNewAccount', array( $u ) ); return true; } @@ -203,6 +203,7 @@ class LoginForm { 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 ) ) { @@ -228,10 +229,13 @@ class LoginForm { return false; } - # Check anonymous user ($wgUser) limitations : - if (!$wgUser->isAllowedToCreateAccount()) { + # Check permissions + if ( !$wgUser->isAllowed( 'createaccount' ) ) { $this->userNotPrivilegedMessage(); return false; + } elseif ( $wgUser->isBlockedFromCreateAccount() ) { + $this->userBlockedMessage(); + return false; } $ip = wfGetIP(); @@ -260,11 +264,29 @@ class LoginForm { return false; } + # check for minimal password length if ( !$u->isValidPassword( $this->mPassword ) ) { - $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) ); + if ( !$this->mCreateaccountMail ) { + $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) ); + return false; + } else { + # do not force a password for account creation by email + # set pseudo password, it will be replaced later by a random generated password + $this->mPassword = '-'; + } + } + + # 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 ); @@ -449,7 +471,11 @@ class LoginForm { $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); break; case self::NOT_EXISTS: - $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) ); + 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' ) ); @@ -469,7 +495,7 @@ class LoginForm { global $wgOut; $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" ); $reset = new PasswordResetForm( $this->mName, $this->mPassword ); - $reset->execute(); + $reset->execute( null ); } /** @@ -519,7 +545,7 @@ class LoginForm { return; } - $result = $this->mailPasswordInternal( $u, true ); + $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' ); if( WikiError::isError( $result ) ) { $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { @@ -529,10 +555,14 @@ class LoginForm { /** + * @param object user + * @param bool throttle + * @param string message name of email title + * @param string message name of email text * @return mixed true on success, WikiError on failure * @private */ - function mailPasswordInternal( $u, $throttle = true ) { + function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) { global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure; global $wgServer, $wgScript; @@ -550,9 +580,9 @@ class LoginForm { $ip = wfGetIP(); if ( '' == $ip ) { $ip = '(Unknown)'; } - $m = wfMsg( 'passwordremindertext', $ip, $u->getName(), $np, $wgServer . $wgScript ); + $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript ); + $result = $u->sendMail( wfMsg( $emailTitle ), $m ); - $result = $u->sendMail( wfMsg( 'passwordremindertitle' ), $m ); return $result; } @@ -589,14 +619,14 @@ class LoginForm { $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiText( wfMsg( 'whitelistacctext' ) ); + $wgOut->addWikiMsg( 'whitelistacctext' ); $wgOut->returnToMain( false ); } /** */ function userBlockedMessage() { - global $wgOut; + 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 @@ -611,7 +641,10 @@ class LoginForm { $wgOut->setArticleRelated( false ); $ip = wfGetIP(); - $wgOut->addWikiText( wfMsg( 'cantcreateaccounttext', $ip ) ); + $blocker = User::whoIs( $wgUser->mBlock->mBy ); + $block_reason = $wgUser->mBlock->mReason; + + $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker ); $wgOut->returnToMain( false ); } @@ -621,7 +654,7 @@ class LoginForm { function mainLoginForm( $msg, $msgtype = 'error' ) { global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector; - global $wgAuth; + global $wgAuth, $wgEmailConfirmToEdit; if ( $this->mType == 'signup' ) { if ( !$wgUser->isAllowed( 'createaccount' ) ) { @@ -666,7 +699,7 @@ class LoginForm { $linkq .= '&uselang=' . $this->mLanguage; $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">'; - $link .= wfMsgHtml( $linkmsg . 'link' ); + $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink' $link .= '</a>'; # Don't show a "create account" link if the user can't @@ -689,6 +722,7 @@ class LoginForm { $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 ); @@ -779,7 +813,7 @@ class LoginForm { function throttleHit( $limit ) { global $wgOut; - $wgOut->addWikiText( wfMsg( 'acct_creation_throttle_hit', $limit ) ); + $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit ); } /** @@ -796,7 +830,9 @@ class LoginForm { foreach( $langs as $lang ) { $lang = trim( $lang, '* ' ); $parts = explode( '|', $lang ); - $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] ); + if (count($parts) >= 2) { + $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] ); + } } return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : ''; } else { @@ -824,3 +860,4 @@ class LoginForm { } } + diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php index 6e464ced..d9952ea5 100644 --- a/includes/SpecialUserlogout.php +++ b/includes/SpecialUserlogout.php @@ -10,17 +10,10 @@ function wfSpecialUserlogout() { global $wgUser, $wgOut; - if (wfRunHooks('UserLogout', array(&$wgUser))) { - - $wgUser->logout(); - - wfRunHooks('UserLogoutComplete', array(&$wgUser)); - - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) ); - $wgOut->returnToMain(); - - } + $wgUser->logout(); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) ); + $wgOut->returnToMain(); } diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php index b97e5168..48fb3628 100644 --- a/includes/SpecialUserrights.php +++ b/includes/SpecialUserrights.php @@ -4,64 +4,111 @@ * Special page to allow managing user group membership * * @addtogroup SpecialPage - * @todo This code is disgusting and needs a total rewrite + * @todo Use checkboxes or something, this list thing is incomprehensible to + * normal human beings. */ -/** */ -require_once( dirname(__FILE__) . '/HTMLForm.php'); - -/** Entry point */ -function wfSpecialUserrights() { - global $wgRequest; - $form = new UserrightsForm($wgRequest); - $form->execute(); -} - /** * A class to manage user levels rights. * @addtogroup SpecialPage */ -class UserrightsForm extends HTMLForm { - var $mPosted, $mRequest, $mSaveprefs; - /** Escaped local url name*/ - var $action; - - /** Constructor*/ - public function __construct( &$request ) { - $this->mPosted = $request->wasPosted(); - $this->mRequest =& $request; - $this->mName = 'userrights'; - - $titleObj = SpecialPage::getTitleFor( 'Userrights' ); - $this->action = $titleObj->escapeLocalURL(); +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 mixed $par String if any subpage provided, else null */ - function execute() { + 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( $this->mPosted ) { - // show some more forms - if( $this->mRequest->getCheck( 'ssearchuser' ) ) { - $this->editUserGroupsForm( $this->mRequest->getVal( 'user-editname' ) ); - } + if( $wgRequest->wasPosted() ) { // save settings - if( $this->mRequest->getCheck( 'saveusergroups' ) ) { - global $wgUser; - $username = $this->mRequest->getVal( 'user-editname' ); - $reason = $this->mRequest->getVal( 'user-reason' ); - if( $wgUser->matchEditToken( $this->mRequest->getVal( 'wpEditToken' ), $username ) ) { - $this->saveUserGroups( $username, - $this->mRequest->getArray( 'member' ), - $this->mRequest->getArray( 'available' ), - $reason ); + if( $wgRequest->getCheck( 'saveusergroups' ) ) { + $reason = $wgRequest->getVal( 'user-reason' ); + if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) { + $this->saveUserGroups( + $this->mTarget, + $wgRequest->getArray( 'removable' ), + $wgRequest->getArray( 'available' ), + $reason + ); } } } + + // show some more forms + if( $this->mTarget ) { + $this->editUserGroupsForm( $this->mTarget ); + } } /** @@ -72,50 +119,155 @@ class UserrightsForm extends HTMLForm { * @param array $removegroup id of groups to be removed. * @param array $addgroup id of groups to be added. * @param string $reason Reason for group change - * + * @return null */ - function saveUserGroups( $username, $removegroup, $addgroup, $reason = '' ) { - global $wgOut; - $u = User::newFromName($username); + function saveUserGroups( $username, $removegroup, $addgroup, $reason = '') { + global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - if(is_null($u)) { - $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) ); + $user = $this->fetchUser( $username ); + if( !$user ) { return; } - - if($u->getID() == 0) { - $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) ); - return; + + // 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']; } - $oldGroups = $u->getGroups(); + $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(isset($removegroup)) { + if( $removegroup ) { $newGroups = array_diff($newGroups, $removegroup); foreach( $removegroup as $group ) { - if ( $this->canRemove( $group ) ) { - $u->removeGroup( $group ); - } + $user->removeGroup( $group ); } } - if(isset($addgroup)) { + if( $addgroup ) { $newGroups = array_merge($newGroups, $addgroup); foreach( $addgroup as $group ) { - if ( $this->canAdd( $group ) ) { - $u->addGroup( $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 ) { + $log = new LogPage( 'rights' ); + + global $wgRequest; + $log->addEntry( 'rights', + $user->getUserPage(), + $wgRequest->getText( 'user-reason' ), + array( + $this->makeGroupNameList( $oldGroups ), + $this->makeGroupNameList( $newGroups ) + ) + ); + } + } + + /** + * Edit user groups membership + * @param string $username Name of the user. + */ + function editUserGroupsForm( $username ) { + global $wgOut; - wfRunHooks( 'UserRights', array( &$u, $addgroup, $removegroup ) ); - $log = new LogPage( 'rights' ); - $log->addEntry( 'rights', Title::makeTitle( NS_USER, $u->getName() ), $reason, array( $this->makeGroupNameList( $oldGroups ), - $this->makeGroupNameList( $newGroups ) ) ); + $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 ) { @@ -126,36 +278,16 @@ class UserrightsForm extends HTMLForm { * Output a form to allow searching for a user */ function switchForm() { - global $wgOut, $wgRequest; - $username = $wgRequest->getText( 'user-editname' ); - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) ); + global $wgOut, $wgScript; + $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser' ) ); + $form .= Xml::hidden( 'title', 'Special:Userrights' ); $form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>'; - $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>'; - $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>'; + $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . '</p>'; + $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ) ) . '</p>'; $form .= '</fieldset>'; $form .= '</form>'; $wgOut->addHTML( $form ); } - - /** - * Edit user groups membership - * @param string $username Name of the user. - */ - function editUserGroupsForm($username) { - global $wgOut; - - $user = User::newFromName($username); - if( is_null( $user ) ) { - $wgOut->addWikiText( wfMsg( 'nouserspecified' ) ); - return; - } elseif( $user->getID() == 0 ) { - $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) ); - return; - } - - $this->showEditUserGroupsForm( $username, $user->getGroups() ); - $this->showLogFragment( $user, $wgOut ); - } /** * Go through used and available groups and return the ones that this @@ -166,9 +298,15 @@ class UserrightsForm extends HTMLForm { * @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($removable, $groups ); // Can't remove groups the user doesn't have - $addable = array_diff( $addable, $groups ); // Can't add groups the user does have + + $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 ); } @@ -177,21 +315,31 @@ class UserrightsForm extends HTMLForm { * Show the form to edit group memberships. * * @todo make all CSS-y and semantic - * @param $username String: Name of user you're editing + * @param $user User or UserRightsProxy you're editing * @param $groups Array: Array of groups the user is in */ - protected function showEditUserGroupsForm( $username, $groups ) { + protected function showEditUserGroupsForm( $user, $groups ) { global $wgOut, $wgUser; - + list( $addable, $removable ) = $this->splitGroups( $groups ); + $list = array(); + foreach( $user->getGroups() as $group ) + $list[] = self::buildGroupLink( $group ); + + $grouplist = ''; + if( count( $list ) > 0 ) { + $grouplist = '<p>' . wfMsgHtml( 'userrights-groupsmember' ) . ' ' . implode( ', ', $list ) . '</p>'; + } $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) . - Xml::hidden( 'user-editname', $username ) . - Xml::hidden( 'wpEditToken', $wgUser->editToken( $username ) ) . + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->escapeLocalURL(), 'name' => 'editGroup' ) ) . + Xml::hidden( 'user', $user->getName() ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken( $user->getName() ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) . - $wgOut->parse( wfMsg( 'editinguser', $username ) ) . + wfMsgExt( 'editinguser', array( 'parse' ), + wfEscapeWikiText( $user->getName() ) ) . + $grouplist . $this->explainRights() . "<table border='0'> <tr> @@ -214,7 +362,7 @@ class UserrightsForm extends HTMLForm { Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) . "</td> <td>" . - Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason' ) ) . + Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) . "</td> </tr> <tr> @@ -228,6 +376,19 @@ class UserrightsForm extends HTMLForm { Xml::closeElement( 'form' ) . "\n" ); } + + /** + * Format a link to a group description page + * + * @param string $group + * @return string + */ + private static function buildGroupLink( $group ) { + static $cache = array(); + if( !isset( $cache[$group] ) ) + $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) ); + return $cache[$group]; + } /** * Prepare a list of groups the user is able to add and remove @@ -236,17 +397,25 @@ class UserrightsForm extends HTMLForm { */ private function explainRights() { global $wgUser, $wgLang; - + $out = array(); - list( $add, $remove ) = array_values( $this->changeableGroups() ); - + list( $add, $remove, $addself, $rmself ) = array_values( $this->changeableGroups() ); + if( count( $add ) > 0 ) - $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) ); + $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', + $wgLang->listToText( $add ), count( $add ) ); if( count( $remove ) > 0 ) - $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) ); - + $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', + $wgLang->listToText( $remove ), count( $add ) ); + if( count( $addself ) > 0 ) + $out[] = wfMsgExt( 'userrights-available-add-self', 'parseinline', + $wgLang->listToText( $addself ), count( $addself ) ); + if( count( $rmself ) > 0 ) + $out[] = wfMsgExt( 'userrights-available-remove-self', 'parseinline', + $wgLang->listToText( $rmself ), count( $rmself ) ); + return count( $out ) > 0 - ? implode( ' ', $out ) + ? implode( '<br />', $out ) : wfMsgExt( 'userrights-available-none', 'parseinline' ); } @@ -257,7 +426,7 @@ class UserrightsForm extends HTMLForm { * @return string XHTML <select> element */ private function removeSelect( $groups ) { - return $this->doSelect( $groups, 'member' ); + return $this->doSelect( $groups, 'removable' ); } /** @@ -274,7 +443,7 @@ class UserrightsForm extends HTMLForm { * Adds the <select> thingie where you can select what groups to add/remove * * @param array $groups The groups that can be added/removed - * @param string $name 'member' or 'available' + * @param string $name 'removable' or 'available' * @return string XHTML <select> element */ private function doSelect( $groups, $name ) { @@ -318,10 +487,29 @@ class UserrightsForm extends HTMLForm { * * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) */ - private function changeableGroups() { - global $wgUser; + function changeableGroups() { + global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - $groups = array( 'add' => array(), 'remove' => array() ); + 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) { @@ -339,25 +527,10 @@ class UserrightsForm extends HTMLForm { * * @param String $group The group to check for whether it can add/remove * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) - */ + */ private function changeableByGroup( $group ) { - global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; - - if( empty($wgGroupPermissions[$group]['userrights']) ) { - // This group doesn't give the right to modify anything - return array( 'add' => array(), 'remove' => array() ); - } - if( empty($wgAddGroups[$group]) and empty($wgRemoveGroups[$group]) ) { - // This group gives the right to modify everything (reverse- - // compatibility with old "userrights lets you change - // everything") - return array( - 'add' => User::getAllGroups(), - 'remove' => User::getAllGroups() - ); - } - - // Okay, it's not so simple, we have to go through the arrays + global $wgAddGroups, $wgRemoveGroups; + $groups = array( 'add' => array(), 'remove' => array() ); if( empty($wgAddGroups[$group]) ) { // Don't add anything to $groups @@ -390,7 +563,7 @@ class UserrightsForm extends HTMLForm { new FauxRequest( array( 'type' => 'rights', - 'page' => $user->getUserPage()->getPrefixedUrl(), + 'page' => $user->getUserPage()->getPrefixedText(), ) ) ) @@ -399,4 +572,4 @@ class UserrightsForm extends HTMLForm { $viewer->showList( $output ); } -}
\ No newline at end of file +} diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php index a6a132e0..70203832 100644 --- a/includes/SpecialVersion.php +++ b/includes/SpecialVersion.php @@ -24,11 +24,13 @@ class SpecialVersion { * main() */ function execute() { - global $wgOut; + global $wgOut, $wgMessageCache; + $wgMessageCache->loadAllMessages(); $wgOut->addHTML( '<div dir="ltr">' ); $wgOut->addWikiText( $this->MediaWikiCredits() . + $this->softwareInformation() . $this->extensionCredits() . $this->wgHooks() ); @@ -41,18 +43,13 @@ class SpecialVersion { */ /** - * Return wiki text showing the licence information and third party - * software versions (apache, php, mysql). - * @static + * @return wiki text showing the license information */ - function MediaWikiCredits() { - $version = self::getVersion(); - $dbr = wfGetDB( DB_SLAVE ); - - $ret = + 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-2007 Magnus Manske, Brion Vibber, Lee Daniel Crocker, + 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 and others. @@ -68,40 +65,68 @@ class SpecialVersion { 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/copyleft/gpl.html read it online] - - * [http://www.mediawiki.org/ MediaWiki]: $version - * [http://www.php.net/ PHP]: " . phpversion() . " (" . php_sapi_name() . ") - * " . $dbr->getSoftwareLink() . ": " . $dbr->getServerVersion(); + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + or [http://www.gnu.org/copyleft/gpl.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::getVersion() . "</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 */ public static function getVersion() { global $wgVersion, $IP; + wfProfileIn( __METHOD__ ); $svn = self::getSvnRevision( $IP ); - return $svn ? "$wgVersion (r$svn)" : $wgVersion; + $version = $svn ? "$wgVersion (r$svn)" : $wgVersion; + wfProfileOut( __METHOD__ ); + return $version; } /** Generate wikitext showing extensions name, URL, author and description */ function extensionCredits() { - global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunction; + global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions; - if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunction ) ) + if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) ) return ''; $extensionTypes = array( - 'specialpage' => 'Special pages', - 'parserhook' => 'Parser hooks', - 'variable' => 'Variables', - 'other' => 'Other', + '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 = "<h2>Extensions</h2>\n"; - $out .= wfOpenElement('table', array('id' => 'sv-ext') ); + $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] ) ) { @@ -111,38 +136,39 @@ class SpecialVersion { foreach ( $wgExtensionCredits[$type] as $extension ) { $out .= $this->formatCredits( - isset ( $extension['name'] ) ? $extension['name'] : '', - isset ( $extension['version'] ) ? $extension['version'] : null, - isset ( $extension['author'] ) ? $extension['author'] : '', - isset ( $extension['url'] ) ? $extension['url'] : null, - isset ( $extension['description'] ) ? $extension['description'] : '' + isset ( $extension['name'] ) ? $extension['name'] : '', + isset ( $extension['version'] ) ? $extension['version'] : null, + 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('Extension functions'); + $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('Parser extension tags'); + $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('Parser function hooks'); + $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) ); $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n"; } - if ( count( $wgSkinExtensionFunction ) ) { - $out .= $this->openExtType('Skin extension functions'); - $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunction ) . "</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 .= wfCloseElement( 'table' ); + $out .= Xml::closeElement( 'table' ); return $out; } @@ -158,21 +184,23 @@ class SpecialVersion { } } - function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) { - $ret = '<tr><td>'; - if ( isset( $url ) ) - $ret .= "[$url "; - $ret .= "''$name"; - if ( isset( $version ) ) - $ret .= " (version $version)"; - $ret .= "''"; - if ( isset( $url ) ) - $ret .= ']'; - $ret .= '</td>'; - $ret .= "<td>$description</td>"; - $ret .= "<td>" . $this->listToText( (array)$author ) . "</td>"; - $ret .= '</tr>'; - return "$ret\n"; + 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"; } /** @@ -185,14 +213,20 @@ class SpecialVersion { $myWgHooks = $wgHooks; ksort( $myWgHooks ); - $ret = "<h2>Hooks</h2>\n" - . wfOpenElement('table', array('id' => 'sv-hooks') ) - . "<tr><th>Hook name</th><th>Subscribed by</th></tr>\n"; + $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"; + foreach ( $myWgHooks as $hook => $hooks ) + $ret .= "<tr> + <td>$hook</td> + <td>" . $this->listToText( $hooks ) . "</td> + </tr>\n"; - $ret .= '</table>'; + $ret .= Xml::closeElement( 'table' ); return $ret; } else return ''; @@ -204,13 +238,13 @@ class SpecialVersion { if(!$this->firstExtOpened) { // Insert a spacing line - $out .= '<tr class="sv-space">' . wfElement( 'td', $opt ) . "</tr>\n"; + $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n"; } $this->firstExtOpened = false; if($name) { $opt['id'] = "sv-$name"; } - $out .= "<tr>" . wfElement( 'th', $opt, $text) . "</tr>\n"; + $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n"; return $out; } @@ -232,18 +266,20 @@ class SpecialVersion { function listToText( $list ) { $cnt = count( $list ); - if ( $cnt == 1 ) { + if ( $cnt == 1 ) { // Enforce always returning a string return (string)$this->arrayToString( $list[0] ); - } elseif ( $cnt == 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"; - } + return implode( ', ', $one ) . " $and $two"; + } } /** @@ -316,3 +352,4 @@ class SpecialVersion { /**#@-*/ + diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php index f3e8966c..580cc6de 100644 --- a/includes/SpecialWantedcategories.php +++ b/includes/SpecialWantedcategories.php @@ -37,7 +37,7 @@ class WantedCategoriesPage extends QueryPage { /** * Fetch user page links and cache their existence */ - function preprocessResults( &$db, &$res ) { + function preprocessResults( $db, $res ) { $batch = new LinkBatch; while ( $row = $db->fetchObject( $res ) ) $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php index 5fc45a88..1fb8cdbb 100644 --- a/includes/SpecialWantedpages.php +++ b/includes/SpecialWantedpages.php @@ -51,7 +51,7 @@ class WantedPagesPage extends QueryPage { /** * Cache page existence for performance */ - function preprocessResults( &$db, &$res ) { + function preprocessResults( $db, $res ) { $batch = new LinkBatch; while ( $row = $db->fetchObject( $res ) ) $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php index e9aa7e68..2dfa8ae5 100644 --- a/includes/SpecialWatchlist.php +++ b/includes/SpecialWatchlist.php @@ -87,19 +87,11 @@ function wfSpecialWatchlist( $par ) { $dbr = wfGetDB( DB_SLAVE, 'watchlist' ); list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' ); - $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_user=$uid"; - $res = $dbr->query( $sql, $fname ); - $s = $dbr->fetchObject( $res ); - -# Patch *** A1 *** (see A2 below) -# adjust for page X, talk:page X, which are both stored separately, but treated together - $nitems = floor($s->n / 2); -# $nitems = $s->n; - - if($nitems == 0) { - $wgOut->addWikiText( wfMsg( 'nowatchlist' ) ); - return; - } + $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 */ @@ -122,6 +114,16 @@ function wfSpecialWatchlist( $par ) { 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 { @@ -180,8 +182,10 @@ function wfSpecialWatchlist( $par ) { '" /><input type="hidden" name="reset" value="all" /></form>' . "\n\n" ); } - - $sql = "SELECT * + if ( $wgShowUpdatedMarker ) { + $wltsfield=", ${watchlist}.wl_notificationtimestamp "; + } + $sql = "SELECT ${recentchanges}.* ${wltsfield} FROM $watchlist,$recentchanges,$page WHERE wl_user=$uid AND wl_namespace=rc_namespace @@ -193,6 +197,7 @@ function wfSpecialWatchlist( $par ) { $andHideBots $andHideMinor $nameSpaceClause + $hookSql ORDER BY rc_timestamp DESC $limitWatchlist"; @@ -251,7 +256,7 @@ function wfSpecialWatchlist( $par ) { # If there's nothing to show, stop here if( $numRows == 0 ) { - $wgOut->addWikiText( wfMsgNoTrans( 'watchnochange' ) ); + $wgOut->addWikiMsg( 'watchnochange' ); return; } @@ -286,10 +291,13 @@ function wfSpecialWatchlist( $par ) { } if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) { - $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" .$dbr->strencode($obj->page_title). "' AND wl_namespace='{$obj->page_namespace}'" ; - $res3 = $dbr->query( $sql3, $fname ); - $x = $dbr->fetchObject( $res3 ); - $rc->numberofWatchingusers = $x->n; + $rc->numberofWatchingusers = $dbr->selectField( 'watchlist', + 'COUNT(*)', + array( + 'wl_namespace' => $obj->rc_namespace, + 'wl_title' => $obj->rc_title, + ), + __METHOD__ ); } else { $rc->numberofWatchingusers = 0; } @@ -364,4 +372,4 @@ function wlCountItems( &$user, $talk = true ) { $count = floor( $count / 2 ); return( $count ); -}
\ No newline at end of file +} diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php index d944f6b4..16a44ee6 100644 --- a/includes/SpecialWhatlinkshere.php +++ b/includes/SpecialWhatlinkshere.php @@ -44,23 +44,23 @@ class WhatLinksHerePage { $targetString = isset($this->par) ? $this->par : $this->request->getVal( 'target' ); - if (is_null($targetString)) { - $wgOut->showErrorPage( 'notargettitle', 'notargettext' ); + if ( is_null( $targetString ) ) { + $wgOut->addHTML( $this->whatlinkshereForm() ); return; } $this->target = Title::newFromURL( $targetString ); if( !$this->target ) { - $wgOut->showErrorPage( 'notargettitle', 'notargettext' ); + $wgOut->addHTML( $this->whatlinkshereForm() ); return; } $this->selfTitle = Title::makeTitleSafe( NS_SPECIAL, 'Whatlinkshere/' . $this->target->getPrefixedDBkey() ); - + $wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) ); $wgOut->setSubtitle( wfMsg( 'linklistsub' ) ); - $wgOut->addHTML( wfMsg( 'whatlinkshere-barrow' ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); + $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); $this->showIndirectLinks( 0, $this->target, $this->limit, $this->from, $this->back ); } @@ -113,34 +113,32 @@ class WhatLinksHerePage { // 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' ); $options['ORDER BY'] = 'pl_from'; $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields, $plConds, $fname, $options ); - + $options['ORDER BY'] = 'tl_from'; $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields, $tlConds, $fname, $options ); - + if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) { - if ( 0 == $level && !isset( $this->namespace ) ) { - // really no links to here - $wgOut->addWikiText( wfMsg( 'nolinkshere', $this->target->getPrefixedText() ) ); - } elseif ( 0 == $level && isset( $this->namespace ) ) { - // no links from requested namespace to here + if ( 0 == $level ) { $options = array(); // reinitialize for a further namespace search + // really no links to here $options['namespace'] = $this->namespace; $options['target'] = $this->target->getPrefixedText(); list( $options['limit'], $options['offset']) = wfCheckLimits(); $wgOut->addHTML( $this->whatlinkshereForm( $options ) ); - $wgOut->addWikiText( wfMsg( 'nolinkshere-ns', $this->target->getPrefixedText() ) ); + $errMsg = isset( $this->namespace ) ? 'nolinkshere-ns' : 'nolinkshere'; + $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() ); } return; } @@ -157,8 +155,8 @@ class WhatLinksHerePage { $options['offset'] = $this->request->getVal( 'offset' ); /* Offset must be an integral. */ if ( !strlen( $options['offset'] ) || !preg_match( '/^[0-9]+$/', $options['offset'] ) ) - $options['offset'] = ''; - $options['target'] = $this->target->getPrefixedDBkey(); + $options['offset'] = ''; + $options['target'] = $this->target->getPrefixedText(); // Read the rows into an array and remove duplicates // templatelinks comes second so that the templatelinks row overwrites the @@ -195,12 +193,8 @@ class WhatLinksHerePage { if ( $level == 0 ) { $wgOut->addHTML( $this->whatlinkshereForm( $options ) ); - $wgOut->addWikiText( wfMsg( 'linkshere', $this->target->getPrefixedText() ) ); - } - $isredir = wfMsg( 'isredirect' ); - $istemplate = wfMsg( 'istemplate' ); + $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() ); - if( $level == 0 ) { $prevnext = $this->getPrevNext( $limit, $prevId, $nextId, $options['namespace'] ); $wgOut->addHTML( $prevnext ); } @@ -221,14 +215,14 @@ class WhatLinksHerePage { // Display properties (redirect or template) $props = array(); if ( $row->page_is_redirect ) { - $props[] = $isredir; + $props[] = wfMsgHtml( 'isredirect' ); } if ( $row->is_template ) { - $props[] = $istemplate; + $props[] = wfMsgHtml( 'istemplate' ); } if ( count( $props ) ) { - // FIXME? Cultural assumption, hard-coded punctuation - $wgOut->addHTML( ' (' . implode( ', ', $props ) . ') ' ); + $list = implode( wfMsgHtml( 'semicolon-separator' ), $props ); + $wgOut->addHTML( " ($list) " ); } # Space for utilities links, with a what-links-here link provided @@ -237,7 +231,7 @@ class WhatLinksHerePage { wfMsgHtml( 'whatlinkshere-links' ), 'target=' . $nt->getPrefixedUrl() ); - $wgOut->addHtml( ' <span class="mw-whatlinkshere-tools">(' . $wlh . ')</span>' ); + $wgOut->addHtml( ' <span class="mw-whatlinkshere-tools">(' . $wlh . ')</span>' ); if ( $row->page_is_redirect ) { if ( $level < 2 ) { @@ -284,7 +278,7 @@ class WhatLinksHerePage { $this->numLink( 250, $prevId ) . ' | ' . $this->numLink( 500, $prevId ); - return wfMsg( 'viewprevnext', $prevLink, $nextLink, $nums ); + return wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $nums ); } function numLink( $limit, $from, $ns = null ) { @@ -295,24 +289,26 @@ class WhatLinksHerePage { return $this->makeSelfLink( $fmtLimit, $query ); } - function whatlinkshereForm( $options ) { + function whatlinkshereForm( $options = array( 'target' => '', 'namespace' => '' ) ) { global $wgScript, $wgTitle; $options['title'] = $wgTitle->getPrefixedText(); - $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => "$wgScript" ) ) . - '<fieldset>' . - Xml::element( 'legend', array(), wfMsg( 'whatlinkshere' ) ); + $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', array(), wfMsg( 'whatlinkshere' ) ) . + Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target', 'mw-whatlinkshere-target', 40, $options['target'] ) . ' '; foreach ( $options as $name => $value ) { - if( $name === 'namespace') continue; + if( $name === 'namespace' || $name === 'target' ) + continue; $f .= "\t" . Xml::hidden( $name, $value ). "\n"; } $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . Xml::namespaceSelector( $options['namespace'], '' ) . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . - '</fieldset>' . + Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n"; return $f; @@ -324,5 +320,3 @@ class WhatLinksHerePage { } } - - diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php index 33464586..37d9a282 100644 --- a/includes/SpecialWithoutinterwiki.php +++ b/includes/SpecialWithoutinterwiki.php @@ -8,13 +8,41 @@ * @author Rob Church <robchur@gmail.com> */ class WithoutInterwikiPage extends PageQueryPage { + private $prefix = ''; function getName() { return 'Withoutinterwiki'; } function getPageHeader() { - return '<p>' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '</p>'; + global $wgScript, $wgContLang; + $prefix = $this->prefix; + $t = SpecialPage::getTitleFor( $this->getName() ); + $align = $wgContLang->isRtl() ? 'left' : 'right'; + + $s = '<p>' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '</p>'; + $s .= Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $s .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $s .= Xml::hidden( 'title', $t->getPrefixedText() ); + $s .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'withoutinterwiki' ) ); + $s .= "<tr> + <td align='$align'>" . + Xml::label( wfMsg( 'allpagesprefix' ), 'wiprefix' ) . + "</td> + <td>" . + Xml::input( 'prefix', 20, htmlspecialchars ( $prefix ), array( 'id' => 'wiprefix' ) ) . + "</td> + </tr> + <tr> + <td align='$align'></td> + <td>" . + Xml::submitButton( wfMsgHtml( 'withoutinterwiki-submit' ) ) . + "</td> + </tr>"; + $s .= Xml::closeElement( 'table' ); + $s .= Xml::closeElement( 'form' ); + $s .= Xml::closeElement( 'div' ); + return $s; } function sortDescending() { @@ -32,6 +60,7 @@ class WithoutInterwikiPage extends PageQueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' ); + $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : ''; return "SELECT 'Withoutinterwiki' AS type, page_namespace AS namespace, @@ -42,14 +71,22 @@ class WithoutInterwikiPage extends PageQueryPage { ON ll_from = page_id WHERE ll_title IS NULL AND page_namespace=" . NS_MAIN . " - AND page_is_redirect = 0"; + AND page_is_redirect = 0 + {$prefix}"; + } + + function setPrefix( $prefix = '' ) { + $this->prefix = $prefix; } } function wfSpecialWithoutinterwiki() { + global $wgRequest; list( $limit, $offset ) = wfCheckLimits(); + $prefix = $wgRequest->getVal( 'prefix' ); $wip = new WithoutInterwikiPage(); + $wip->setPrefix( $prefix ); $wip->doQuery( $offset, $limit ); } diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php index 5d7350a9..db2750cd 100644 --- a/includes/SquidUpdate.php +++ b/includes/SquidUpdate.php @@ -32,7 +32,7 @@ class SquidUpdate { array( 'page_namespace', 'page_title' ), array( 'pl_namespace' => $title->getNamespace(), - 'pl_title' => $title->getDbKey(), + 'pl_title' => $title->getDBkey(), 'pl_from=page_id' ), $fname ); $blurlArr = $title->getSquidURLs(); diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 8ecaa4f0..2dbbe6de 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -2,7 +2,7 @@ /** */ /** */ -function wfStreamFile( $fname ) { +function wfStreamFile( $fname, $headers = array() ) { $stat = @stat( $fname ); if ( !$stat ) { header( 'HTTP/1.0 404 Not Found' ); @@ -34,6 +34,10 @@ function wfStreamFile( $fname ) { global $wgContLanguageCode; header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) ) ); + foreach ( $headers as $header ) { + header( $header ); + } + if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); $sinceTime = strtotime( $modsince ); diff --git a/includes/StubObject.php b/includes/StubObject.php index a9a6bde9..aa72c360 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -105,7 +105,7 @@ class StubUserLang extends StubObject { $code = $wgContLanguageCode; } - if( $code == $wgContLanguageCode ) { + if( $code === $wgContLanguageCode || !Language::localisationExist( $code ) ) { return $wgContLang; } else { $obj = Language::factory( $code ); @@ -135,3 +135,4 @@ class StubUser extends StubObject { } + diff --git a/includes/Title.php b/includes/Title.php index c4db4172..8a9d3eee 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -207,6 +207,9 @@ class Title { * Make an array of titles from an array of IDs */ public static function newFromIDs( $ids ) { + if ( !count( $ids ) ) { + return array(); + } $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ), 'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ ); @@ -265,7 +268,12 @@ class Title { * @return Title the new object */ public static function newMainPage() { - return Title::newFromText( wfMsgForContent( 'mainpage' ) ); + $title = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + // Don't give fatal errors if the message is broken + if ( !$title ) { + $title = Title::newFromText( 'Main Page' ); + } + return $title; } /** @@ -670,7 +678,7 @@ class Title { */ public function getBaseText() { global $wgNamespacesWithSubpages; - if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) { + if( !empty( $wgNamespacesWithSubpages[$this->mNamespace] ) ) { $parts = explode( '/', $this->getText() ); # Don't discard the real title if there's no subpage involved if( count( $parts ) > 1 ) @@ -794,16 +802,15 @@ class Title { } else { $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); if ( $query == '' ) { - if($variant!=false && $wgContLang->hasVariants()){ - if($wgVariantArticlePath==false) { + if( $variant != false && $wgContLang->hasVariants() ) { + if( $wgVariantArticlePath == false ) { $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default } else { $variantArticlePath = $wgVariantArticlePath; } $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath ); $url = str_replace( '$1', $dbkey, $url ); - } - else { + } else { $url = str_replace( '$1', $dbkey, $wgArticlePath ); } } else { @@ -931,7 +938,7 @@ class Title { /** * Does the title correspond to a protected article? * @param string $what the action the page is protected from, - * by default checks move and edit + * by default checks move and edit * @return boolean */ public function isProtected( $action = '' ) { @@ -980,7 +987,7 @@ class Title { return $this->mWatched; } - /** + /** * Can $wgUser perform $action on this page? * This skips potentially expensive cascading permission checks. * @@ -999,7 +1006,7 @@ class Title { /** * Determines if $wgUser is unable to edit this page because it has been protected * by $wgNamespaceProtection. - * + * * @return boolean */ public function isNamespaceProtected() { @@ -1013,7 +1020,7 @@ class Title { return false; } - /** + /** * Can $wgUser perform $action on this page? * @param string $action action that permission needs to be checked for * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. @@ -1024,27 +1031,23 @@ class Title { return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array()); } - /** + /** * Can $user perform $action on this page? + * + * FIXME: This *does not* check throttles (User::pingLimiter()). + * * @param string $action action that permission needs to be checked for * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. - */ + */ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) { $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); global $wgContLang; global $wgLang; - - if ( wfReadOnly() && $action != 'read' ) { - global $wgReadOnly; - $errors[] = array( 'readonlytext', $wgReadOnly ); - } - global $wgEmailConfirmToEdit, $wgUser; - if ( $wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed() ) - { + if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { $errors[] = array( 'confirmedittext' ); } @@ -1056,6 +1059,9 @@ class Title { $id = $user->blockedBy(); $reason = $user->blockedFor(); + if( $reason == '' ) { + $reason = wfMsg( 'blockednoreason' ); + } $ip = wfGetIP(); if ( is_numeric( $id ) ) { @@ -1067,7 +1073,7 @@ class Title { $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; $blockid = $block->mId; $blockExpiry = $user->mBlock->mExpiry; - $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true ); if ( $blockExpiry == 'infinity' ) { // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' @@ -1097,22 +1103,45 @@ class Title { } /** - * Can $user perform $action on this page? - * This is an internal function, which checks ONLY that previously checked by userCan (i.e. it leaves out checks on wfReadOnly() and blocks) + * Can $user perform $action on this page? This is an internal function, + * which checks ONLY that previously checked by userCan (i.e. it leaves out + * checks on wfReadOnly() and blocks) + * * @param string $action action that permission needs to be checked for * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. */ private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { - $fname = 'Title::userCan'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $errors = array(); + // Use getUserPermissionsErrors instead if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { return $result ? array() : array( array( 'badaccess-group0' ) ); } + if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { + if ($result != array() && is_array($result) && !is_array($result[0])) + $errors[] = $result; # A single array representing an error + else if (is_array($result) && is_array($result[0])) + $errors = array_merge( $errors, $result ); # A nested array representing multiple errors + else if ($result != '' && $result != null && $result !== true && $result !== false) + $errors[] = array($result); # A string representing a message-id + else if ($result === false ) + $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" + } + if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) { + if ($result != array() && is_array($result) && !is_array($result[0])) + $errors[] = $result; # A single array representing an error + else if (is_array($result) && is_array($result[0])) + $errors = array_merge( $errors, $result ); # A nested array representing multiple errors + else if ($result != '' && $result != null && $result !== true && $result !== false) + $errors[] = array($result); # A string representing a message-id + else if ($result === false ) + $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" + } + if( NS_SPECIAL == $this->mNamespace ) { $errors[] = array('ns-specialprotected'); } @@ -1135,7 +1164,7 @@ class Title { # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working if( $this->isCssJsSubpage() - && !$user->isAllowed('editinterface') + && !$user->isAllowed('editusercssjs') && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) { $errors[] = array('customcssjsprotected'); } @@ -1169,52 +1198,146 @@ class Title { $right = 'protect'; } if( '' != $right && !$user->isAllowed( $right ) ) { - $errors[] = array( 'protectedpagetext' ); + $errors[] = array( 'protectedpagetext', $right ); } } - if( $action == 'create' ) { + if ($action == 'protect') { + if ($this->getUserPermissionsErrors('edit', $user) != array()) { + $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect. + } + } + + if ($action == 'create') { + $title_protection = $this->getTitleProtection(); + + if (is_array($title_protection)) { + extract($title_protection); + + if ($pt_create_perm == 'sysop') + $pt_create_perm = 'protect'; + + if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) { + $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason ); + } + } + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); } } elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) { $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } else if ( !$user->isAllowed( $action ) ) { + } elseif ( !$user->isAllowed( $action ) ) { $return = null; - $groups = array(); + $groups = array(); global $wgGroupPermissions; - foreach( $wgGroupPermissions as $key => $value ) { - if( isset( $value[$action] ) && $value[$action] == true ) { - $groupName = User::getGroupName( $key ); - $groupPage = User::getGroupPage( $key ); - if( $groupPage ) { - $skin = $user->getSkin(); - $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]'; - } else { - $groups[] = $groupName; - } - } - } - $n = count( $groups ); - $groups = implode( ', ', $groups ); - switch( $n ) { - case 0: - case 1: - case 2: - $return = array( "badaccess-group$n", $groups ); - break; - default: - $return = array( 'badaccess-groups', $groups ); - } + foreach( $wgGroupPermissions as $key => $value ) { + if( isset( $value[$action] ) && $value[$action] == true ) { + $groupName = User::getGroupName( $key ); + $groupPage = User::getGroupPage( $key ); + if( $groupPage ) { + $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]'; + } else { + $groups[] = $groupName; + } + } + } + $n = count( $groups ); + $groups = implode( ', ', $groups ); + switch( $n ) { + case 0: + case 1: + case 2: + $return = array( "badaccess-group$n", $groups ); + break; + default: + $return = array( 'badaccess-groups', $groups ); + } $errors[] = $return; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $errors; } /** + * Is this title subject to title protection? + * @return mixed An associative array representing any existent title + * protection, or false if there's none. + */ + private function getTitleProtection() { + // Can't protect pages in special namespaces + if ( $this->getNamespace() < 0 ) { + return false; + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'protected_titles', '*', + array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) ); + + if ($row = $dbr->fetchRow( $res )) { + return $row; + } else { + return false; + } + } + + public function updateTitleProtection( $create_perm, $reason, $expiry ) { + global $wgGroupPermissions,$wgUser,$wgContLang; + + if ($create_perm == implode(',',$this->getRestrictions('create')) + && $expiry == $this->mRestrictionsExpiry) { + // No change + return true; + } + + list ($namespace, $title) = array( $this->getNamespace(), $this->getDBkey() ); + + $dbw = wfGetDB( DB_MASTER ); + + $encodedExpiry = Block::encodeExpiry($expiry, $dbw ); + + $expiry_description = ''; + if ( $encodedExpiry != 'infinity' ) { + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + } + + # Update protection table + if ($create_perm != '' ) { + $dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')), + array( 'pt_namespace' => $namespace, 'pt_title' => $title + , 'pt_create_perm' => $create_perm + , 'pt_timestamp' => Block::encodeExpiry(wfTimestampNow(), $dbw) + , 'pt_expiry' => $encodedExpiry + , 'pt_user' => $wgUser->getId(), 'pt_reason' => $reason ), __METHOD__ ); + } else { + $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace, + 'pt_title' => $title ), __METHOD__ ); + } + # Update the protection log + $log = new LogPage( 'protect' ); + + if( $create_perm ) { + $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) ); + } else { + $log->addEntry( 'unprotect', $this, $reason ); + } + + return true; + } + + /** + * Remove any title protection (due to page existing + */ + public function deleteTitleProtection() { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->delete( 'protected_titles', + array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ ); + } + + /** * Can $wgUser edit this page? * @return boolean * @deprecated use userCan('edit') @@ -1258,8 +1381,12 @@ class Title { * @todo fold these checks into userCan() */ public function userCanRead() { - global $wgUser; - + global $wgUser, $wgGroupPermissions; + + # Shortcut for public wikis, allows skipping quite a bit of code path + if ($wgGroupPermissions['*']['read']) + return true; + $result = null; wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) ); if ( $result !== null ) { @@ -1278,19 +1405,26 @@ class Title { if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) { return true; } + + /** + * Bail out if there isn't whitelist + */ + if( !is_array($wgWhitelistRead) ) { + return false; + } /** * Check for explicit whitelisting */ $name = $this->getPrefixedText(); - if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead, true ) ) + if( in_array( $name, $wgWhitelistRead, true ) ) return true; /** * Old settings might have the title prefixed with * a colon for main-namespace pages */ - if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) { + if( $this->getNamespace() == NS_MAIN ) { if( in_array( ':' . $name, $wgWhitelistRead ) ) return true; } @@ -1300,8 +1434,13 @@ class Title { * and check again */ if( $this->getNamespace() == NS_SPECIAL ) { - $name = $this->getText(); + $name = $this->getDBkey(); list( $name, /* $subpage */) = SpecialPage::resolveAliasWithSubpage( $name ); + if ( $name === false ) { + # Invalid special page, but we show standard login required message + return false; + } + $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); if( in_array( $pure, $wgWhitelistRead, true ) ) return true; @@ -1394,7 +1533,7 @@ class Title { */ public function userCanEditCssJsSubpage() { global $wgUser; - return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); + return ( $wgUser->isAllowed('editusercssjs') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); } /** @@ -1579,12 +1718,32 @@ class Title { public function loadRestrictions( $oldFashionedRestrictions = NULL ) { if( !$this->mRestrictionsLoaded ) { - $dbr = wfGetDB( DB_SLAVE ); + if ($this->exists()) { + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( 'page_restrictions', '*', + array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); + + $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + } else { + $title_protection = $this->getTitleProtection(); + + if (is_array($title_protection)) { + extract($title_protection); - $res = $dbr->select( 'page_restrictions', '*', - array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); + $now = wfTimestampNow(); + $expiry = Block::decodeExpiry($pt_expiry); - $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + if (!$expiry || $expiry > $now) { + // Apply the restrictions + $this->mRestrictionsExpiry = $expiry; + $this->mRestrictions['create'] = explode(',', trim($pt_create_perm) ); + } else { // Get rid of the old restrictions + Title::purgeExpiredRestrictions(); + } + } + $this->mRestrictionsLoaded = true; + } } } @@ -1596,6 +1755,10 @@ class Title { $dbw->delete( 'page_restrictions', array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + + $dbw->delete( 'protected_titles', + array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), + __METHOD__ ); } /** @@ -1605,16 +1768,12 @@ class Title { * @return array the array of groups allowed to edit this article */ public function getRestrictions( $action ) { - if( $this->exists() ) { - if( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - return isset( $this->mRestrictions[$action] ) - ? $this->mRestrictions[$action] - : array(); - } else { - return array(); + if( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); } /** @@ -1753,8 +1912,18 @@ class Title { # Initialisation static $rxTc = false; if( !$rxTc ) { - # % is needed as well - $rxTc = '/[^' . Title::legalChars() . ']|%[0-9A-Fa-f]{2}/S'; + # Matching titles will be held as illegal. + $rxTc = '/' . + # Any character not allowed is forbidden... + '[^' . Title::legalChars() . ']' . + # URL percent encoding sequences interfere with the ability + # to round-trip titles -- you can't link to them consistently. + '|%[0-9A-Fa-f]{2}' . + # XML/HTML character references produce similar issues. + '|&[A-Za-z0-9\x80-\xff]+;' . + '|&#[0-9]+;' . + '|&#x[0-9A-Fa-f]+;' . + '/S'; } $this->mInterwiki = $this->mFragment = ''; @@ -1868,7 +2037,9 @@ class Title { strpos( $dbkey, './' ) === 0 || strpos( $dbkey, '../' ) === 0 || strpos( $dbkey, '/./' ) !== false || - strpos( $dbkey, '/../' ) !== false ) ) + strpos( $dbkey, '/../' ) !== false || + substr( $dbkey, -2 ) == '/.' || + substr( $dbkey, -3 ) == '/..' ) ) { return false; } @@ -1993,7 +2164,7 @@ class Title { array( "{$prefix}_from=page_id", "{$prefix}_namespace" => $this->getNamespace(), - "{$prefix}_title" => $this->getDbKey() ), + "{$prefix}_title" => $this->getDBkey() ), 'Title::getLinksTo', $options ); @@ -2027,10 +2198,16 @@ class Title { /** * Get an array of Title objects referring to non-existent articles linked from this page * + * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case) * @param string $options may be FOR UPDATE * @return array the Title objects */ public function getBrokenLinksFrom( $options = '' ) { + if ( $this->getArticleId() == 0 ) { + # All links from article ID 0 are false positives + return array(); + } + if ( $options ) { $db = wfGetDB( DB_MASTER ); } else { @@ -2137,10 +2314,20 @@ class Title { return 'badarticleerror'; } - if ( $auth && ( - !$this->userCan( 'edit' ) || !$nt->userCan( 'edit' ) || - !$this->userCan( 'move' ) || !$nt->userCan( 'move' ) ) ) { - return 'protectedpage'; + if ( $auth ) { + global $wgUser; + $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser), + $this->getUserPermissionsErrors('edit', $wgUser), + $nt->getUserPermissionsErrors('move', $wgUser), + $nt->getUserPermissionsErrors('edit', $wgUser)); + if($errors !== array()) + return $errors[0][0]; + } + + global $wgUser; + $err = null; + if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err ) ) ) { + return 'hookaborted'; } # The move is allowed only if (1) the target doesn't exist, or @@ -2151,6 +2338,11 @@ class Title { if ( ! $this->isValidMoveTarget( $nt ) ) { return 'articleexists'; } + } else { + $tp = $nt->getTitleProtection(); + if ( $tp and !$wgUser->isAllowed( $tp['pt_create_perm'] ) ) { + return 'cantmove-titleprotected'; + } } return true; } @@ -2160,9 +2352,12 @@ class Title { * @param Title &$nt the new title * @param bool $auth indicates whether $wgUser's permissions * should be checked + * @param string $reason The reason for the move + * @param bool $createRedirect Whether to create a redirect from the old title to the new title. + * Ignored if the user doesn't have the suppressredirect right. * @return mixed true on success, message name on failure */ - public function moveTo( &$nt, $auth = true, $reason = '' ) { + public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $err = $this->isValidMoveOperation( $nt, $auth ); if( is_string( $err ) ) { return $err; @@ -2170,21 +2365,34 @@ class Title { $pageid = $this->getArticleID(); if( $nt->exists() ) { - $this->moveOverExistingRedirect( $nt, $reason ); - $pageCountChange = 0; + $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); + $pageCountChange = ($createRedirect ? 0 : -1); } else { # Target didn't exist, do normal move. - $this->moveToNewTitle( $nt, $reason ); - $pageCountChange = 1; + $this->moveToNewTitle( $nt, $reason, $createRedirect ); + $pageCountChange = ($createRedirect ? 1 : 0); } $redirid = $this->getArticleID(); - # Fixing category links (those without piped 'alternate' names) to be sorted under the new title + // Category memberships include a sort key which may be customized. + // If it's left as the default (the page title), we need to update + // the sort key to match the new title. + // + // Be careful to avoid resetting cl_timestamp, which may disturb + // time-based lists on some sites. + // + // Warning -- if the sort key is *explicitly* set to the old title, + // we can't actually distinguish it from a default here, and it'll + // be set to the new title even though it really shouldn't. + // It'll get corrected on the next edit, but resetting cl_timestamp. $dbw = wfGetDB( DB_MASTER ); - $categorylinks = $dbw->tableName( 'categorylinks' ); - $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) . - " WHERE cl_from=" . $dbw->addQuotes( $pageid ) . - " AND cl_sortkey=" . $dbw->addQuotes( $this->getPrefixedText() ); - $dbw->query( $sql, 'SpecialMovepage::doSubmit' ); + $dbw->update( 'categorylinks', + array( + 'cl_sortkey' => $nt->getPrefixedText(), + 'cl_timestamp=cl_timestamp' ), + array( + 'cl_from' => $pageid, + 'cl_sortkey' => $this->getPrefixedText() ), + __METHOD__ ); # Update watchlists @@ -2221,6 +2429,14 @@ class Title { } if( $u ) $u->doUpdate(); + # Update message cache for interface messages + if( $nt->getNamespace() == NS_MEDIAWIKI ) { + global $wgMessageCache; + $oldarticle = new Article( $this ); + $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + $newarticle = new Article( $nt ); + $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() ); + } global $wgUser; wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); @@ -2233,9 +2449,12 @@ class Title { * * @param Title &$nt the page to move to, which should currently * be a redirect + * @param string $reason The reason for the move + * @param bool $createRedirect Whether to leave a redirect at the old title. + * Ignored if the user doesn't have the suppressredirect right */ - private function moveOverExistingRedirect( &$nt, $reason = '' ) { - global $wgUseSquid; + private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { + global $wgUseSquid, $wgUser; $fname = 'Title::moveOverExistingRedirect'; $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() ); @@ -2247,13 +2466,25 @@ class Title { $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); $dbw = wfGetDB( DB_MASTER ); - $linkCache =& LinkCache::singleton(); # Delete the old redirect. We don't save it to history since # by definition if we've got here it's rather uninteresting. # We have to remove it so that the next step doesn't trigger # a conflict on the unique namespace+title index... $dbw->delete( 'page', array( 'page_id' => $newid ), $fname ); + if ( !$dbw->cascadingDeletes() ) { + $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ ); + global $wgUseTrackbacks; + if ($wgUseTrackbacks) + $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ ); + $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ ); + $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ ); + $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ ); + $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ ); + } # Save a null revision in the page's history notifying of the move $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); @@ -2270,35 +2501,39 @@ class Title { /* WHERE */ array( 'page_id' => $oldid ), $fname ); - $linkCache->clearLink( $nt->getPrefixedDBkey() ); + $nt->resetArticleID( $oldid ); # Recreate the redirect, this time in the other direction. - $mwRedir = MagicWord::get( 'redirect' ); - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; - $redirectArticle = new Article( $this ); - $newid = $redirectArticle->insertOn( $dbw ); - $redirectRevision = new Revision( array( - 'page' => $newid, - 'comment' => $comment, - 'text' => $redirectText ) ); - $redirectRevision->insertOn( $dbw ); - $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - $linkCache->clearLink( $this->getPrefixedDBkey() ); - + if($createRedirect || !$wgUser->isAllowed('suppressredirect')) + { + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $redirectArticle = new Article( $this ); + $newid = $redirectArticle->insertOn( $dbw ); + $redirectRevision = new Revision( array( + 'page' => $newid, + 'comment' => $comment, + 'text' => $redirectText ) ); + $redirectRevision->insertOn( $dbw ); + $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); + + # 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' => $newid ), $fname ); + $dbw->insert( 'pagelinks', + array( + 'pl_from' => $newid, + 'pl_namespace' => $nt->getNamespace(), + 'pl_title' => $nt->getDBkey() ), + $fname ); + } else { + $this->resetArticleID( 0 ); + } + # Log the move $log = new LogPage( 'move' ); $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) ); - # 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' => $newid ), $fname ); - $dbw->insert( 'pagelinks', - array( - 'pl_from' => $newid, - 'pl_namespace' => $nt->getNamespace(), - 'pl_title' => $nt->getDbKey() ), - $fname ); - # Purge squid if ( $wgUseSquid ) { $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() ); @@ -2310,9 +2545,12 @@ class Title { /** * Move page to non-existing title. * @param Title &$nt the new Title + * @param string $reason The reason for the move + * @param bool $createRedirect Whether to create a redirect from the old title to the new title + * Ignored if the user doesn't have the suppressredirect right */ - private function moveToNewTitle( &$nt, $reason = '' ) { - global $wgUseSquid; + private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { + global $wgUseSquid, $wgUser; $fname = 'MovePageForm::moveToNewTitle'; $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); if ( $reason ) { @@ -2323,13 +2561,12 @@ class Title { $oldid = $this->getArticleID(); $dbw = wfGetDB( DB_MASTER ); $now = $dbw->timestamp(); - $linkCache =& LinkCache::singleton(); # Save a null revision in the page's history notifying of the move $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); - # Rename cur entry + # Rename page entry $dbw->update( 'page', /* SET */ array( 'page_touched' => $now, @@ -2340,21 +2577,32 @@ class Title { /* WHERE */ array( 'page_id' => $oldid ), $fname ); + $nt->resetArticleID( $oldid ); - $linkCache->clearLink( $nt->getPrefixedDBkey() ); - - # Insert redirect - $mwRedir = MagicWord::get( 'redirect' ); - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; - $redirectArticle = new Article( $this ); - $newid = $redirectArticle->insertOn( $dbw ); - $redirectRevision = new Revision( array( - 'page' => $newid, - 'comment' => $comment, - 'text' => $redirectText ) ); - $redirectRevision->insertOn( $dbw ); - $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - $linkCache->clearLink( $this->getPrefixedDBkey() ); + if($createRedirect || !$wgUser->isAllowed('suppressredirect')) + { + # Insert redirect + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $redirectArticle = new Article( $this ); + $newid = $redirectArticle->insertOn( $dbw ); + $redirectRevision = new Revision( array( + 'page' => $newid, + 'comment' => $comment, + 'text' => $redirectText ) ); + $redirectRevision->insertOn( $dbw ); + $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); + + # Record the just-created redirect's linking to the page + $dbw->insert( 'pagelinks', + array( + 'pl_from' => $newid, + 'pl_namespace' => $nt->getNamespace(), + 'pl_title' => $nt->getDBkey() ), + $fname ); + } else { + $this->resetArticleID( 0 ); + } # Log the move $log = new LogPage( 'move' ); @@ -2363,14 +2611,6 @@ class Title { # Purge caches as per article creation Article::onArticleCreate( $nt ); - # Record the just-created redirect's linking to the page - $dbw->insert( 'pagelinks', - array( - 'pl_from' => $newid, - 'pl_namespace' => $nt->getNamespace(), - 'pl_title' => $nt->getDBkey() ), - $fname ); - # Purge old title from squid # The new title, and links to the new title, are purged in Article::onArticleCreate() $this->purgeSquid(); @@ -2469,7 +2709,7 @@ class Title { $data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText(); $dbr->freeResult ( $res ) ; } else { - $data = ''; + $data = array(); } return $data; } @@ -2562,7 +2802,7 @@ class Title { // Note: === is necessary for proper matching of number-like titles. return $this->getInterwiki() === $title->getInterwiki() && $this->getNamespace() == $title->getNamespace() - && $this->getDbkey() === $title->getDbkey(); + && $this->getDBkey() === $title->getDBkey(); } /** @@ -2589,9 +2829,15 @@ class Title { * @return bool */ public function isAlwaysKnown() { + // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes + // the full l10n of that language to be loaded. That takes much memory and + // isn't needed. So we strip the language part away. + // Also, extension messages which are not loaded, are shown as red, because + // we don't call MessageCache::loadAllMessages. + list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 ); return $this->isExternal() || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' ) - || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $this->mDbkeyform ) ); + || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) ); } /** @@ -2730,5 +2976,3 @@ class Title { } } - - diff --git a/includes/User.php b/includes/User.php index 51b0b2ec..8e3c776a 100644 --- a/includes/User.php +++ b/includes/User.php @@ -34,8 +34,8 @@ class PasswordError extends MWException { class User { /** - * A list of default user toggles, i.e. boolean user preferences that are - * displayed by Special:Preferences as checkboxes. This list can be + * A list of default user toggles, i.e. boolean user preferences that are + * displayed by Special:Preferences as checkboxes. This list can be * extended via the UserToggles hook or $wgContLang->getExtraUserToggles(). */ static public $mToggles = array( @@ -80,7 +80,7 @@ class User { /** * List of member variables which are saved to the shared cache (memcached). - * Any operation which changes the corresponding database fields must + * Any operation which changes the corresponding database fields must * call a cache-clearing function. */ static $mCacheVars = array( @@ -107,8 +107,8 @@ class User { /** * The cache variable declarations */ - var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, - $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, + var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, + $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; /** @@ -133,7 +133,7 @@ class User { var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights, $mBlockreason, $mBlock, $mEffectiveGroups; - /** + /** * Lightweight constructor for anonymous user * Use the User::newFrom* factory functions for other kinds of users */ @@ -188,7 +188,7 @@ class User { if ( $this->mId == 0 ) { $this->loadDefaults(); return false; - } + } # Try cache $key = wfMemcKey( 'user', 'id', $this->mId ); @@ -197,7 +197,7 @@ class User { # Object is expired, load from DB $data = false; } - + if ( !$data ) { wfDebug( "Cache miss for user {$this->mId}\n" ); # Load from DB @@ -206,13 +206,7 @@ class User { return false; } - # Save to cache - $data = array(); - foreach ( self::$mCacheVars as $name ) { - $data[$name] = $this->$name; - } - $data['mVersion'] = MW_USER_VERSION; - $wgMemc->set( $key, $data ); + $this->saveToCache(); } else { wfDebug( "Got user {$this->mId} from cache\n" ); # Restore from cache @@ -224,19 +218,38 @@ class User { } /** + * Save user data to the shared cache + */ + function saveToCache() { + $this->load(); + if ( $this->isAnon() ) { + // Anonymous users are uncached + return; + } + $data = array(); + foreach ( self::$mCacheVars as $name ) { + $data[$name] = $this->$name; + } + $data['mVersion'] = MW_USER_VERSION; + $key = wfMemcKey( 'user', 'id', $this->mId ); + global $wgMemc; + $wgMemc->set( $key, $data ); + } + + /** * Static factory method for creation from username. * * This is slightly less efficient than newFromId(), so use newFromId() if - * you have both an ID and a name handy. + * you have both an ID and a name handy. * * @param string $name Username, validated by Title:newFromText() - * @param mixed $validate Validate username. Takes the same parameters as - * User::getCanonicalName(), except that true is accepted as an alias + * @param mixed $validate Validate username. Takes the same parameters as + * User::getCanonicalName(), except that true is accepted as an alias * for 'valid', for BC. - * - * @return User object, or null if the username is invalid. If the username + * + * @return User object, or null if the username is invalid. If the username * is not present in the database, the result will be a user object with - * a name, zero user ID and default settings. + * a name, zero user ID and default settings. * @static */ static function newFromName( $name, $validate = 'valid' ) { @@ -285,7 +298,7 @@ class User { return null; } } - + /** * Create a new user object using data from session or cookies. If the * login credentials are invalid, the result is an anonymous user. @@ -348,9 +361,9 @@ class User { * * This function exists for username validation, in order to reject * usernames which are similar in form to IP addresses. Strings such - * as 300.300.300.300 will return true because it looks like an IP + * as 300.300.300.300 will return true because it looks like an IP * address, despite not being strictly valid. - * + * * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP * address because the usemod software would "cloak" anonymous IP * addresses like this, if we allowed accounts like this to be created @@ -374,8 +387,8 @@ class User { * Check if $name is an IPv6 IP. */ static function isIPv6($name) { - /* - * if it has any non-valid characters, it can't be a valid IPv6 + /* + * if it has any non-valid characters, it can't be a valid IPv6 * address. */ if (preg_match("/[^:a-fA-F0-9]/", $name)) @@ -420,7 +433,7 @@ class User { || $parsed->getNamespace() || strcmp( $name, $parsed->getPrefixedText() ) ) return false; - + // Check an additional blacklist of troublemaker characters. // Should these be merged into the title char list? $unicodeBlacklist = '/[' . @@ -434,10 +447,10 @@ class User { if( preg_match( $unicodeBlacklist, $name ) ) { return false; } - + return true; } - + /** * Usernames which fail to pass this function will be blocked * from user login and new account registrations, but may be used @@ -454,11 +467,11 @@ class User { return // Must be a valid username, obviously ;) self::isValidUserName( $name ) && - + // Certain names may be reserved for batch processes. !in_array( $name, $wgReservedUsernames ); } - + /** * Usernames which fail to pass this function will be blocked * from new account registrations, but may be used internally @@ -475,7 +488,7 @@ class User { static function isCreatableName( $name ) { return self::isUsableName( $name ) && - + // Registration-time character blacklisting... strpos( $name, '@' ) === false; } @@ -494,7 +507,7 @@ class User { return $result; if( $result === false ) return false; - + // Password needs to be long enough, and can't be the same as the username return strlen( $password ) >= $wgMinimalPasswordLength && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName ); @@ -513,11 +526,16 @@ class User { * @return bool */ public static function isValidEmailAddr( $addr ) { + $result = null; + if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) { + return $result; + } + return strpos( $addr, '@' ) !== false; } /** - * Given unvalidated user input, return a canonical username, or false if + * Given unvalidated user input, return a canonical username, or false if * the username is invalid. * @param string $name * @param mixed $validate Type of validation to use: @@ -576,7 +594,7 @@ class User { * Count the number of edits of a user * * It should not be static and some day should be merged as proper member function / deprecated -- domas - * + * * @param int $uid The user ID to check * @return int * @static @@ -592,7 +610,7 @@ class User { ); if( $field === null ) { // it has not been initialized. do so. - $dbw = wfGetDb( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $count = $dbr->selectField( 'revision', 'count(*)', array( 'rev_user' => $uid ), @@ -633,7 +651,7 @@ class User { } /** - * Set cached properties to default. Note: this no longer clears + * Set cached properties to default. Note: this no longer clears * uncached lazy-initialised properties. The constructor does that instead. * * @private @@ -666,7 +684,7 @@ class User { wfProfileOut( __METHOD__ ); } - + /** * Initialise php session * @deprecated use wfSetupSession() @@ -713,7 +731,7 @@ class User { # Not a valid ID, loadFromId has switched the object to anon for us return false; } - + if ( isset( $_SESSION['wsToken'] ) ) { $passwordCorrect = $_SESSION['wsToken'] == $this->mToken; $from = 'session'; @@ -737,11 +755,11 @@ class User { return false; } } - + /** * Load user and user_group data from the database * $this->mId must be set, this is how the user is identified. - * + * * @return true if the user exists, false if the user is anonymous * @private */ @@ -773,7 +791,7 @@ class User { $this->mEmailToken = $s->user_email_token; $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires ); $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration ); - $this->mEditCount = $s->user_editcount; + $this->mEditCount = $s->user_editcount; $this->getEditCount(); // revalidation for nulls # Load group data @@ -795,9 +813,9 @@ class User { } /** - * Clear various cached data stored in this object. - * @param string $reloadFrom Reload user and user_groups table data from a - * given source. May be "name", "id", "defaults", "session" or false for + * Clear various cached data stored in this object. + * @param string $reloadFrom Reload user and user_groups table data from a + * given source. May be "name", "id", "defaults", "session" or false for * no reload. */ function clearInstanceCache( $reloadFrom = false ) { @@ -891,7 +909,7 @@ class User { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": checking...\n" ); - $this->mBlockedby = 0; + $this->mBlockedby = 0; $this->mHideName = 0; $ip = wfGetIP(); @@ -1004,7 +1022,7 @@ class User { return $result; } - global $wgRateLimits, $wgRateLimitsExcludedGroups; + global $wgRateLimits; if( !isset( $wgRateLimits[$action] ) ) { return false; } @@ -1158,12 +1176,12 @@ class User { } /** - * Set the user name. + * Set the user name. * - * This does not reload fields from the database according to the given + * This does not reload fields from the database according to the given * name. Rather, it is used to create a temporary "nonexistent user" for - * later addition to the database. It can also be used to set the IP - * address for an anonymous user to something other than the current + * later addition to the database. It can also be used to set the IP + * address for an anonymous user to something other than the current * remote IP. * * User::newFromName() has rougly the same function, when the named user @@ -1196,11 +1214,13 @@ class User { global $wgMemc; $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); $newtalk = $wgMemc->get( $key ); - if( $newtalk != "" ) { + if( strval( $newtalk ) !== '' ) { $this->mNewtalk = (bool)$newtalk; } else { - $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() ); - $wgMemc->set( $key, (int)$this->mNewtalk, time() + 1800 ); + // Since we are caching this, make sure it is up to date by getting it + // from the master + $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); + $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); } } else { $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); @@ -1225,20 +1245,24 @@ class User { return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL())); } - + /** - * Perform a user_newtalk check on current slaves; if the memcached data - * is funky we don't want newtalk state to get stuck on save, as that's - * damn annoying. - * + * Perform a user_newtalk check, uncached. + * Use getNewtalk for a cached check. + * * @param string $field * @param mixed $id + * @param bool $fromMaster True to fetch from the master, false for a slave * @return bool * @private */ - function checkNewtalk( $field, $id ) { - $dbr = wfGetDB( DB_SLAVE ); - $ok = $dbr->selectField( 'user_newtalk', $field, + function checkNewtalk( $field, $id, $fromMaster = false ) { + if ( $fromMaster ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_SLAVE ); + } + $ok = $db->selectField( 'user_newtalk', $field, array( $field => $id ), __METHOD__ ); return $ok !== false; } @@ -1250,17 +1274,18 @@ class User { * @private */ function updateNewtalk( $field, $id ) { - if( $this->checkNewtalk( $field, $id ) ) { - wfDebug( __METHOD__." already set ($field, $id), ignoring\n" ); - return false; - } $dbw = wfGetDB( DB_MASTER ); $dbw->insert( 'user_newtalk', array( $field => $id ), __METHOD__, 'IGNORE' ); - wfDebug( __METHOD__.": set on ($field, $id)\n" ); - return true; + if ( $dbw->affectedRows() ) { + wfDebug( __METHOD__.": set on ($field, $id)\n" ); + return true; + } else { + wfDebug( __METHOD__." already set ($field, $id)\n" ); + return false; + } } /** @@ -1270,16 +1295,17 @@ class User { * @private */ function deleteNewtalk( $field, $id ) { - if( !$this->checkNewtalk( $field, $id ) ) { - wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" ); - return false; - } $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'user_newtalk', array( $field => $id ), __METHOD__ ); - wfDebug( __METHOD__.": killed on ($field, $id)\n" ); - return true; + if ( $dbw->affectedRows() ) { + wfDebug( __METHOD__.": killed on ($field, $id)\n" ); + return true; + } else { + wfDebug( __METHOD__.": already gone ($field, $id)\n" ); + return false; + } } /** @@ -1301,6 +1327,7 @@ class User { $field = 'user_id'; $id = $this->getId(); } + global $wgMemc; if( $val ) { $changed = $this->updateNewtalk( $field, $id ); @@ -1308,24 +1335,17 @@ class User { $changed = $this->deleteNewtalk( $field, $id ); } - if( $changed ) { - if( $this->isAnon() ) { - // Anons have a separate memcached space, since - // user records aren't kept for them. - global $wgMemc; - $key = wfMemcKey( 'newtalk', 'ip', $val ); - $wgMemc->set( $key, $val ? 1 : 0 ); - } else { - if( $val ) { - // Make sure the user page is watched, so a notification - // will be sent out if enabled. - $this->addWatch( $this->getTalkPage() ); - } - } + if( $this->isAnon() ) { + // Anons have a separate memcached space, since + // user records aren't kept for them. + $key = wfMemcKey( 'newtalk', 'ip', $id ); + $wgMemc->set( $key, $val ? 1 : 0, 1800 ); + } + if ( $changed ) { $this->invalidateCache(); } } - + /** * Generate a current or new-future timestamp to be stored in the * user_touched field when we update things. @@ -1334,7 +1354,7 @@ class User { global $wgClockSkewFudge; return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); } - + /** * Clear user data from memcached. * Use after applying fun updates to the database; caller's @@ -1358,13 +1378,13 @@ class User { $this->load(); if( $this->mId ) { $this->mTouched = self::newTouchedTimestamp(); - + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), array( 'user_id' => $this->mId ), __METHOD__ ); - + $this->clearSharedCache(); } } @@ -1402,12 +1422,12 @@ class User { */ function setPassword( $str ) { global $wgAuth; - + if( $str !== null ) { if( !$wgAuth->allowPasswordChange() ) { throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); } - + if( !$this->isValidPassword( $str ) ) { global $wgMinimalPasswordLength; throw new PasswordError( wfMsg( 'passwordtooshort', @@ -1418,7 +1438,7 @@ class User { if( !$wgAuth->setPassword( $this, $str ) ) { throw new PasswordError( wfMsg( 'externaldberror' ) ); } - + $this->setInternalPassword( $str ); return true; @@ -1433,7 +1453,7 @@ class User { function setInternalPassword( $str ) { $this->load(); $this->setToken(); - + if( $str === null ) { // Save an invalid hash... $this->mPassword = ''; @@ -1495,7 +1515,7 @@ class User { $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; return time() < $expiry; } - + function getEmail() { $this->load(); return $this->mEmail; @@ -1544,7 +1564,7 @@ class User { } /** - * Get the user's date preference, including some important migration for + * Get the user's date preference, including some important migration for * old user rows. */ function getDatePreference() { @@ -1567,7 +1587,7 @@ class User { function getBoolOption( $oname ) { return (bool)$this->getOption( $oname ); } - + /** * Get an option as an integer value from the source string. * @param string $oname The option to check @@ -1619,8 +1639,8 @@ class User { /** * Get the list of implicit group memberships this user has. - * This includes all explicit groups, plus 'user' if logged in - * and '*' for all accounts. + * This includes all explicit groups, plus 'user' if logged in, + * '*' for all accounts and autopromoted groups * @param boolean $recache Don't use the cache * @return array of strings */ @@ -1632,43 +1652,32 @@ class User { if( $this->mId ) { $this->mEffectiveGroups[] = 'user'; - global $wgAutoConfirmAge, $wgAutoConfirmCount; + $this->mEffectiveGroups = array_unique( array_merge( + $this->mEffectiveGroups, + Autopromote::getAutopromoteGroups( $this ) + ) ); - $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); - if( $accountAge >= $wgAutoConfirmAge && $this->getEditCount() >= $wgAutoConfirmCount ) { - $this->mEffectiveGroups[] = 'autoconfirmed'; - } - # Implicit group for users whose email addresses are confirmed - global $wgEmailAuthentication; - if( self::isValidEmailAddr( $this->mEmail ) ) { - if( $wgEmailAuthentication ) { - if( $this->mEmailAuthenticated ) - $this->mEffectiveGroups[] = 'emailconfirmed'; - } else { - $this->mEffectiveGroups[] = 'emailconfirmed'; - } - } # Hook for additional groups wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); } } return $this->mEffectiveGroups; } - + /* Return the edit count for the user. This is where User::edits should have been */ function getEditCount() { if ($this->mId) { if ( !isset( $this->mEditCount ) ) { /* Populate the count, if it has not been populated yet */ $this->mEditCount = User::edits($this->mId); - } + } return $this->mEditCount; } else { /* nil */ return null; } } - + /** * Add the user to the given group. * This takes immediate effect. @@ -1722,10 +1731,6 @@ class User { * @return bool */ function isLoggedIn() { - if( $this->mId === null and $this->mName !== null ) { - // Special-case optimization - return !self::isIP( $this->mName ); - } return $this->getID() != 0; } @@ -1893,7 +1898,7 @@ class User { 'wl_notificationtimestamp' => NULL ), array( /* WHERE */ 'wl_user' => $currentUser - ), 'UserMailer::clearAll' + ), __METHOD__ ); # we also need to clear here the "you have new message" notification for the own user_talk page @@ -1953,10 +1958,21 @@ class User { } /** - * Logout user - * Clears the cookies and session, resets the instance cache + * Logout user. */ function logout() { + global $wgUser; + if( wfRunHooks( 'UserLogout', array(&$this) ) ) { + $this->doLogout(); + wfRunHooks( 'UserLogoutComplete', array(&$wgUser) ); + } + } + + /** + * Really logout user + * Clears the cookies and session, resets the instance cache + */ + function doLogout() { global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix; $this->clearInstanceCache( 'defaults' ); @@ -1977,7 +1993,7 @@ class User { $this->load(); if ( wfReadOnly() ) { return; } if ( 0 == $this->mId ) { return; } - + $this->mTouched = self::newTouchedTimestamp(); $dbw = wfGetDB( DB_MASTER ); @@ -2002,11 +2018,11 @@ class User { /** - * Checks if a user with the given name exists, returns the ID + * Checks if a user with the given name exists, returns the ID. */ function idForName() { $s = trim( $this->getName() ); - if ( 0 == strcmp( '', $s ) ) return 0; + if ( $s === '' ) return 0; $dbr = wfGetDB( DB_SLAVE ); $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); @@ -2066,7 +2082,7 @@ class User { } return $newUser; } - + /** * Add an existing user object to the database */ @@ -2252,6 +2268,9 @@ class User { } elseif( $wgAuth->strict() ) { /* Auth plugin doesn't allow local authentication */ return false; + } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) { + /* Auth plugin doesn't allow local authentication for this user name */ + return false; } $ep = $this->encryptPassword( $password ); if ( 0 == strcmp( $ep, $this->mPassword ) ) { @@ -2266,7 +2285,7 @@ class User { } return false; } - + /** * Check if the given clear-text password matches the temporary password * sent by e-mail for password reset operations. @@ -2366,25 +2385,18 @@ class User { * * @param string $subject * @param string $body - * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise. + * @param string $from Optional from address; default $wgPasswordSender will be used otherwise. * @return mixed True on success, a WikiError object on failure. */ - function sendMail( $subject, $body, $from = null ) { + function sendMail( $subject, $body, $from = null, $replyto = null ) { if( is_null( $from ) ) { global $wgPasswordSender; $from = $wgPasswordSender; } - require_once( 'UserMailer.php' ); $to = new MailAddress( $this ); $sender = new MailAddress( $from ); - $error = userMailer( $to, $sender, $subject, $body ); - - if( $error == '' ) { - return true; - } else { - return new WikiError( $error ); - } + return UserMailer::send( $to, $sender, $subject, $body, $replyto ); } /** @@ -2441,7 +2453,9 @@ class User { * @return bool */ function canSendEmail() { - return $this->isEmailConfirmed(); + $canSend = $this->isEmailConfirmed(); + wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); + return $canSend; } /** @@ -2450,7 +2464,7 @@ class User { * @return bool */ function canReceiveEmail() { - return $this->canSendEmail() && !$this->getOption( 'disablemail' ); + return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); } /** @@ -2479,7 +2493,7 @@ class User { return $confirmed; } } - + /** * Return true if there is an outstanding request for e-mail confirmation. * @return bool @@ -2491,7 +2505,7 @@ class User { $this->mEmailToken && $this->mEmailTokenExpires > wfTimestamp(); } - + /** * Get the timestamp of account creation, or false for * non-existent/anonymous user accounts @@ -2573,11 +2587,9 @@ class User { * @return array */ public static function getImplicitGroups() { - static $groups = null; - if( !is_array( $groups ) ) { - $groups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ); - wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); - } + global $wgImplicitGroups; + $groups = $wgImplicitGroups; + wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead return $groups; } @@ -2639,7 +2651,7 @@ class User { return $text; } } - + /** * Increment the user's edit-count field. * Will have no effect for anonymous users. @@ -2651,7 +2663,7 @@ class User { array( 'user_editcount=user_editcount+1' ), array( 'user_id' => $this->getId() ), __METHOD__ ); - + // Lazy initialization check... if( $dbw->affectedRows() == 0 ) { // Pull from a slave to be less cruel to servers @@ -2661,7 +2673,7 @@ class User { 'COUNT(rev_user)', array( 'rev_user' => $this->getId() ), __METHOD__ ); - + // Now here's a goddamn hack... if( $dbr !== $dbw ) { // If we actually have a slave server, the count is @@ -2673,7 +2685,7 @@ class User { // count we just read includes the revision that was // just added in the working transaction. } - + $dbw->update( 'user', array( 'user_editcount' => $count ), array( 'user_id' => $this->getId() ), @@ -2686,3 +2698,4 @@ class User { } + diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 835dd310..d043a6b5 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -1,9 +1,5 @@ <?php /** - * UserMailer.php - * Copyright (C) 2004 Thomas Gries <mail@tgries.de> - * 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 @@ -21,16 +17,10 @@ * * @author <brion@pobox.com> * @author <mail@tgries.de> + * @author Tim Starling * */ -/** - * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name - */ -function wfRFC822Phrase( $phrase ) { - $phrase = strtr( $phrase, array( "\r" => '', "\n" => '', '"' => '' ) ); - return '"' . $phrase . '"'; -} /** * Stores a single person's name and email address. @@ -62,7 +52,7 @@ class MailAddress { # so don't bother generating them if( $this->name != '' && !wfIsWindows() ) { $quoted = wfQuotedPrintable( $this->name ); - if( strpos( $quoted, '.' ) !== false ) { + if( strpos( $quoted, '.' ) !== false || strpos( $quoted, ',' ) !== false ) { $quoted = '"' . $quoted . '"'; } return "$quoted <{$this->address}>"; @@ -70,155 +60,178 @@ class MailAddress { return $this->address; } } -} -function send_mail($mailer, $dest, $headers, $body) -{ - $mailResult =& $mailer->send($dest, $headers, $body); - - # Based on the result return an error string, - if ($mailResult === true) { - return ''; - } elseif (is_object($mailResult)) { - wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" ); - return $mailResult->getMessage(); - } else { - wfDebug( "PEAR::Mail failed, unknown error result\n" ); - return 'Mail object return unknown error.'; + function __toString() { + return $this->toString(); } } + /** - * This function will perform a direct (authenticated) login to - * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an - * array of parameters. It requires PEAR:Mail to do that. - * Otherwise it just uses the standard PHP 'mail' function. - * - * @param $to MailAddress: recipient's email - * @param $from MailAddress: sender's email - * @param $subject String: email's subject. - * @param $body String: email's text. - * @param $replyto String: optional reply-to email (default: null). + * Collection of static functions for sending mail */ -function userMailer( $to, $from, $subject, $body, $replyto=null ) { - global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal; - global $wgEnotifMaxRecips; +class UserMailer { + /** + * Send mail using a PEAR mailer + */ + protected static function sendWithPear($mailer, $dest, $headers, $body) + { + $mailResult = $mailer->send($dest, $headers, $body); + + # Based on the result return an error string, + if( PEAR::isError( $mailResult ) ) { + wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" ); + return new WikiError( $mailResult->getMessage() ); + } else { + return true; + } + } - if (is_array( $wgSMTP )) { - require_once( 'Mail.php' ); + /** + * This function will perform a direct (authenticated) login to + * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an + * array of parameters. It requires PEAR:Mail to do that. + * Otherwise it just uses the standard PHP 'mail' function. + * + * @param $to MailAddress: recipient's email + * @param $from MailAddress: sender's email + * @param $subject String: email's subject. + * @param $body String: email's text. + * @param $replyto String: optional reply-to email (default: null). + * @return mixed True on success, a WikiError object on failure. + */ + static function send( $to, $from, $subject, $body, $replyto=null ) { + global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal; + global $wgEnotifMaxRecips; - $msgid = str_replace(" ", "_", microtime()); - if (function_exists('posix_getpid')) - $msgid .= '.' . posix_getpid(); + if ( is_array( $to ) ) { + wfDebug( __METHOD__.': sending mail to ' . implode( ',', $to ) . "\n" ); + } else { + wfDebug( __METHOD__.': sending mail to ' . implode( ',', array( $to->toString() ) ) . "\n" ); + } - if (is_array($to)) { - $dest = array(); - foreach ($to as $u) - $dest[] = $u->address; - } else - $dest = $to->address; + if (is_array( $wgSMTP )) { + require_once( 'Mail.php' ); - $headers['From'] = $from->toString(); + $msgid = str_replace(" ", "_", microtime()); + if (function_exists('posix_getpid')) + $msgid .= '.' . posix_getpid(); - if ($wgEnotifImpersonal) - $headers['To'] = 'undisclosed-recipients:;'; - else - $headers['To'] = $to->toString(); + if (is_array($to)) { + $dest = array(); + foreach ($to as $u) + $dest[] = $u->address; + } else + $dest = $to->address; - if ( $replyto ) { - $headers['Reply-To'] = $replyto->toString(); - } - $headers['Subject'] = wfQuotedPrintable( $subject ); - $headers['Date'] = date( 'r' ); - $headers['MIME-Version'] = '1.0'; - $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding; - $headers['Content-transfer-encoding'] = '8bit'; - $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME - $headers['X-Mailer'] = 'MediaWiki mailer'; - - // Create the mail object using the Mail::factory method - $mail_object =& Mail::factory('smtp', $wgSMTP); - if( PEAR::isError( $mail_object ) ) { - wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" ); - return $mail_object->getMessage(); - } + $headers['From'] = $from->toString(); + + if ($wgEnotifImpersonal) + $headers['To'] = 'undisclosed-recipients:;'; + else + $headers['To'] = $to->toString(); + + if ( $replyto ) { + $headers['Reply-To'] = $replyto->toString(); + } + $headers['Subject'] = wfQuotedPrintable( $subject ); + $headers['Date'] = date( 'r' ); + $headers['MIME-Version'] = '1.0'; + $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding; + $headers['Content-transfer-encoding'] = '8bit'; + $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME + $headers['X-Mailer'] = 'MediaWiki mailer'; + + // Create the mail object using the Mail::factory method + $mail_object =& Mail::factory('smtp', $wgSMTP); + if( PEAR::isError( $mail_object ) ) { + wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" ); + return new WikiError( $mail_object->getMessage() ); + } - wfDebug( "Sending mail via PEAR::Mail to $dest\n" ); - if (is_array($dest)) { - $chunks = array_chunk($dest, $wgEnotifMaxRecips); + wfDebug( "Sending mail via PEAR::Mail to $dest\n" ); + $chunks = array_chunk( (array)$dest, $wgEnotifMaxRecips ); foreach ($chunks as $chunk) { - $e = send_mail($mail_object, $chunk, $headers, $body); - if ($e != '') + $e = self::sendWithPear($mail_object, $chunk, $headers, $body); + if( WikiError::isError( $e ) ) return $e; } - } else - return $mail_object->send($dest, $headers, $body); - - } else { - # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently - # (fifth parameter of the PHP mail function, see some lines below) - - # Line endings need to be different on Unix and Windows due to - # the bug described at http://trac.wordpress.org/ticket/2603 - if ( wfIsWindows() ) { - $body = str_replace( "\n", "\r\n", $body ); - $endl = "\r\n"; - } else { - $endl = "\n"; - } - $headers = - "MIME-Version: 1.0$endl" . - "Content-type: text/plain; charset={$wgOutputEncoding}$endl" . - "Content-Transfer-Encoding: 8bit$endl" . - "X-Mailer: MediaWiki mailer$endl". - 'From: ' . $from->toString(); - if ($replyto) { - $headers .= "{$endl}Reply-To: " . $replyto->toString(); - } - - $wgErrorString = ''; - set_error_handler( 'mailErrorHandler' ); - wfDebug( "Sending mail via internal mail() function\n" ); + } else { + # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently + # (fifth parameter of the PHP mail function, see some lines below) + + # Line endings need to be different on Unix and Windows due to + # the bug described at http://trac.wordpress.org/ticket/2603 + if ( wfIsWindows() ) { + $body = str_replace( "\n", "\r\n", $body ); + $endl = "\r\n"; + } else { + $endl = "\n"; + } + $headers = + "MIME-Version: 1.0$endl" . + "Content-type: text/plain; charset={$wgOutputEncoding}$endl" . + "Content-Transfer-Encoding: 8bit$endl" . + "X-Mailer: MediaWiki mailer$endl". + 'From: ' . $from->toString(); + if ($replyto) { + $headers .= "{$endl}Reply-To: " . $replyto->toString(); + } - if (function_exists('mail')) - if (is_array($to)) - foreach ($to as $recip) - $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers ); - else - $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers ); - else - $wgErrorString = 'PHP is not configured to send mail'; + $wgErrorString = ''; + $html_errors = ini_get( 'html_errors' ); + ini_set( 'html_errors', '0' ); + set_error_handler( array( 'UserMailer', 'errorHandler' ) ); + wfDebug( "Sending mail via internal mail() function\n" ); + if (function_exists('mail')) { + if (is_array($to)) { + foreach ($to as $recip) { + $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers ); + } + } else { + $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers ); + } + } else { + $wgErrorString = 'PHP is not configured to send mail'; + } - restore_error_handler(); + restore_error_handler(); + ini_set( 'html_errors', $html_errors ); - if ( $wgErrorString ) { - wfDebug( "Error sending mail: $wgErrorString\n" ); - return $wgErrorString; - } elseif (! $sent) { - //mail function only tells if there's an error - wfDebug( "Error sending mail\n" ); - return 'mailer error'; - } else { - return ''; + if ( $wgErrorString ) { + wfDebug( "Error sending mail: $wgErrorString\n" ); + return new WikiError( $wgErrorString ); + } elseif (! $sent) { + //mail function only tells if there's an error + wfDebug( "Error sending mail\n" ); + return new WikiError( 'mailer error' ); + } else { + return true; + } } } -} - + /** + * Get the mail error message in global $wgErrorString + * + * @param $code Integer: error number + * @param $string String: error message + */ + static function errorHandler( $code, $string ) { + global $wgErrorString; + $wgErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); + } -/** - * Get the mail error message in global $wgErrorString - * - * @param $code Integer: error number - * @param $string String: error message - */ -function mailErrorHandler( $code, $string ) { - global $wgErrorString; - $wgErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); + /** + * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name + */ + static function rfc822Phrase( $phrase ) { + $phrase = strtr( $phrase, array( "\r" => '', "\n" => '', '"' => '' ) ); + return '"' . $phrase . '"'; + } } - /** * This module processes the email notifications when the current page is * changed. It looks up the table watchlist to find out which users are watching @@ -245,10 +258,24 @@ class EmailNotification { */ var $to, $subject, $body, $replyto, $from; var $user, $title, $timestamp, $summary, $minorEdit, $oldid; + var $mailTargets = array(); /**@}}*/ - function notifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid = false) { + /** + * Send emails corresponding to the user $editor editing the page $title. + * Also updates wl_notificationtimestamp. + * + * May be deferred via the job queue. + * + * @param $editor User object + * @param $title Title object + * @param $timestamp + * @param $summary + * @param $minorEdit + * @param $oldid (default: false) + */ + function notifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid = false) { global $wgEnotifUseJobQ; if( $title->getNamespace() < 0 ) @@ -269,23 +296,27 @@ class EmailNotification { } - /** - * @todo document + /* + * Immediate version of notifyOnPageChange(). + * + * Send emails corresponding to the user $editor editing the page $title. + * Also updates wl_notificationtimestamp. + * + * @param $editor User object * @param $title Title object * @param $timestamp * @param $summary * @param $minorEdit * @param $oldid (default: false) */ - function actuallyNotifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid=false) { + function actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid=false) { # we use $wgEmergencyContact as sender's address global $wgEnotifWatchlist; global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker; global $wgEnotifImpersonal; - $fname = 'UserMailer::notifyOnPageChange'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # The following code is only run, if several conditions are met: # 1. EmailNotification for pages (other than user_talk pages) must be enabled @@ -295,37 +326,40 @@ class EmailNotification { $enotifusertalkpage = ($isUserTalkPage && $wgEnotifUserTalk); $enotifwatchlistpage = $wgEnotifWatchlist; - $this->title =& $title; + $this->title = $title; $this->timestamp = $timestamp; $this->summary = $summary; $this->minorEdit = $minorEdit; $this->oldid = $oldid; $this->composeCommonMailtext($editor); - $impersonals = array(); + $userTalkId = false; if ( (!$minorEdit || $wgEnotifMinorEdits) ) { - if( $wgEnotifWatchlist ) { - // Send updates to watchers other than the current editor - $userCondition = 'wl_user <> ' . intval( $editor->getId() ); - } elseif( $wgEnotifUserTalk && $title->getNamespace() == NS_USER_TALK ) { + if ( $wgEnotifUserTalk && $isUserTalkPage ) { $targetUser = User::newFromName( $title->getText() ); - if( is_null( $targetUser ) ) { - wfDebug( "$fname: user-talk-only mode; no such user\n" ); - $userCondition = false; - } elseif( $targetUser->getId() == $editor->getId() ) { - wfDebug( "$fname: user-talk-only mode; editor is target user\n" ); - $userCondition = false; + if ( !$targetUser || $targetUser->isAnon() ) { + wfDebug( __METHOD__.": user talk page edited, but user does not exist\n" ); + } elseif ( $targetUser->getId() == $editor->getId() ) { + wfDebug( __METHOD__.": user edited their own talk page, no notification sent\n" ); + } elseif( $targetUser->getOption( 'enotifusertalkpages' ) ) { + wfDebug( __METHOD__.": sending talk page update notification\n" ); + $this->compose( $targetUser ); + $userTalkId = $targetUser->getId(); } else { - // Don't notify anyone other than the owner of the talk page - $userCondition = 'wl_user = ' . intval( $targetUser->getId() ); + wfDebug( __METHOD__.": talk page owner doesn't want notifications\n" ); } - } else { - // Notifications disabled - $userCondition = false; } - if( $userCondition ) { - $dbr = wfGetDB( DB_MASTER ); + + + if ( $wgEnotifWatchlist ) { + // Send updates to watchers other than the current editor + $userCondition = 'wl_user <> ' . intval( $editor->getId() ); + if ( $userTalkId !== false ) { + // Already sent an email to this person + $userCondition .= ' AND wl_user <> ' . intval( $userTalkId ); + } + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'watchlist', array( 'wl_user' ), array( @@ -333,67 +367,44 @@ class EmailNotification { 'wl_namespace' => $title->getNamespace(), $userCondition, 'wl_notificationtimestamp IS NULL', - ), $fname ); - - # if anyone is watching ... set up the email message text which is - # common for all receipients ... - if ( $dbr->numRows( $res ) > 0 ) { - - $watchingUser = new User(); - - # ... now do for all watching users ... if the options fit - for ($i = 1; $i <= $dbr->numRows( $res ); $i++) { - - $wuser = $dbr->fetchObject( $res ); - $watchingUser->setID($wuser->wl_user); - - if ( ( $enotifwatchlistpage && $watchingUser->getOption('enotifwatchlistpages') ) || - ( $enotifusertalkpage - && $watchingUser->getOption('enotifusertalkpages') - && $title->equals( $watchingUser->getTalkPage() ) ) - && (!$minorEdit || ($wgEnotifMinorEdits && $watchingUser->getOption('enotifminoredits') ) ) - && ($watchingUser->isEmailConfirmed() ) ) { - # ... adjust remaining text and page edit time placeholders - # which needs to be personalized for each user - if ($wgEnotifImpersonal) - $impersonals[] = $watchingUser; - else - $this->composeAndSendPersonalisedMail( $watchingUser ); - - } # if the watching user has an email address in the preferences + ), __METHOD__ ); + + foreach ( $res as $row ) { + $watchingUser = User::newFromId( $row->wl_user ); + if ( $watchingUser->getOption( 'enotifwatchlistpages' ) && + ( !$minorEdit || $watchingUser->getOption('enotifminoredits') ) && + $watchingUser->isEmailConfirmed() ) + { + $this->compose( $watchingUser ); } } - } # if anyone is watching - } # if $wgEnotifWatchlist = true + } + } global $wgUsersNotifedOnAllChanges; foreach ( $wgUsersNotifedOnAllChanges as $name ) { $user = User::newFromName( $name ); - if ($wgEnotifImpersonal) - $impersonals[] = $user; - else - $this->composeAndSendPersonalisedMail( $user ); + $this->compose( $user ); } - $this->composeAndSendImpersonalMail($impersonals); + $this->sendMails(); if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) { # mark the changed watch-listed page with a timestamp, so that the page is # listed with an "updated since your last visit" icon in the watch list, ... $dbw = wfGetDB( DB_MASTER ); - $success = $dbw->update( 'watchlist', + $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => $dbw->timestamp($timestamp) ), array( /* WHERE */ 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace(), 'wl_notificationtimestamp IS NULL' - ), 'UserMailer::NotifyOnChange' + ), __METHOD__ ); - # FIXME what do we do on failure ? } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } # function NotifyOnChange /** @@ -495,6 +506,31 @@ class EmailNotification { } /** + * Compose a mail to a given user and either queue it for sending, or send it now, + * depending on settings. + * + * Call sendMails() to send any mails that were queued. + */ + function compose( $user ) { + global $wgEnotifImpersonal; + if ( $wgEnotifImpersonal ) { + $this->mailTargets[] = new MailAddress( $user ); + } else { + $this->sendPersonalised( $user ); + } + } + + /** + * Send any queued mails + */ + function sendMails() { + global $wgEnotifImpersonal; + if ( $wgEnotifImpersonal ) { + $this->sendImpersonal( $this->mailTargets ); + } + } + + /** * Does the per-user customizations to a notification e-mail (name, * timestamp in proper timezone, etc) and sends it out. * Returns true if the mail was sent successfully. @@ -504,7 +540,7 @@ class EmailNotification { * @return bool * @private */ - function composeAndSendPersonalisedMail( $watchingUser ) { + function sendPersonalised( $watchingUser ) { global $wgLang; // From the PHP manual: // Note: The to parameter cannot be an address in the form of "Something <someone@example.com>". @@ -521,23 +557,19 @@ class EmailNotification { $wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ), $body); - return userMailer($to, $this->from, $this->subject, $body, $this->replyto); + return UserMailer::send($to, $this->from, $this->subject, $body, $this->replyto); } /** - * Same as composeAndSendPersonalisedMail but does impersonal mail - * suitable for bulk mailing. Takes an array of users. + * Same as sendPersonalised but does impersonal mail suitable for bulk + * mailing. Takes an array of MailAddress objects. */ - function composeAndSendImpersonalMail($users) { + function sendImpersonal( $addresses ) { global $wgLang; - if (empty($users)) + if (empty($addresses)) return; - $to = array(); - foreach ($users as $user) - $to[] = new MailAddress($user); - $body = str_replace( array( '$WATCHINGUSERNAME', '$PAGEEDITDATE'), @@ -545,8 +577,17 @@ class EmailNotification { $wgLang->timeanddate($this->timestamp, true, false, false)), $this->body); - return userMailer($to, $this->from, $this->subject, $body, $this->replyto); + return UserMailer::send($addresses, $this->from, $this->subject, $body, $this->replyto); } } # end of class EmailNotification +/** + * Backwards compatibility functions + */ +function wfRFC822Phrase( $s ) { + return UserMailer::rfc822Phrase( $s ); +} +function userMailer( $to, $from, $subject, $body, $replyto=null ) { + return UserMailer::send( $to, $from, $subject, $body, $replyto ); +} diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php new file mode 100644 index 00000000..de0e770c --- /dev/null +++ b/includes/UserRightsProxy.php @@ -0,0 +1,161 @@ +<?php + + +/** + * Cut-down copy of User interface for local-interwiki-database + * user rights manipulation. + */ +class UserRightsProxy { + private function __construct( $db, $database, $name, $id ) { + $this->db = $db; + $this->database = $database; + $this->name = $name; + $this->id = intval( $id ); + } + + /** + * Confirm the selected database name is a valid local interwiki database name. + * @return bool + */ + public static function validDatabase( $database ) { + global $wgLocalDatabases; + return in_array( $database, $wgLocalDatabases ); + } + + public static function whoIs( $database, $id ) { + $user = self::newFromId( $database, $id ); + if( $user ) { + return $user->name; + } else { + return false; + } + } + + /** + * Factory function; get a remote user entry by ID number. + * @return UserRightsProxy or null if doesn't exist + */ + public static function newFromId( $database, $id ) { + return self::newFromLookup( $database, 'user_id', intval( $id ) ); + } + + public static function newFromName( $database, $name ) { + return self::newFromLookup( $database, 'user_name', $name ); + } + + private static function newFromLookup( $database, $field, $value ) { + $db = self::getDB( $database ); + if( $db ) { + $row = $db->selectRow( 'user', + array( 'user_id', 'user_name' ), + array( $field => $value ), + __METHOD__ ); + if( $row !== false ) { + return new UserRightsProxy( $db, $database, + $row->user_name, + intval( $row->user_id ) ); + } + } + return null; + } + + /** + * Open a database connection to work on for the requested user. + * This may be a new connection to another database for remote users. + * @param string $database + * @return Database or null if invalid selection + */ + private static function getDB( $database ) { + global $wgLocalDatabases, $wgDBname; + if( self::validDatabase( $database ) ) { + if( $database == $wgDBname ) { + // Hmm... this shouldn't happen though. :) + return wfGetDB( DB_MASTER ); + } else { + global $wgDBuser, $wgDBpassword; + $server = self::getMaster( $database ); + return new Database( $server, $wgDBuser, $wgDBpassword, $database ); + } + } + return null; + } + + /** + * Return the master server to connect to for the requested database. + */ + private static function getMaster( $database ) { + global $wgDBserver, $wgAlternateMaster; + if( isset( $wgAlternateMaster[$database] ) ) { + return $wgAlternateMaster[$database]; + } + return $wgDBserver; + } + + public function getId() { + return $this->id; + } + + public function isAnon() { + return $this->getId() == 0; + } + + public function getName() { + return $this->name . '@' . $this->database; + } + + public function getUserPage() { + return Title::makeTitle( NS_USER, $this->getName() ); + } + + // Replaces getUserGroups() + function getGroups() { + $res = $this->db->select( 'user_groups', + array( 'ug_group' ), + array( 'ug_user' => $this->id ), + __METHOD__ ); + $groups = array(); + while( $row = $this->db->fetchObject( $res ) ) { + $groups[] = $row->ug_group; + } + return $groups; + } + + // replaces addUserGroup + function addGroup( $group ) { + $this->db->insert( 'user_groups', + array( + 'ug_user' => $this->id, + 'ug_group' => $group, + ), + __METHOD__, + array( 'IGNORE' ) ); + } + + // replaces removeUserGroup + function removeGroup( $group ) { + $this->db->delete( 'user_groups', + array( + 'ug_user' => $this->id, + 'ug_group' => $group, + ), + __METHOD__ ); + } + + // replaces touchUser + function invalidateCache() { + $this->db->update( 'user', + array( 'user_touched' => $this->db->timestamp() ), + array( 'user_id' => $this->id ), + __METHOD__ ); + + global $wgMemc; + if ( function_exists( 'wfForeignMemcKey' ) ) { + $key = wfForeignMemcKey( $this->database, false, 'user', 'id', $this->id ); + } else { + $key = "$this->database:user:id:" . $this->id; + } + $wgMemc->delete( $key ); + } +} + +?>
\ No newline at end of file diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php index e03225a3..7e37dca7 100644 --- a/includes/WatchlistEditor.php +++ b/includes/WatchlistEditor.php @@ -32,15 +32,8 @@ class WatchlistEditor { } switch( $mode ) { case self::EDIT_CLEAR: - $output->setPageTitle( wfMsg( 'watchlistedit-clear-title' ) ); - if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) { - $this->clearWatchlist( $user ); - $user->invalidateCache(); - $output->addHtml( wfMsgExt( 'watchlistedit-clear-done', 'parse' ) ); - } else { - $this->showClearForm( $output, $user ); - } - break; + // The "Clear" link scared people too much. + // Pass on to the raw editor, from which it's very easy to clear. case self::EDIT_RAW: $output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) ); if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) { @@ -333,27 +326,6 @@ class WatchlistEditor { } } } - - /** - * Show a confirmation form for users wishing to clear their watchlist - * - * @param OutputPage $output - * @param User $user - */ - private function showClearForm( $output, $user ) { - global $wgUser; - if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) { - $self = SpecialPage::getTitleFor( 'Watchlist' ); - $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $self->getLocalUrl( 'action=clear' ) ) ); - $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) ); - $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-clear-legend' ) . '</legend>'; - $form .= wfMsgExt( 'watchlistedit-clear-confirm', 'parse' ); - $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-clear-submit' ) ) . '</p>'; - $form .= '</fieldset></form>'; - $output->addHtml( $form ); - } - } /** * Show the standard watchlist editing form @@ -481,11 +453,9 @@ class WatchlistEditor { */ public static function buildTools( $skin ) { $tools = array(); - $self = SpecialPage::getTitleFor( 'Watchlist' ); - $modes = array( 'view' => '', 'edit' => 'edit', 'raw' => 'raw', 'clear' => 'clear' ); - foreach( $modes as $mode => $action ) { - $action = $action ? "action={$action}" : ''; - $tools[] = $skin->makeKnownLinkObj( $self, wfMsgHtml( "watchlisttools-{$mode}" ), $action ); + $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' ); + foreach( $modes as $mode => $subpage ) { + $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) ); } return implode( ' | ', $tools ); } diff --git a/includes/WebRequest.php b/includes/WebRequest.php index aa9885f0..944be3c9 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -42,8 +42,17 @@ if ( !function_exists( '__autoload' ) ) { * */ class WebRequest { + var $data = array(); + function __construct() { + /// @fixme This preemptive de-quoting can interfere with other web libraries + /// and increases our memory footprint. It would be cleaner to do on + /// demand; but currently we have no wrapper for $_SERVER etc. $this->checkMagicQuotes(); + + // POST overrides GET data + // We don't use $_REQUEST here to avoid interference from cookies... + $this->data = wfArrayMerge( $_GET, $_POST ); } /** @@ -70,11 +79,22 @@ class WebRequest { if( $a ) { $path = $a['path']; + global $wgScript; + if( $path == $wgScript ) { + // Script inside a rewrite path? + // Abort to keep from breaking... + return; + } + // Raw PATH_INFO style + $matches = $this->extractTitle( $path, "$wgScript/$1" ); + global $wgArticlePath; - $matches = $this->extractTitle( $path, $wgArticlePath ); + if( !$matches && $wgArticlePath ) { + $matches = $this->extractTitle( $path, $wgArticlePath ); + } global $wgActionPaths; - if( !$matches && $wgActionPaths) { + if( !$matches && $wgActionPaths ) { $matches = $this->extractTitle( $path, $wgActionPaths, 'action' ); } @@ -99,7 +119,7 @@ class WebRequest { $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); } foreach( $matches as $key => $val) { - $_GET[$key] = $_REQUEST[$key] = $val; + $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val; } } } @@ -225,7 +245,7 @@ class WebRequest { * @return string */ function getVal( $name, $default = NULL ) { - $val = $this->getGPCVal( $_REQUEST, $name, $default ); + $val = $this->getGPCVal( $this->data, $name, $default ); if( is_array( $val ) ) { $val = $default; } @@ -246,7 +266,7 @@ class WebRequest { * @return array */ function getArray( $name, $default = NULL ) { - $val = $this->getGPCVal( $_REQUEST, $name, $default ); + $val = $this->getGPCVal( $this->data, $name, $default ); if( is_null( $val ) ) { return null; } else { @@ -351,7 +371,7 @@ class WebRequest { function getValues() { $names = func_get_args(); if ( count( $names ) == 0 ) { - $names = array_keys( $_REQUEST ); + $names = array_keys( $this->data ); } $retVal = array(); @@ -576,9 +596,13 @@ class WebRequest { * */ class FauxRequest extends WebRequest { - var $data = null; var $wasPosted = false; + /** + * @param array $data Array of *non*-urlencoded key => value pairs, the + * fake GET/POST values + * @param bool $wasPosted Whether to treat the data as POST + */ function FauxRequest( $data, $wasPosted = false ) { if( is_array( $data ) ) { $this->data = $data; @@ -588,13 +612,9 @@ class FauxRequest extends WebRequest { $this->wasPosted = $wasPosted; } - function getVal( $name, $default = NULL ) { - return $this->getGPCVal( $this->data, $name, $default ); - } - function getText( $name, $default = '' ) { # Override; don't recode since we're using internal data - return $this->getVal( $name, $default ); + return (string)$this->getVal( $name, $default ); } function getValues() { diff --git a/includes/Wiki.php b/includes/Wiki.php index 72a6a61d..e0a57445 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -57,18 +57,10 @@ class MediaWiki { } function checkMaxLag( $maxLag ) { - global $wgLoadBalancer, $wgShowHostnames; + global $wgLoadBalancer; list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); if ( $lag > $maxLag ) { - header( 'HTTP/1.1 503 Service Unavailable' ); - header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); - header( 'X-Database-Lag: ' . intval( $lag ) ); - header( 'Content-Type: text/plain' ); - if( $wgShowHostnames ) { - echo "Waiting for $host: $lag seconds lagged\n"; - } else { - echo "Waiting for a database server: $lag seconds lagged\n"; - } + wfMaxlagError( $host, $lag, $maxLag ); return false; } else { return true; @@ -228,6 +220,10 @@ class MediaWiki { switch( $title->getNamespace() ) { case NS_IMAGE: + $file = wfFindFile( $title ); + if( $file && $file->getRedirected() ) { + return new Article( $title ); + } return new ImagePage( $title ); case NS_CATEGORY: return new CategoryPage( $title ); @@ -252,8 +248,9 @@ class MediaWiki { $article = $this->articleFromTitle( $title ); // Namespace might change when using redirects - if( $action == 'view' && !$request->getVal( 'oldid' ) && - $request->getVal( 'redirect' ) != 'no' ) { + if( ( $action == 'view' || $action == 'render' ) && !$request->getVal( 'oldid' ) && + $request->getVal( 'redirect' ) != 'no' && + !( $wgTitle->getNamespace() == NS_IMAGE && wfFindFile( $wgTitle->getText() ) ) ) { $dbr = wfGetDB(DB_SLAVE); $article->loadPageData($article->pageDataFromTitle($dbr, $title)); @@ -297,7 +294,7 @@ class MediaWiki { $this->doJobs(); $loadBalancer->saveMasterPos(); # Now commit any transactions, so that unreported errors after output() don't roll back the whole thing - $loadBalancer->commitAll(); + $loadBalancer->commitMasterChanges(); $output->output(); wfProfileOut( 'MediaWiki::finalCleanup' ); } @@ -309,6 +306,12 @@ class MediaWiki { */ function doUpdates ( &$updates ) { wfProfileIn( 'MediaWiki::doUpdates' ); + /* No need to get master connections in case of empty updates array */ + if (!$updates) { + wfProfileOut('MediaWiki::doUpdates'); + return; + } + $dbw = wfGetDB( DB_MASTER ); foreach( $updates as $up ) { $up->doUpdate(); @@ -360,7 +363,6 @@ class MediaWiki { */ function restInPeace ( &$loadBalancer ) { wfLogProfilingData(); - $loadBalancer->closeAll(); wfDebug( "Request ended normally\n" ); } @@ -371,6 +373,11 @@ class MediaWiki { wfProfileIn( 'MediaWiki::performAction' ); + if ( !wfRunHooks('MediaWikiPerformAction', array($output, $article, $title, $user, $request)) ) { + wfProfileOut( 'MediaWiki::performAction' ); + return; + } + $action = $this->getVal('Action'); if( in_array( $action, $this->getVal('DisabledActions',array()) ) ) { /* No such action; this will switch to the default case */ diff --git a/includes/WikiError.php b/includes/WikiError.php index efb645bb..b155f9bf 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -101,12 +101,12 @@ class WikiXmlError extends WikiError { /** @return string */ function getMessage() { - return sprintf( '%s at line %d, col %d (byte %d%s): %s', + // '$1 at line $2, col $3 (byte $4): $5', + return wfMsgHtml( 'xml-error-string', $this->mMessage, $this->mLine, $this->mColumn, - $this->mByte, - $this->mContext, + $this->mByte . $this->mContext, xml_error_string( $this->mXmlError ) ); } @@ -120,5 +120,3 @@ class WikiXmlError extends WikiError { } } } - - diff --git a/includes/Xml.php b/includes/Xml.php index fe4bb0cd..6689a4a4 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -104,6 +104,14 @@ class Xml { $namespaces = $wgContLang->getFormattedNamespaces(); $options = array(); + // Godawful hack... we'll be frequently passed selected namespaces + // as strings since PHP is such a shithole. + // But we also don't want blanks and nulls and "all"s matching 0, + // so let's convert *just* string ints to clean ints. + if( preg_match( '/^\d+$/', $selected ) ) { + $selected = intval( $selected ); + } + if( !is_null( $all ) ) $namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces; foreach( $namespaces as $index => $name ) { @@ -301,7 +309,7 @@ class Xml { 'type' => 'hidden', 'value' => $value ) + $attribs ); } - + /** * Convenience function to build an HTML drop-down list item. * @param $text String: text for this item @@ -322,6 +330,63 @@ class Xml { } /** + * Build a drop-down box from a textual list. + * + * @param mixed $name Name and id for the drop-down + * @param mixed $class CSS classes for the drop-down + * @param mixed $other Text for the "Other reasons" option + * @param mixed $list Correctly formatted text to be used to generate the options + * @param mixed $selected Option which should be pre-selected + * @return string + */ + public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = Null ) { + $options = ''; + $optgroup = false; + + $options = self::option( $other, 'other', $selected === 'other' ); + + foreach ( explode( "\n", $list ) as $option) { + $value = trim( $option ); + if ( $value == '' ) { + continue; + } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) { + // A new group is starting ... + $value = trim( substr( $value, 1 ) ); + if( $optgroup ) $options .= self::closeElement('optgroup'); + $options .= self::openElement( 'optgroup', array( 'label' => $value ) ); + $optgroup = true; + } elseif ( substr( $value, 0, 2) == '**' ) { + // groupmember + $value = trim( substr( $value, 2 ) ); + $options .= self::option( $value, $value, $selected === $value ); + } else { + // groupless reason list + if( $optgroup ) $options .= self::closeElement('optgroup'); + $options .= self::option( $value, $value, $selected === $value ); + $optgroup = false; + } + } + if( $optgroup ) $options .= self::closeElement('optgroup'); + + $attribs = array(); + if( $name ) { + $attribs['id'] = $name; + $attribs['name'] = $name; + } + if( $class ) { + $attribs['class'] = $class; + } + if( $tabindex ) { + $attribs['tabindex'] = $tabindex; + } + return Xml::openElement( 'select', $attribs ) + . "\n" + . $options + . "\n" + . Xml::closeElement( 'select' ); + } + + /** * Returns an escaped string suitable for inclusion in a string literal * for JavaScript source code. * Illegal control characters are assumed not to be present. diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php new file mode 100644 index 00000000..639d1f85 --- /dev/null +++ b/includes/XmlTypeCheck.php @@ -0,0 +1,93 @@ +<?php + +class XmlTypeCheck { + /** + * Will be set to true or false to indicate whether the file is + * well-formed XML. Note that this doesn't check schema validity. + */ + public $wellFormed = false; + + /** + * Name of the document's root element, including any namespace + * as an expanded URL. + */ + public $rootElement = ''; + + private $softNamespaces; + private $namespaces = array(); + + /** + * @param $file string filename + * @param $softNamespaces bool + * If set to true, use of undeclared XML namespaces will be ignored. + * This matches the behavior of rsvg, but more compliant consumers + * such as Firefox will reject such files. + * Leave off for the default, stricter checks. + */ + function __construct( $file, $softNamespaces=false ) { + $this->softNamespaces = $softNamespaces; + $this->run( $file ); + } + + private function run( $fname ) { + if( $this->softNamespaces ) { + $parser = xml_parser_create( 'UTF-8' ); + } else { + $parser = xml_parser_create_ns( 'UTF-8' ); + } + + // case folding violates XML standard, turn it off + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + xml_set_element_handler( $parser, array( $this, 'elementOpen' ), false ); + + $file = fopen( $fname, "rb" ); + do { + $chunk = fread( $file, 32768 ); + $ret = xml_parse( $parser, $chunk, feof( $file ) ); + if( $ret == 0 ) { + // XML isn't well-formed! + fclose( $file ); + xml_parser_free( $parser ); + return; + } + } while( !feof( $file ) ); + + $this->wellFormed = true; + + fclose( $file ); + xml_parser_free( $parser ); + } + + private function elementOpen( $parser, $name, $attribs ) { + if( $this->softNamespaces ) { + // Check namespaces manually, so expat doesn't throw + // errors on use of undeclared namespaces. + foreach( $attribs as $attrib => $val ) { + if( $attrib == 'xmlns' ) { + $this->namespaces[''] = $val; + } elseif( substr( $attrib, 0, strlen( 'xmlns:' ) ) == 'xmlns:' ) { + $this->namespaces[substr( $attrib, strlen( 'xmlns:' ) )] = $val; + } + } + + if( strpos( $name, ':' ) === false ) { + $ns = ''; + $subname = $name; + } else { + list( $ns, $subname ) = explode( ':', $name, 2 ); + } + + if( isset( $this->namespaces[$ns] ) ) { + $name = $this->namespaces[$ns] . ':' . $subname; + } else { + // Technically this is invalid for XML with Namespaces. + // But..... we'll just let it slide in soft mode. + } + } + + // We only need the first open element + $this->rootElement = $name; + xml_set_element_handler( $parser, false, false ); + } +} diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 1b9d884b..62bebb4e 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -1,13 +1,12 @@ <?php /** - * Simplified/Traditional Chinese conversion tables + * Simplified / Traditional Chinese conversion tables * * Automatically generated using code and data in includes/zhtable/ * Do not modify directly! - * -*/ + */ -$zh2TW=array( +$zh2Hant = array( "画"=>"畫", "板"=>"板", "表"=>"表", @@ -185,8 +184,10 @@ $zh2TW=array( "鳄"=>"鱷", "鸡"=>"雞", "鹚"=>"鶿", -"䌶"=>"䊷", -"䜥"=>"𧩙", +"荡"=>"盪", +"锤"=>"錘", +"㟆"=>"㠏", +"㛟"=>"𡞵", "专"=>"專", "业"=>"業", "丛"=>"叢", @@ -263,6 +264,7 @@ $zh2TW=array( "傧"=>"儐", "储"=>"儲", "傩"=>"儺", +"㑩"=>"儸", "兑"=>"兌", "兖"=>"兗", "兰"=>"蘭", @@ -309,6 +311,8 @@ $zh2TW=array( "剑"=>"劍", "剥"=>"剝", "剧"=>"劇", +"㓥"=>"劏", +"㔉"=>"劚", "劝"=>"勸", "办"=>"辦", "务"=>"務", @@ -409,6 +413,7 @@ $zh2TW=array( "嘘"=>"噓", "嘤"=>"嚶", "嘱"=>"囑", +"㖊"=>"噚", "噜"=>"嚕", "嚣"=>"囂", "园"=>"園", @@ -558,7 +563,6 @@ $zh2TW=array( "帻"=>"幘", "帼"=>"幗", "幂"=>"冪", -"幺"=>"么", "庄"=>"莊", "庆"=>"慶", "庐"=>"廬", @@ -824,6 +828,7 @@ $zh2TW=array( "殚"=>"殫", "殡"=>"殯", "㱮"=>"殨", +"㱩"=>"殰", "殴"=>"毆", "毁"=>"毀", "毂"=>"轂", @@ -926,6 +931,7 @@ $zh2TW=array( "澜"=>"瀾", "濑"=>"瀨", "濒"=>"瀕", +"㲿"=>"瀇", "灏"=>"灝", "灭"=>"滅", "灯"=>"燈", @@ -956,7 +962,9 @@ $zh2TW=array( "焕"=>"煥", "焖"=>"燜", "焘"=>"燾", +"㶽"=>"煱", "煴"=>"熅", +"㶶"=>"燶", "爱"=>"愛", "爷"=>"爺", "牍"=>"牘", @@ -987,6 +995,7 @@ $zh2TW=array( "猬"=>"蝟", "献"=>"獻", "獭"=>"獺", +"㺍"=>"獱", "玑"=>"璣", "玚"=>"瑒", "玛"=>"瑪", @@ -1101,6 +1110,7 @@ $zh2TW=array( "秾"=>"穠", "稆"=>"穭", "税"=>"稅", +"䅉"=>"稏", "稣"=>"穌", "稳"=>"穩", "穑"=>"穡", @@ -1157,8 +1167,11 @@ $zh2TW=array( "䌷"=>"紬", "䌹"=>"絅", "絷"=>"縶", +"䌼"=>"綐", +"䌽"=>"綵", "䌸"=>"縳", "䍁"=>"繸", +"䍀"=>"繿", "纟"=>"糹", "纠"=>"糾", "纡"=>"紆", @@ -1387,7 +1400,6 @@ $zh2TW=array( "荞"=>"蕎", "荟"=>"薈", "荠"=>"薺", -"荡"=>"蕩", "荣"=>"榮", "荤"=>"葷", "荥"=>"滎", @@ -1434,6 +1446,7 @@ $zh2TW=array( "蕰"=>"薀", "蕲"=>"蘄", "薮"=>"藪", +"䓕"=>"薳", "藓"=>"蘚", "蘖"=>"櫱", "虏"=>"虜", @@ -1467,6 +1480,7 @@ $zh2TW=array( "蝾"=>"蠑", "螀"=>"螿", "螨"=>"蟎", +"䗖"=>"螮", "蟏"=>"蠨", "衅"=>"釁", "衔"=>"銜", @@ -1488,6 +1502,7 @@ $zh2TW=array( "裥"=>"襇", "褛"=>"褸", "褴"=>"襤", +"䙓"=>"襬", "见"=>"見", "观"=>"觀", "觃"=>"覎", @@ -1512,6 +1527,7 @@ $zh2TW=array( "䜣"=>"訢", "誉"=>"譽", "誊"=>"謄", +"䜧"=>"譅", "讠"=>"訁", "计"=>"計", "订"=>"訂", @@ -1660,6 +1676,8 @@ $zh2TW=array( "谵"=>"譫", "谶"=>"讖", "豮"=>"豶", +"䝙"=>"貙", +"䞐"=>"賰", "贝"=>"貝", "贞"=>"貞", "负"=>"負", @@ -1859,6 +1877,7 @@ $zh2TW=array( "鉴"=>"鑒", "銮"=>"鑾", "錾"=>"鏨", +"𨱏"=>"鎝", "钅"=>"釒", "钆"=>"釓", "钇"=>"釔", @@ -2013,7 +2032,6 @@ $zh2TW=array( "锡"=>"錫", "锢"=>"錮", "锣"=>"鑼", -"锤"=>"錘", "锥"=>"錐", "锦"=>"錦", "锧"=>"鑕", @@ -2174,6 +2192,7 @@ $zh2TW=array( "靓"=>"靚", "静"=>"靜", "靥"=>"靨", +"䩄"=>"靦", "鞑"=>"韃", "鞒"=>"鞽", "鞯"=>"韉", @@ -2298,6 +2317,7 @@ $zh2TW=array( "馓"=>"饊", "馔"=>"饌", "馕"=>"饢", +"䯄"=>"騧", "马"=>"馬", "驭"=>"馭", "驮"=>"馱", @@ -2471,6 +2491,7 @@ $zh2TW=array( "䴗"=>"鶪", "䴘"=>"鷈", "䴙"=>"鷿", +"㶉"=>"鸂", "鸟"=>"鳥", "鸠"=>"鳩", "鸢"=>"鳶", @@ -2557,7 +2578,6 @@ $zh2TW=array( "鹾"=>"鹺", "麦"=>"麥", "麸"=>"麩", -"麽"=>"麼", "黄"=>"黃", "黉"=>"黌", "黡"=>"黶", @@ -2590,7 +2610,6 @@ $zh2TW=array( "龟"=>"龜", "BIG-" => "BIG-", -".PRG" => ".PRG", "一伙" => "一伙", "一并" => "一併", "一准" => "一准", @@ -2615,51 +2634,47 @@ $zh2TW=array( "下面" => "下麵", "不准" => "不准", "不吊" => "不吊", -"不干" => "不幹", -"不舍" => "不捨", +"不知就里" => "不知就裡", "不知所云" => "不知所云", -"不识台举" => "不識檯舉", "不锈钢" => "不鏽鋼", "丑剧" => "丑劇", "丑旦" => "丑旦", "丑角" => "丑角", -"世界杯" => "世界盃", "并存着" => "並存著", "中岳" => "中嶽", -"中台路" => "中臺路", "中台医专" => "中臺醫專", "丰南" => "丰南", "丰台" => "丰台", "丰姿" => "丰姿", -"丰神俊朗" => "丰神俊朗", "丰采" => "丰采", "丰韵" => "丰韻", "主干" => "主幹", +"么么唱唱" => "么么唱唱", +"么儿" => "么兒", +"么喝" => "么喝", +"么妹" => "么妹", +"么弟" => "么弟", +"么爷" => "么爺", "九世之雠" => "九世之讎", "九只" => "九隻", "干丝" => "乾絲", "干着急" => "乾著急", -"干面" => "乾麵", "乱发" => "亂髮", "云云" => "云云", -"云何" => "云何", "云尔" => "云爾", "五岳" => "五嶽", "五斗柜" => "五斗櫃", "五斗橱" => "五斗櫥", -"五斗米" => "五斗米", "五谷" => "五穀", "五行生克" => "五行生剋", "五只" => "五隻", "五出" => "五齣", -"井里" => "井裡", "交卷" => "交卷", "人云亦云" => "人云亦云", "人物志" => "人物誌", "什锦面" => "什錦麵", "什么" => "什麼", "仆倒" => "仆倒", -"仇雠" => "仇讎", "介系词" => "介係詞", "介系词" => "介繫詞", "仿制" => "仿製", @@ -2677,7 +2692,6 @@ $zh2TW=array( "布岗" => "佈崗", "布施" => "佈施", "布景" => "佈景", -"布有" => "佈有", "布满" => "佈滿", "布线" => "佈線", "布置" => "佈置", @@ -2710,6 +2724,7 @@ $zh2TW=array( "并进" => "併進", "来复" => "來複", "供制" => "供製", +"依依不舍" => "依依不捨", "侵并" => "侵併", "便辟" => "便辟", "系数" => "係數", @@ -2720,9 +2735,7 @@ $zh2TW=array( "修胡刀" => "修鬍刀", "俯冲" => "俯衝", "个里" => "個裡", -"倒绷孩儿" => "倒繃孩兒", "借着" => "借著", -"偃仆" => "偃仆", "假发" => "假髮", "停制" => "停製", "偷鸡不着" => "偷雞不著", @@ -2732,6 +2745,7 @@ $zh2TW=array( "传布" => "傳佈", "债台高筑" => "債臺高築", "傻里傻气" => "傻裡傻氣", +"倾家荡产" => "傾家蕩產", "倾复" => "傾複", "倾复" => "傾覆", "僱佣" => "僱佣", @@ -2741,7 +2755,6 @@ $zh2TW=array( "尽先" => "儘先", "尽其所有" => "儘其所有", "尽力" => "儘力", -"尽可能" => "儘可能", "尽快" => "儘快", "尽早" => "儘早", "尽是" => "儘是", @@ -2763,29 +2776,21 @@ $zh2TW=array( "公干" => "公幹", "公斗" => "公斗", "公历" => "公曆", -"公里" => "公裡", -"六谷" => "六穀", "六只" => "六隻", "六出" => "六齣", "兼并" => "兼併", -"册卷" => "冊卷", "冤雠" => "冤讎", "准予" => "准予", "准假" => "准假", -"准定" => "准定", "准将" => "准將", -"准尉" => "准尉", -"准此" => "准此", "准考证" => "准考證", "准许" => "准許", "几几" => "几几", -"几杖" => "几杖", "几案" => "几案", -"几筵" => "几筵", "几丝" => "几絲", "凹洞里" => "凹洞裡", "出征" => "出征", -"函复" => "函覆", +"出锤" => "出鎚", "刀削面" => "刀削麵", "刁斗" => "刁斗", "分布" => "分佈", @@ -2826,39 +2831,30 @@ $zh2TW=array( "剃须" => "剃鬚", "削发" => "削髮", "克制" => "剋制", -"克扣" => "剋扣", -"克日" => "剋日", "克星" => "剋星", "克服" => "剋服", -"克期" => "剋期", "克死" => "剋死", "克薄" => "剋薄", -"前仆后仰" => "前仆後仰", "前仆后继" => "前仆後繼", "前台" => "前臺", "前车之复" => "前車之覆", "刚才" => "剛纔", -"剥制" => "剝製", "剪发" => "剪髮", "割舍" => "割捨", -"创获" => "創穫", "创制" => "創製", "加里宁" => "加裡寧", +"动荡" => "動蕩", "劳力士表" => "勞力士錶", "包准" => "包准", "包谷" => "包穀", -"匏系" => "匏繫", -"北岳" => "北嶽", "北斗" => "北斗", "北回" => "北迴", "匡复" => "匡複", "匪干" => "匪幹", "十卷" => "十卷", -"十干" => "十干", "十台" => "十臺", "十只" => "十隻", "十出" => "十齣", -"千百只" => "千百隻", "千丝万缕" => "千絲萬縷", "千回百折" => "千迴百折", "千回百转" => "千迴百轉", @@ -2868,7 +2864,6 @@ $zh2TW=array( "半只" => "半隻", "南岳" => "南嶽", "南征" => "南征", -"南斗" => "南斗", "南台" => "南臺", "南回" => "南迴", "卡里" => "卡裡", @@ -2888,14 +2883,12 @@ $zh2TW=array( "卷筒" => "卷筒", "卷纬" => "卷緯", "卷绕" => "卷繞", -"卷舌" => "卷舌", "卷装" => "卷裝", "卷轴" => "卷軸", "卷云" => "卷雲", "卷领" => "卷領", "卷发" => "卷髮", "卷须" => "卷鬚", -"厚朴" => "厚朴", "参与" => "參与", "参与者" => "參与者", "参合" => "參合", @@ -2913,7 +2906,6 @@ $zh2TW=array( "反复" => "反覆", "取舍" => "取捨", "口里" => "口裡", -"古柯咸" => "古柯鹹", "只准" => "只准", "只冲" => "只衝", "叮当" => "叮噹", @@ -2970,23 +2962,18 @@ $zh2TW=array( "吊钟" => "吊鐘", "同伙" => "同伙", "名表" => "名錶", -"後冠" => "后冠", -"後北街" => "后北街", -"後土" => "后土", -"後妃" => "后妃", -"後安路" => "后安路", -"後平路" => "后平路", -"後座" => "后座", -"後稷" => "后稷", -"後羿" => "后羿", -"後街" => "后街", -"後里" => "后里", +"后冠" => "后冠", +"后土" => "后土", +"后妃" => "后妃", +"后座" => "后座", +"后稷" => "后稷", +"后羿" => "后羿", +"后里" => "后里", "向着" => "向著", "吞并" => "吞併", "吹发" => "吹髮", -"吕後" => "呂后", -"呆里呆气" => "呆裡呆氣", -"呈准" => "呈准", +"吕后" => "呂后", +"獃里獃气" => "呆裡呆氣", "周而复始" => "周而複始", "呼吁" => "呼籲", "和面" => "和麵", @@ -2994,10 +2981,8 @@ $zh2TW=array( "哭脏" => "哭髒", "问卷" => "問卷", "喝采" => "喝采", -"乔岳" => "喬嶽", "单干" => "單干", "单只" => "單隻", -"嘴里" => "嘴裏", "嘴里" => "嘴裡", "恶心" => "噁心", "当啷" => "噹啷", @@ -3010,13 +2995,12 @@ $zh2TW=array( "向迩" => "嚮邇", "严丝合缝" => "嚴絲合縫", "严复" => "嚴複", -"囉苏" => "囉囌", "四舍五入" => "四捨五入", "四只" => "四隻", "四出" => "四齣", -"回历新年" => "回曆新年", "回丝" => "回絲", "回着" => "回著", +"回荡" => "回蕩", "回复" => "回覆", "回采" => "回采", "圈子里" => "圈子裡", @@ -3024,17 +3008,15 @@ $zh2TW=array( "国历" => "國曆", "国雠" => "國讎", "园里" => "園裡", -"圆台" => "圓臺", "图里" => "圖裡", "土里" => "土裡", "土制" => "土製", "地志" => "地誌", "坍台" => "坍臺", "坑里" => "坑裡", +"坦荡" => "坦蕩", "垂发" => "垂髮", "垮台" => "垮臺", -"埃及豔後" => "埃及豔后", -"埃荣冲" => "埃榮衝", "埋布" => "埋佈", "城里" => "城裡", "基干" => "基幹", @@ -3046,7 +3028,6 @@ $zh2TW=array( "墨斗" => "墨斗", "墨索里尼" => "墨索裡尼", "垦复" => "墾複", -"压卷" => "壓卷", "垄断价格" => "壟斷價格", "垄断资产" => "壟斷資產", "垄断集团" => "壟斷集團", @@ -3065,20 +3046,19 @@ $zh2TW=array( "大卷" => "大卷", "大干" => "大干", "大干" => "大幹", -"大辟" => "大辟", +"大锤" => "大鎚", "大只" => "大隻", -"天後" => "天后", +"天后" => "天后", "天干" => "天干", "天文台" => "天文臺", "天翻地复" => "天翻地覆", -"太後" => "太后", +"太后" => "太后", "奏折" => "奏摺", "女丑" => "女丑", "女佣" => "女佣", "好家夥" => "好傢夥", "好戏连台" => "好戲連臺", -"好困" => "好睏", -"如饥似渴" => "如饑似渴", +"如法泡制" => "如法泡製", "妆台" => "妝臺", "姜太公" => "姜太公", "姜子牙" => "姜子牙", @@ -3088,26 +3068,18 @@ $zh2TW=array( "存折" => "存摺", "孟姜女" => "孟姜女", "宇宙志" => "宇宙誌", -"宋皇台道" => "宋皇臺道", "定准" => "定准", "定制" => "定製", "宣布" => "宣佈", "宫里" => "宮裡", "家伙" => "家伙", -"家里" => "家裏", "家里" => "家裡", "密布" => "密佈", -"密致" => "密緻", "寇雠" => "寇讎", -"富台街" => "富臺街", -"寓禁于征" => "寓禁於征", "实干" => "實幹", "写字台" => "寫字檯", "写字台" => "寫字臺", "宽松" => "寬鬆", -"宝卷" => "寶卷", -"宝里宝气" => "寶裡寶氣", -"封後" => "封后", "封面里" => "封面裡", "射干" => "射干", "对表" => "對錶", @@ -3115,7 +3087,6 @@ $zh2TW=array( "小伙" => "小伙", "小只" => "小隻", "少吊" => "少吊", -"就里" => "就裡", "尺布斗粟" => "尺布斗粟", "尼克松" => "尼克鬆", "尼采" => "尼采", @@ -3125,17 +3096,11 @@ $zh2TW=array( "屋子里" => "屋子裡", "屋里" => "屋裡", "展布" => "展佈", -"展卷" => "展卷", "屡仆屡起" => "屢仆屢起", "屯里" => "屯裡", "山岳" => "山嶽", -"山斗" => "山斗", "山里" => "山裡", -"山重水复" => "山重水複", -"岱岳" => "岱嶽", "峰回" => "峰迴", -"岳岳" => "嶽嶽", -"巅复" => "巔覆", "巡回" => "巡迴", "巧干" => "巧幹", "巴尔干" => "巴爾幹", @@ -3149,55 +3114,37 @@ $zh2TW=array( "席卷" => "席卷", "带团参加" => "帶團參加", "带发修行" => "帶髮修行", -"干世" => "干世", "干休" => "干休", "干系" => "干係", -"干冒" => "干冒", "干卿何事" => "干卿何事", -"干卿底事" => "干卿底事", -"干城" => "干城", "干将" => "干將", -"干德道" => "干德道", "干戈" => "干戈", "干挠" => "干撓", "干扰" => "干擾", "干支" => "干支", "干政" => "干政", "干时" => "干時", -"干没" => "干沒", "干涉" => "干涉", "干犯" => "干犯", -"干禄" => "干祿", "干与" => "干與", "干着急" => "干著急", -"干诺道中" => "干諾道中", -"干诺道西" => "干諾道西", -"干谒" => "干謁", -"干证" => "干證", -"干誉" => "干譽", "干贝" => "干貝", -"干连" => "干連", -"干云蔽日" => "干雲蔽日", "干预" => "干預", "平台" => "平臺", "年历" => "年曆", "年里" => "年裡", "干上" => "幹上", "干下去" => "幹下去", -"干不了" => "幹不了", -"干不成" => "幹不成", "干了" => "幹了", "干事" => "幹事", "干些" => "幹些", "干个" => "幹個", "干劲" => "幹勁", "干员" => "幹員", -"干啥" => "幹啥", "干吗" => "幹嗎", "干嘛" => "幹嘛", "干坏事" => "幹壞事", "干完" => "幹完", -"干将" => "幹將", "干得" => "幹得", "干性油" => "幹性油", "干才" => "幹才", @@ -3206,15 +3153,11 @@ $zh2TW=array( "干活" => "幹活", "干流" => "幹流", "干球温度" => "幹球溫度", -"干略" => "幹略", "干线" => "幹線", "干练" => "幹練", "干警" => "幹警", "干起来" => "幹起來", "干路" => "幹路", -"干办" => "幹辦", -"干这一行" => "幹這一行", -"干这种事" => "幹這種事", "干道" => "幹道", "干部" => "幹部", "干么" => "幹麼", @@ -3222,43 +3165,34 @@ $zh2TW=array( "几只" => "幾隻", "几出" => "幾齣", "底里" => "底裡", -"店里" => "店裡", "康采恩" => "康采恩", "庙里" => "廟裡", "建台" => "建臺", "弄脏" => "弄髒", "弔卷" => "弔卷", "弘历" => "弘曆", -"强干弱枝" => "強幹弱枝", "别扭" => "彆扭", "别拗" => "彆拗", "别气" => "彆氣", "别脚" => "彆腳", "别着" => "彆著", "弹子台" => "彈子檯", -"弹珠台" => "彈珠檯", "弹药" => "彈葯", -"汇刊" => "彙刊", "汇报" => "彙報", "汇整" => "彙整", -"汇算" => "彙算", "汇编" => "彙編", "汇总" => "彙總", "汇纂" => "彙纂", "汇辑" => "彙輯", "汇集" => "彙集", "形单影只" => "形單影隻", -"影後" => "影后", +"影后" => "影后", "往里" => "往裡", "往复" => "往複", "征伐" => "征伐", "征兵" => "征兵", -"征利" => "征利", "征尘" => "征塵", "征夫" => "征夫", -"征属" => "征屬", -"征帆" => "征帆", -"征戌" => "征戌", "征战" => "征戰", "征收" => "征收", "征服" => "征服", @@ -3274,18 +3208,18 @@ $zh2TW=array( "复辟" => "復辟", "德干高原" => "德干高原", "心愿" => "心愿", -"心里" => "心裏", +"心荡神驰" => "心蕩神馳", "心里" => "心裡", "忙里" => "忙裡", "快干" => "快幹", "快冲" => "快衝", "怎么" => "怎麼", "怎么着" => "怎麼著", +"怒发冲冠" => "怒髮衝冠", "急冲而下" => "急衝而下", "怪里怪气" => "怪裡怪氣", "恩准" => "恩准", "情有所钟" => "情有所鍾", -"情有独钟" => "情有獨鍾", "意面" => "意麵", "慌里慌张" => "慌裡慌張", "慰借" => "慰藉", @@ -3297,7 +3231,7 @@ $zh2TW=array( "怀里" => "懷裡", "怀表" => "懷錶", "悬吊" => "懸吊", -"悬心吊胆" => "懸心吊膽", +"恋恋不舍" => "戀戀不捨", "戏台" => "戲臺", "戴表" => "戴錶", "戽斗" => "戽斗", @@ -3305,7 +3239,6 @@ $zh2TW=array( "手不释卷" => "手不釋卷", "手卷" => "手卷", "手折" => "手摺", -"手里" => "手裏", "手里" => "手裡", "手表" => "手錶", "手松" => "手鬆", @@ -3331,6 +3264,7 @@ $zh2TW=array( "拖吊" => "拖吊", "拗别" => "拗彆", "拮据" => "拮据", +"振荡" => "振蕩", "捍御" => "捍禦", "舍不得" => "捨不得", "舍出" => "捨出", @@ -3353,18 +3287,18 @@ $zh2TW=array( "舍近求远" => "捨近求遠", "捲发" => "捲髮", "捵面" => "捵麵", +"扫荡" => "掃蕩", "掌柜" => "掌柜", "排骨面" => "排骨麵", "挂帘" => "掛帘", "挂面" => "掛麵", "接着说" => "接著說", -"掩卷" => "掩卷", "提心吊胆" => "提心吊膽", "插图卷" => "插圖卷", "换吊" => "換吊", "换只" => "換隻", "换发" => "換髮", -"握发" => "握髮", +"摇荡" => "搖蕩", "搭伙" => "搭伙", "折合" => "摺合", "折奏" => "摺奏", @@ -3388,50 +3322,41 @@ $zh2TW=array( "担负着" => "擔負著", "据云" => "據云", "擢发难数" => "擢髮難數", -"拟准" => "擬准", "摆布" => "擺佈", "摄制" => "攝製", "支干" => "支幹", "收获" => "收穫", "改制" => "改製", "攻克" => "攻剋", +"放荡" => "放蕩", "放松" => "放鬆", -"故布疑阵" => "故佈疑陣", "叙说着" => "敘說著", "散伙" => "散伙", "散布" => "散佈", +"散荡" => "散蕩", "散发" => "散髮", "整只" => "整隻", "整出" => "整齣", -"敌忾同雠" => "敵愾同讎", -"文借" => "文藉", "文采" => "文采", -"斗亚兰路" => "斗亞蘭路", "斗六" => "斗六", "斗南" => "斗南", "斗大" => "斗大", "斗子" => "斗子", "斗室" => "斗室", -"斗宿" => "斗宿", "斗方" => "斗方", "斗栱" => "斗栱", "斗笠" => "斗笠", -"斗筲" => "斗筲", "斗箕" => "斗箕", "斗篷" => "斗篷", "斗胆" => "斗膽", -"斗蓬" => "斗蓬", "斗转参横" => "斗轉參橫", "斗量" => "斗量", "斗门" => "斗門", "料斗" => "料斗", -"斤斗" => "斤斗", "斯里兰卡" => "斯裡蘭卡", "新历" => "新曆", "断头台" => "斷頭臺", -"断发文身" => "斷髮文身", "方才" => "方纔", -"方志" => "方誌", "施舍" => "施捨", "旋绕着" => "旋繞著", "旋回" => "旋迴", @@ -3450,15 +3375,14 @@ $zh2TW=array( "星辰表" => "星辰錶", "春假里" => "春假裡", "春天里" => "春天裡", +"晃荡" => "晃蕩", "景致" => "景緻", "暗地里" => "暗地裡", "暗沟里" => "暗溝裡", "暗里" => "暗裡", -"暴敛横征" => "暴斂橫征", "历数" => "曆數", "历书" => "曆書", "历法" => "曆法", -"历象" => "曆象", "书卷" => "書卷", "会干" => "會幹", "会里" => "會裡", @@ -3469,22 +3393,16 @@ $zh2TW=array( "本台" => "本臺", "朴子" => "朴子", "朴实" => "朴實", -"朴忠" => "朴忠", -"朴直" => "朴直", "朴硝" => "朴硝", "朴素" => "朴素", -"朴茂" => "朴茂", "朴资茅斯" => "朴資茅斯", -"朴钝" => "朴鈍", -"材干" => "材幹", "村里" => "村裡", -"杜老志道" => "杜老誌道", "束发" => "束髮", -"杯面" => "杯麵", "东岳" => "東嶽", "东征" => "東征", "松赞干布" => "松贊干布", "板着脸" => "板著臉", +"板荡" => "板蕩", "枕借" => "枕藉", "林宏岳" => "林宏嶽", "枝干" => "枝幹", @@ -3494,18 +3412,14 @@ $zh2TW=array( "柜上" => "柜上", "柜台" => "柜台", "柜子" => "柜子", -"柜柳" => "柜柳", "查卷" => "查卷", "查号台" => "查號臺", "校雠学" => "校讎學", "核准" => "核准", "核复" => "核覆", "格里" => "格裡", -"案准" => "案准", "案卷" => "案卷", "条干" => "條幹", -"梯冲" => "梯衝", -"械系" => "械繫", "棉卷" => "棉卷", "棉制" => "棉製", "植发" => "植髮", @@ -3528,12 +3442,10 @@ $zh2TW=array( "柜台" => "櫃臺", "栏干" => "欄干", "欺蒙" => "欺矇", -"歌後" => "歌后", -"歌台舞榭" => "歌臺舞榭", +"歌后" => "歌后", "欧几里得" => "歐幾裡得", "正当着" => "正當著", -"此仆彼起" => "此仆彼起", -"武後" => "武后", +"武后" => "武后", "武松" => "武鬆", "归并" => "歸併", "死里求生" => "死裡求生", @@ -3541,23 +3453,20 @@ $zh2TW=array( "残卷" => "殘卷", "杀虫药" => "殺虫藥", "壳里" => "殼裡", -"母後" => "母后", +"母后" => "母后", "每只" => "每隻", "比干" => "比干", "毛卷" => "毛卷", -"毛坏" => "毛坏", "毛发" => "毛髮", "毫发" => "毫髮", -"气冲斗牛" => "氣沖斗牛", "气冲牛斗" => "氣沖牛斗", "气象台" => "氣象臺", +"氯霉素" => "氯黴素", "水斗" => "水斗", "水里" => "水裡", "水表" => "水錶", "永历" => "永曆", -"永志不忘" => "永誌不忘", "污蔑" => "汙衊", -"江干" => "江干", "池里" => "池裡", "污蔑" => "污衊", "沈着" => "沈著", @@ -3565,47 +3474,47 @@ $zh2TW=array( "没精打采" => "沒精打采", "冲着" => "沖著", "沙里淘金" => "沙裡淘金", -"河岳" => "河嶽", "河里" => "河裡", "油面" => "油麵", -"泡制" => "泡製", "泡面" => "泡麵", "泰斗" => "泰斗", -"洗发" => "洗髮", +"洗手不干" => "洗手不幹", +"洗发精" => "洗髮精", "派团参加" => "派團參加", +"流荡" => "流蕩", +"浩荡" => "浩蕩", "浪琴表" => "浪琴錶", -"浮吊" => "浮吊", +"浪荡" => "浪蕩", +"浮荡" => "浮蕩", "海里" => "海裡", "涂着" => "涂著", "液晶表" => "液晶錶", "凉面" => "涼麵", "淡朱" => "淡硃", -"渊淳岳峙" => "淵淳嶽峙", -"渠冲" => "渠衝", +"淫荡" => "淫蕩", "测验卷" => "測驗卷", "港制" => "港製", +"游荡" => "游蕩", "凑合着" => "湊合著", "湖里" => "湖裡", "汤团" => "湯糰", "汤面" => "湯麵", -"温郁" => "溫郁", "卤制" => "滷製", "卤面" => "滷麵", "满布" => "滿佈", +"漂荡" => "漂蕩", "漏斗" => "漏斗", "演奏台" => "演奏臺", -"潜意识里" => "潛意識裡", "潭里" => "潭裡", +"激荡" => "激蕩", "浓郁" => "濃郁", "浓发" => "濃髮", "湿地松" => "濕地鬆", "蒙蒙" => "濛濛", "蒙雾" => "濛霧", -"蒙鸿" => "濛鴻", "瀛台" => "瀛臺", "弥漫" => "瀰漫", "弥漫着" => "瀰漫著", -"漓江" => "灕江", "火并" => "火併", "灰蒙" => "灰濛", "炒面" => "炒麵", @@ -3634,7 +3543,7 @@ $zh2TW=array( "烫面" => "燙麵", "烛台" => "燭臺", "炉台" => "爐臺", -"墙里" => "牆裡", +"爽荡" => "爽蕩", "片言只语" => "片言隻語", "牛肉面" => "牛肉麵", "牛只" => "牛隻", @@ -3648,8 +3557,8 @@ $zh2TW=array( "奖杯" => "獎盃", "获准" => "獲准", "率团参加" => "率團參加", -"王侯後" => "王侯后", -"王後" => "王后", +"王侯后" => "王侯后", +"王后" => "王后", "班里" => "班裡", "理发" => "理髮", "瑶台" => "瑤臺", @@ -3660,14 +3569,12 @@ $zh2TW=array( "生发" => "生髮", "田里" => "田裡", "由馀" => "由余", -"由表及里" => "由表及裡", "男佣" => "男佣", "男用表" => "男用錶", "留发" => "留髮", "畚斗" => "畚斗", "当着" => "當著", "疏松" => "疏鬆", -"疑系" => "疑係", "疲困" => "疲睏", "病症" => "病癥", "症候" => "癥候", @@ -3675,19 +3582,16 @@ $zh2TW=array( "症结" => "癥結", "登台" => "登臺", "发布" => "發佈", -"发蒙" => "發矇", "发着" => "發著", "发面" => "發麵", "发霉" => "發黴", "白卷" => "白卷", "白干儿" => "白干兒", -"白里透红" => "白裡透紅", "白发" => "白髮", "白面" => "白麵", -"百谷" => "百穀", "百里" => "百裡", "百只" => "百隻", -"皇後" => "皇后", +"皇后" => "皇后", "皇历" => "皇曆", "皓发" => "皓髮", "皮里阳秋" => "皮裏陽秋", @@ -3708,14 +3612,12 @@ $zh2TW=array( "眼眶里" => "眼眶裡", "眼里" => "眼裡", "困乏" => "睏乏", -"困倦" => "睏倦", "睡着了" => "睡著了", "了如" => "瞭如", "了望" => "瞭望", "了然" => "瞭然", "了若指掌" => "瞭若指掌", "了解" => "瞭解", -"瞳蒙" => "瞳矇", "蒙住" => "矇住", "蒙昧无知" => "矇昧無知", "蒙混" => "矇混", @@ -3724,9 +3626,7 @@ $zh2TW=array( "蒙蔽" => "矇蔽", "蒙骗" => "矇騙", "短发" => "短髮", -"矮几" => "矮几", "石英表" => "石英錶", -"石莼" => "石蓴", "研制" => "研製", "砰当" => "砰噹", "砲台" => "砲臺", @@ -3736,7 +3636,6 @@ $zh2TW=array( "朱笔" => "硃筆", "朱红色" => "硃紅色", "朱色" => "硃色", -"朱谕" => "硃諭", "硬干" => "硬幹", "砚台" => "硯臺", "碑志" => "碑誌", @@ -3749,9 +3648,7 @@ $zh2TW=array( "御寇" => "禦寇", "御寒" => "禦寒", "御敌" => "禦敵", -"礼义干橹" => "禮義干櫓", "秃发" => "禿髮", -"秀斗" => "秀斗", "秀发" => "秀髮", "私下里" => "私下裡", "秋天里" => "秋天裡", @@ -3761,17 +3658,13 @@ $zh2TW=array( "禀复" => "稟覆", "稻谷" => "稻穀", "稽征" => "稽征", -"谷人" => "穀人", -"谷保家商" => "穀保家商", "谷仓" => "穀倉", "谷场" => "穀場", "谷子" => "穀子", -"谷梁" => "穀梁", "谷壳" => "穀殼", "谷物" => "穀物", "谷皮" => "穀皮", "谷神" => "穀神", -"谷谷" => "穀穀", "谷粒" => "穀粒", "谷舱" => "穀艙", "谷苗" => "穀苗", @@ -3780,19 +3673,16 @@ $zh2TW=array( "谷道" => "穀道", "谷雨" => "穀雨", "谷类" => "穀類", -"谷风" => "穀風", "积极参与" => "積极參与", "积极参加" => "積极參加", -"积谷防饥" => "積穀防饑", -"空蒙" => "空濛", +"空荡" => "空蕩", "窗帘" => "窗帘", "窗明几净" => "窗明几淨", "窗台" => "窗檯", "窗台" => "窗臺", "窝里" => "窩裡", "窝阔台" => "窩闊臺", -"穷发" => "窮髮", -"站台" => "站臺", +"穷追不舍" => "窮追不捨", "笆斗" => "笆斗", "笑里藏刀" => "笑裡藏刀", "第一卷" => "第一卷", @@ -3802,7 +3692,6 @@ $zh2TW=array( "答复" => "答覆", "筵几" => "筵几", "箕斗" => "箕斗", -"算历" => "算曆", "签着" => "簽著", "吁求" => "籲求", "吁请" => "籲請", @@ -3817,14 +3706,12 @@ $zh2TW=array( "糊里糊涂" => "糊裡糊塗", "团子" => "糰子", "系着" => "系著", -"系里" => "系裡", "纪历" => "紀曆", -"红绳系足" => "紅繩繫足", "红发" => "紅髮", +"红霉素" => "紅黴素", "纡回" => "紆迴", "纳采" => "納采", "素食面" => "素食麵", -"素发" => "素髮", "素面" => "素麵", "紫微斗数" => "紫微斗數", "细致" => "細緻", @@ -3841,11 +3728,13 @@ $zh2TW=array( "丝虫" => "絲蟲", "綑吊" => "綑吊", "经卷" => "經卷", +"绿霉素" => "綠黴素", "维系" => "維繫", "绾发" => "綰髮", "网里" => "網裡", "紧绷" => "緊繃", "紧绷着" => "緊繃著", +"紧追不舍" => "緊追不捨", "编制" => "編製", "编发" => "編髮", "缓冲" => "緩衝", @@ -3856,7 +3745,6 @@ $zh2TW=array( "缝里" => "縫裡", "缝制" => "縫製", "纤夫" => "縴夫", -"纤手" => "縴手", "繁复" => "繁複", "绷住" => "繃住", "绷子" => "繃子", @@ -3869,30 +3757,24 @@ $zh2TW=array( "绷开" => "繃開", "绘制" => "繪製", "系上" => "繫上", -"系世" => "繫世", "系到" => "繫到", "系囚" => "繫囚", "系心" => "繫心", "系念" => "繫念", "系怀" => "繫懷", -"系恋" => "繫戀", "系数" => "繫數", "系于" => "繫於", "系系" => "繫系", -"系结" => "繫結", "系紧" => "繫緊", "系绳" => "繫繩", -"系累" => "繫纍", "系着" => "繫著", "系辞" => "繫辭", -"系风捕影" => "繫風捕影", "缴卷" => "繳卷", "累囚" => "纍囚", "累累" => "纍纍", "坛子" => "罈子", "坛坛罐罐" => "罈罈罐罐", "骂着" => "罵著", -"羁系" => "羈繫", "美制" => "美製", "美发" => "美髮", "翻来复去" => "翻來覆去", @@ -3909,10 +3791,7 @@ $zh2TW=array( "肉丝面" => "肉絲麵", "肉羹面" => "肉羹麵", "肉松" => "肉鬆", -"肚里" => "肚裏", -"肚里" => "肚裡", "肢体" => "肢体", -"胃里" => "胃裡", "背向着" => "背向著", "背地里" => "背地裡", "胡里胡涂" => "胡裡胡塗", @@ -3925,13 +3804,11 @@ $zh2TW=array( "脑子里" => "腦子裡", "腰里" => "腰裡", "胶卷" => "膠卷", -"膨松" => "膨鬆", "自制" => "自製", "自觉自愿" => "自覺自愿", "台上" => "臺上", "台下" => "臺下", "台中" => "臺中", -"台儿庄" => "臺兒莊", "台北" => "臺北", "台南" => "臺南", "台地" => "臺地", @@ -3942,8 +3819,6 @@ $zh2TW=array( "台东" => "臺東", "台柱" => "臺柱", "台榭" => "臺榭", -"台机路" => "臺機路", -"台步" => "臺步", "台汽" => "臺汽", "台海" => "臺海", "台澎金马" => "臺澎金馬", @@ -3955,7 +3830,6 @@ $zh2TW=array( "台糖" => "臺糖", "台肥" => "臺肥", "台航" => "臺航", -"台西" => "臺西", "台视" => "臺視", "台词" => "臺詞", "台车" => "臺車", @@ -3968,7 +3842,6 @@ $zh2TW=array( "兴高采烈" => "興高采烈", "旧历" => "舊曆", "舒卷" => "舒卷", -"舞榭歌台" => "舞榭歌臺", "舞台" => "舞臺", "航海历" => "航海曆", "船只" => "船隻", @@ -3979,7 +3852,6 @@ $zh2TW=array( "花采" => "花采", "苑里" => "苑裡", "若干" => "若干", -"若干" => "若幹", "苦干" => "苦幹", "苦里" => "苦裏", "苦卤" => "苦鹵", @@ -3991,6 +3863,7 @@ $zh2TW=array( "草丛里" => "草叢裡", "庄里" => "莊裡", "茎干" => "莖幹", +"莽荡" => "莽蕩", "菌丝体" => "菌絲体", "菌丝体" => "菌絲體", "华里" => "華裡", @@ -4010,10 +3883,20 @@ $zh2TW=array( "蓬发" => "蓬髮", "蓬松" => "蓬鬆", "莲台" => "蓮臺", +"荡来荡去" => "蕩來蕩去", +"荡女" => "蕩女", +"荡妇" => "蕩婦", +"荡寇" => "蕩寇", +"荡平" => "蕩平", +"荡涤" => "蕩滌", +"荡漾" => "蕩漾", +"荡然" => "蕩然", +"荡舟" => "蕩舟", +"荡船" => "蕩船", +"荡荡" => "蕩蕩", "薑丝" => "薑絲", "薙发" => "薙髮", "借以" => "藉以", -"借助" => "藉助", "借口" => "藉口", "借故" => "藉故", "借机" => "藉機", @@ -4032,9 +3915,7 @@ $zh2TW=array( "萝卜" => "蘿蔔", "虎须" => "虎鬚", "号志" => "號誌", -"蜂後" => "蜂后", -"蜜里调油" => "蜜裡調油", -"蠁干" => "蠁幹", +"蜂后" => "蜂后", "蛮干" => "蠻幹", "行事历" => "行事曆", "胡同" => "衚衕", @@ -4042,7 +3923,6 @@ $zh2TW=array( "冲下" => "衝下", "冲来" => "衝來", "冲倒" => "衝倒", -"冲冠" => "衝冠", "冲出" => "衝出", "冲到" => "衝到", "冲刺" => "衝刺", @@ -4075,7 +3955,6 @@ $zh2TW=array( "冲过" => "衝過", "冲锋" => "衝鋒", "表里" => "表裡", -"袋里" => "袋裡", "袖里" => "袖裡", "被里" => "被裡", "被复" => "被複", @@ -4177,7 +4056,6 @@ $zh2TW=array( "复苏" => "複蘇", "复制" => "複製", "复诊" => "複診", -"复评" => "複評", "复词" => "複詞", "复试" => "複試", "复课" => "複課", @@ -4187,7 +4065,6 @@ $zh2TW=array( "复述" => "複述", "复选" => "複選", "复钱" => "複錢", -"复阅" => "複閱", "复杂" => "複雜", "复电" => "複電", "复音" => "複音", @@ -4202,13 +4079,10 @@ $zh2TW=array( "复亡" => "覆亡", "复住" => "覆住", "复信" => "覆信", -"复冒" => "覆冒", -"复判" => "覆判", "复命" => "覆命", "复在" => "覆在", "复审" => "覆審", -"复写" => "覆寫", -"复巢" => "覆巢", +"复巢之下" => "覆巢之下", "复成" => "覆成", "复败" => "覆敗", "复文" => "覆文", @@ -4217,22 +4091,17 @@ $zh2TW=array( "复水难收" => "覆水難收", "复没" => "覆沒", "复灭" => "覆滅", -"复叠" => "覆疊", "复盆" => "覆盆", "复舟" => "覆舟", "复着" => "覆著", "复盖" => "覆蓋", "复盖着" => "覆蓋著", "复试" => "覆試", -"复诵" => "覆誦", "复议" => "覆議", "复车" => "覆車", "复载" => "覆載", "复辙" => "覆轍", -"复述" => "覆述", -"复选" => "覆選", "复电" => "覆電", -"复鼎金" => "覆鼎金", "见复" => "見覆", "亲征" => "親征", "观众台" => "觀眾臺", @@ -4244,9 +4113,7 @@ $zh2TW=array( "订制" => "訂製", "诉说着" => "訴說著", "词汇" => "詞彙", -"词采" => "詞采", "试卷" => "試卷", -"试制" => "試製", "诗卷" => "詩卷", "话里有话" => "話裡有話", "志哀" => "誌哀", @@ -4264,7 +4131,6 @@ $zh2TW=array( "讲台" => "講臺", "谢绝参观" => "謝絕參觀", "护发" => "護髮", -"雠正" => "讎正", "雠隙" => "讎隙", "豆腐干" => "豆腐干", "竖着" => "豎著", @@ -4274,22 +4140,20 @@ $zh2TW=array( "丰采" => "豐采", "象征着" => "象徵著", "贵干" => "貴幹", -"贾後" => "賈后", +"贾后" => "賈后", "赈饥" => "賑饑", -"赐复" => "賜覆", -"贤後" => "賢后", +"贤后" => "賢后", "质朴" => "質朴", "赌台" => "賭檯", "购并" => "購併", -"赤绳系足" => "赤繩繫足", "赤松" => "赤鬆", "起吊" => "起吊", "起复" => "起複", -"超级杯" => "超級盃", "赶制" => "趕製", +"跌荡" => "跌蕩", "跟斗" => "跟斗", +"跳荡" => "跳蕩", "跳表" => "跳錶", -"蹈借" => "蹈藉", "踬仆" => "躓仆", "躯干" => "軀幹", "车库里" => "車庫裡", @@ -4299,7 +4163,6 @@ $zh2TW=array( "轮回" => "輪迴", "转台" => "轉檯", "辛丑" => "辛丑", -"辟易" => "辟易", "辟邪" => "辟邪", "办伙" => "辦伙", "办公台" => "辦公檯", @@ -4317,10 +4180,9 @@ $zh2TW=array( "回旋" => "迴旋", "回流" => "迴流", "回环" => "迴環", -"回盪" => "迴盪", +"回荡" => "迴盪", "回纹针" => "迴紋針", "回绕" => "迴繞", -"回翔" => "迴翔", "回肠" => "迴腸", "回荡" => "迴蕩", "回诵" => "迴誦", @@ -4328,8 +4190,6 @@ $zh2TW=array( "回转" => "迴轉", "回递性" => "迴遞性", "回避" => "迴避", -"回銮" => "迴鑾", -"回音" => "迴音", "回响" => "迴響", "回风" => "迴風", "回首" => "迴首", @@ -4345,6 +4205,7 @@ $zh2TW=array( "速食面" => "速食麵", "连系" => "連繫", "连台好戏" => "連臺好戲", +"游荡" => "遊蕩", "遍布" => "遍佈", "递回" => "遞迴", "远征" => "遠征", @@ -4352,7 +4213,6 @@ $zh2TW=array( "遮复" => "遮覆", "还冲" => "還衝", "邋里邋遢" => "邋裡邋遢", -"那里" => "那裏", "那里" => "那裡", "那只" => "那隻", "那么" => "那麼", @@ -4381,22 +4241,18 @@ $zh2TW=array( "采女" => "采女", "采声" => "采聲", "采色" => "采色", -"采薇" => "采薇", -"采薪之忧" => "采薪之憂", -"采兰赠药" => "采蘭贈藥", "采邑" => "采邑", -"采采" => "采采", -"采风" => "采風", "里程表" => "里程錶", "重折" => "重摺", -"重制" => "重製", "重复" => "重複", "重复" => "重覆", +"重锤" => "重鎚", "野台戏" => "野臺戲", "金斗" => "金斗", -"金装玉里" => "金裝玉裡", "金表" => "金錶", "金发" => "金髮", +"金霉素" => "金黴素", +"钉锤" => "釘鎚", "银朱" => "銀硃", "银发" => "銀髮", "铜制" => "銅製", @@ -4413,11 +4269,17 @@ $zh2TW=array( "锅台" => "鍋臺", "锻鍊出" => "鍛鍊出", "锻鍊身体" => "鍛鍊身体", +"锲而不舍" => "鍥而不捨", +"锤儿" => "鎚兒", +"锤子" => "鎚子", +"锤头" => "鎚頭", +"链霉素" => "鏈黴素", "镜台" => "鏡臺", "锈病" => "鏽病", "锈菌" => "鏽菌", "锈蚀" => "鏽蝕", "钟表" => "鐘錶", +"铁锤" => "鐵鎚", "铁锈" => "鐵鏽", "长征" => "長征", "长发" => "長髮", @@ -4430,6 +4292,7 @@ $zh2TW=array( "开诚布公" => "開誠佈公", "开采" => "開采", "閒情逸致" => "閒情逸緻", +"閒荡" => "閒蕩", "间不容发" => "間不容髮", "闵采尔" => "閔采爾", "阅卷" => "閱卷", @@ -4464,7 +4327,6 @@ $zh2TW=array( "鸡腿面" => "雞腿麵", "鸡只" => "雞隻", "难舍" => "難捨", -"雨花台" => "雨花臺", "雪里" => "雪裡", "云须" => "雲鬚", "电子表" => "電子錶", @@ -4473,7 +4335,7 @@ $zh2TW=array( "电复" => "電覆", "电视台" => "電視臺", "电表" => "電錶", -"雾台" => "霧臺", +"震荡" => "震蕩", "雾里" => "霧裡", "露台" => "露臺", "灵台" => "靈臺", @@ -4487,7 +4349,6 @@ $zh2TW=array( "鞭辟入里" => "鞭辟入裡", "韩国制" => "韓國製", "韩制" => "韓製", -"颂系" => "頌繫", "预制" => "預製", "颁布" => "頒佈", "头里" => "頭裡", @@ -4511,6 +4372,7 @@ $zh2TW=array( "刮走" => "颳走", "刮起" => "颳起", "刮风" => "颳風", +"飘荡" => "飄蕩", "饭团" => "飯糰", "饼干" => "餅干", "馄饨面" => "餛飩麵", @@ -4528,13 +4390,13 @@ $zh2TW=array( "馥郁" => "馥郁", "马里" => "馬裡", "马表" => "馬錶", +"骀荡" => "駘蕩", "腾冲" => "騰衝", "骨子里" => "骨子裡", "骨干" => "骨幹", "骨灰坛" => "骨灰罈", "肮脏" => "骯髒", "脏乱" => "髒亂", -"脏了" => "髒了", "脏兮兮" => "髒兮兮", "脏字" => "髒字", "脏得" => "髒得", @@ -4572,7 +4434,6 @@ $zh2TW=array( "发针" => "髮針", "发长" => "髮長", "发际" => "髮際", -"发雕" => "髮雕", "发霜" => "髮霜", "发髻" => "髮髻", "发鬓" => "髮鬢", @@ -4587,7 +4448,6 @@ $zh2TW=array( "松快" => "鬆快", "松懈" => "鬆懈", "松手" => "鬆手", -"松掉" => "鬆掉", "松散" => "鬆散", "松林" => "鬆林", "松柔" => "鬆柔", @@ -4624,11 +4484,9 @@ $zh2TW=array( "闹着玩儿" => "鬧著玩儿", "闹着玩儿" => "鬧著玩兒", "郁郁" => "鬱郁", -"魂牵梦系" => "魂牽夢繫", "鱼松" => "魚鬆", "鲸须" => "鯨鬚", "鲇鱼" => "鯰魚", -"鸿篇巨制" => "鴻篇巨製", "鹤发" => "鶴髮", "卤化" => "鹵化", "卤味" => "鹵味", @@ -4664,7 +4522,6 @@ $zh2TW=array( "面团" => "麵團", "面店" => "麵店", "面厂" => "麵廠", -"面摊" => "麵攤", "面杖" => "麵杖", "面条" => "麵條", "面灰" => "麵灰", @@ -4678,341 +4535,28 @@ $zh2TW=array( "面饺" => "麵餃", "面饼" => "麵餅", "麻酱面" => "麻醬麵", -"黄卷" => "黃卷", "黄历" => "黃曆", -"黄发" => "黃髮", +"黄发垂髫" => "黃髮垂髫", "黑发" => "黑髮", "黑松" => "黑鬆", "霉毒" => "黴毒", -"霉素" => "黴素", "霉菌" => "黴菌", "鼓里" => "鼓裡", "冬冬" => "鼕鼕", "龙卷" => "龍卷", "龙须" => "龍鬚", -"内存"=>"記憶體", -"默认"=>"預設", -"缺省"=>"預設", -"串行"=>"串列", -"以太网"=>"乙太網", -"位图"=>"點陣圖", -"例程"=>"常式", -"信道"=>"通道", -"光标"=>"游標", -"光盘"=>"光碟", -"光驱"=>"光碟機", -"全角"=>"全形", -"共享"=>"共用", -"兼容"=>"相容", -"前缀"=>"首碼", -"后缀"=>"尾碼", -"加载"=>"載入", -"半角"=>"半形", -"变量"=>"變數", -"噪声"=>"雜訊", -"因子"=>"因數", -"在线"=>"線上", -"脱机"=>"離線", -"域名"=>"功能變數名稱", -"声卡"=>"音效卡", -"字号"=>"字型大小", -"字库"=>"字型檔", -"字段"=>"欄位", -"字符"=>"字元", -"存盘"=>"存檔", -"寻址"=>"定址", -"尾注"=>"章節附註", -"异步"=>"非同步", -"总线"=>"匯流排", -"括号"=>"括弧", -"接口"=>"介面", -"控件"=>"控制項", -"权限"=>"許可權", -"盘片"=>"碟片", -"硅片"=>"矽片", -"硅谷"=>"矽谷", -"硬盘"=>"硬碟", -"磁盘"=>"磁碟", -"磁道"=>"磁軌", -"程控"=>"程式控制", -"端口"=>"埠", -"算子"=>"運算元", -"算法"=>"演算法", -"芯片"=>"晶片", -"芯片"=>"晶元", -"词组"=>"片語", -"译码"=>"解碼", -"软驱"=>"軟碟機", -"闪存"=>"快閃記憶體", -"鼠标"=>"滑鼠", -"进制"=>"進位", -"交互式"=>"互動式", -"仿真"=>"模擬", -"优先级"=>"優先順序", -"传感"=>"感測", -"便携式"=>"攜帶型", -"信息论"=>"資訊理論", -"循环"=>"迴圈", -"写保护"=>"防寫", -"分布式"=>"分散式", -"分辨率"=>"解析度", -"程序"=>"程式", -"服务器"=>"伺服器", -"等于"=>"等於", -"局域网"=>"區域網", -"上载"=>"上傳", -"计算机"=>"電腦", -"宏"=>"巨集", -"扫瞄仪"=>"掃瞄器", -"宽带"=>"寬頻", -"窗口"=>"視窗", -"数据库"=>"資料庫", -"公历"=>"西曆", -"奶酪"=>"乳酪", -"巨商"=>"鉅賈", -"手电"=>"手電筒", -"万历"=>"萬曆", -"永历"=>"永曆", -"词汇"=>"辭彙", -"保安"=>"保全", -"习用"=>"慣用", -"元音"=>"母音", -"任意球"=>"自由球", -"头球"=>"頭槌", -"入球"=>"進球", -"粒入球"=>"顆進球", -"打门"=>"射門", -"火锅盖帽"=>"蓋火鍋", -"打印机"=>"印表機", -"打印機"=>"印表機", -"字节"=>"位元組", -"字節"=>"位元組", -"打印"=>"列印", -"打印"=>"列印", -"硬件"=>"硬體", -"硬件"=>"硬體", -"二极管"=>"二極體", -"二極管"=>"二極體", -"三极管"=>"三極體", -"三極管"=>"三極體", -"数码"=>"數位", -"數碼"=>"數位", -"软件"=>"軟體", -"軟件"=>"軟體", -"网络"=>"網路", -"網絡"=>"網路", -"人工智能"=>"人工智慧", -"航天飞机"=>"太空梭", -"穿梭機"=>"太空梭", -"因特网"=>"網際網路", -"互聯網"=>"網際網路", -"机器人"=>"機器人", -"機械人"=>"機器人", -"移动电话"=>"行動電話", -"流動電話"=>"行動電話", -"调制解调器"=>"數據機", -"調制解調器"=>"數據機", -"短信"=>"簡訊", -"短訊"=>"簡訊", -"乌兹别克斯坦"=>"烏茲別克", -"乍得"=>"查德", -"乍得"=>"查德", -"也门"=>"葉門", -"也門"=>"葉門", -"伯利兹"=>"貝里斯", -"伯利茲"=>"貝里斯", -"佛得角"=>"維德角", -"佛得角"=>"維德角", -"克罗地亚"=>"克羅埃西亞", -"克羅地亞"=>"克羅埃西亞", -"冈比亚"=>"甘比亞", -"岡比亞"=>"甘比亞", -"几内亚比绍"=>"幾內亞比索", -"幾內亞比紹"=>"幾內亞比索", -"列支敦士登"=>"列支敦斯登", -"列支敦士登"=>"列支敦斯登", -"利比里亚"=>"賴比瑞亞", -"利比里亞"=>"賴比瑞亞", -"加纳"=>"迦納", -"加納"=>"迦納", -"加蓬"=>"加彭", -"加蓬"=>"加彭", -"博茨瓦纳"=>"波札那", -"博茨瓦納"=>"波札那", -"卡塔尔"=>"卡達", -"卡塔爾"=>"卡達", -"卢旺达"=>"盧安達", -"盧旺達"=>"盧安達", -"危地马拉"=>"瓜地馬拉", -"危地馬拉"=>"瓜地馬拉", -"厄瓜多尔"=>"厄瓜多", -"厄瓜多爾"=>"厄瓜多", -"厄立特里亚"=>"厄利垂亞", -"厄立特里亞"=>"厄利垂亞", -"吉布提"=>"吉布地", -"吉布堤"=>"吉布地", -"哈萨克斯坦"=>"哈薩克", -"哥斯达黎加"=>"哥斯大黎加", -"哥斯達黎加"=>"哥斯大黎加", -"图瓦卢"=>"吐瓦魯", -"圖瓦盧"=>"吐瓦魯", -"土库曼斯坦"=>"土庫曼", -"圣卢西亚"=>"聖露西亞", -"聖盧西亞"=>"聖露西亞", -"圣基茨和尼维斯"=>"聖克里斯多福及尼維斯", -"聖吉斯納域斯"=>"聖克里斯多福及尼維斯", -"圣文森特和格林纳丁斯"=>"聖文森及格瑞那丁", -"聖文森特和格林納丁斯"=>"聖文森及格瑞那丁", -"圣马力诺"=>"聖馬利諾", -"聖馬力諾"=>"聖馬利諾", -"圭亚那"=>"蓋亞那", -"圭亞那"=>"蓋亞那", -"坦桑尼亚"=>"坦尚尼亞", -"坦桑尼亞"=>"坦尚尼亞", -"埃塞俄比亚"=>"衣索比亞", -"埃塞俄比亞"=>"衣索比亞", -"基里巴斯"=>"吉里巴斯", -"基里巴斯"=>"吉里巴斯", -"塔吉克斯坦"=>"塔吉克", -"塞拉利昂"=>"獅子山", -"塞拉利昂"=>"獅子山", -"塞浦路斯"=>"塞普勒斯", -"塞浦路斯"=>"塞普勒斯", -"塞舌尔"=>"塞席爾", -"塞舌爾"=>"塞席爾", -"多米尼加"=>"多明尼加", -"多明尼加共和國"=>"多明尼加", -"多米尼加联邦"=>"多米尼克", -"多明尼加聯邦"=>"多米尼克", -"安提瓜和巴布达"=>"安地卡及巴布達", -"安提瓜和巴布達"=>"安地卡及巴布達", -"尼日利亚"=>"奈及利亞", -"尼日利亞"=>"奈及利亞", -"尼日尔"=>"尼日", -"尼日爾"=>"尼日", -"巴巴多斯"=>"巴貝多", -"巴巴多斯"=>"巴貝多", -"巴布亚新几内亚"=>"巴布亞紐幾內亞", -"巴布亞新畿內亞"=>"巴布亞紐幾內亞", -"布基纳法索"=>"布吉納法索", -"布基納法索"=>"布吉納法索", -"布隆迪"=>"蒲隆地", -"布隆迪"=>"蒲隆地", -"希腊"=>"希臘", -"帕劳"=>"帛琉", -"意大利"=>"義大利", -"意大利"=>"義大利", -"所罗门群岛"=>"索羅門群島", -"所羅門群島"=>"索羅門群島", -"文莱"=>"汶萊", -"斯威士兰"=>"史瓦濟蘭", -"斯威士蘭"=>"史瓦濟蘭", -"斯洛文尼亚"=>"斯洛維尼亞", -"斯洛文尼亞"=>"斯洛維尼亞", -"新西兰"=>"紐西蘭", -"新西蘭"=>"紐西蘭", -"朝鲜"=>"北韓", -"格林纳达"=>"格瑞那達", -"格林納達"=>"格瑞那達", -"格鲁吉亚"=>"喬治亞", -"格魯吉亞"=>"喬治亞", -"梵蒂冈"=>"教廷", -"梵蒂岡"=>"教廷", -"毛里塔尼亚"=>"茅利塔尼亞", -"毛里塔尼亞"=>"茅利塔尼亞", -"毛里求斯"=>"模里西斯", -"毛里裘斯"=>"模里西斯", -"沙特阿拉伯"=>"沙烏地阿拉伯", -"沙地阿拉伯"=>"沙烏地阿拉伯", -"波斯尼亚和黑塞哥维那"=>"波士尼亞赫塞哥維納", -"波斯尼亞黑塞哥維那"=>"波士尼亞赫塞哥維納", -"津巴布韦"=>"辛巴威", -"津巴布韋"=>"辛巴威", -"洪都拉斯"=>"宏都拉斯", -"洪都拉斯"=>"宏都拉斯", -"特立尼达和托巴哥"=>"千里達托貝哥", -"特立尼達和多巴哥"=>"千里達托貝哥", -"瑙鲁"=>"諾魯", -"瑙魯"=>"諾魯", -"瓦努阿图"=>"萬那杜", -"瓦努阿圖"=>"萬那杜", -"溫納圖萬"=>"那杜", -"科摩罗"=>"葛摩", -"科摩羅"=>"葛摩", -"科特迪瓦"=>"象牙海岸", -"突尼斯"=>"突尼西亞", -"索马里"=>"索馬利亞", -"索馬里"=>"索馬利亞", -"老挝"=>"寮國", -"老撾"=>"寮國", -"肯尼亚"=>"肯亞", -"肯雅"=>"肯亞", -"苏里南"=>"蘇利南", -"莫桑比克"=>"莫三比克", -"莱索托"=>"賴索托", -"萊索托"=>"賴索托", -"贝宁"=>"貝南", -"貝寧"=>"貝南", -"赞比亚"=>"尚比亞", -"贊比亞"=>"尚比亞", -"阿塞拜疆"=>"亞塞拜然", -"阿塞拜疆"=>"亞塞拜然", -"阿拉伯联合酋长国"=>"阿拉伯聯合大公國", -"阿拉伯聯合酋長國"=>"阿拉伯聯合大公國", -"韩国"=>"南韓", -"马尔代夫"=>"馬爾地夫", -"馬爾代夫"=>"馬爾地夫", -"马耳他"=>"馬爾他", -"马里"=>"馬利", -"馬里"=>"馬利", -"方便面"=>"速食麵", -"快速面"=>"速食麵", -"即食麵"=>"速食麵", -"薯仔"=>"土豆", -"蹦极跳"=>"笨豬跳", -"绑紧跳"=>"笨豬跳", -"冷菜"=>"冷盤", -"凉菜"=>"冷盤", -"的士"=>"計程車", -"出租车"=>"計程車", -"巴士"=>"公車", -"公共汽车"=>"公車", -"台球"=>"撞球", -"桌球"=>"撞球", -"雪糕"=>"冰淇淋", -"卫生"=>"衛生", -"衞生"=>"衛生", -"平治"=>"賓士", -"奔驰"=>"賓士", -"積架"=>"捷豹", -"福士"=>"福斯", -"雪铁龙"=>"雪鐵龍", -"马自达"=>"馬自達", -"萬事得"=>"馬自達", -"布什"=>"布希", -"布殊"=>"布希", -"克林顿"=>"柯林頓", -"克林頓"=>"柯林頓", -"萨达姆"=>"海珊", -"薩達姆"=>"海珊", -"凡高"=>"梵谷", -"狄安娜"=>"黛安娜", -"戴安娜"=>"黛安娜", -"赫拉"=>"希拉", ); - -$zh2CN=array( -"么"=>"么", +$zh2Hans = array( "瀋"=>"沈", "畫"=>"划", "鍾"=>"钟", +"靦"=>"腼", "餘"=>"余", "鯰"=>"鲇", "鹼"=>"硷", -"麼"=>"么", -"䊷"=>"䌶", -"𧩙"=>"䜥", +"㠏"=>"㟆", +"𡞵"=>"㛟", "万"=>"万", "与"=>"与", "丑"=>"丑", @@ -5087,6 +4631,7 @@ $zh2CN=array( "優"=>"优", "儲"=>"储", "儷"=>"俪", +"儸"=>"㑩", "儺"=>"傩", "儻"=>"傥", "儼"=>"俨", @@ -5125,7 +4670,9 @@ $zh2CN=array( "劊"=>"刽", "劌"=>"刿", "劍"=>"剑", +"劏"=>"㓥", "劑"=>"剂", +"劚"=>"㔉", "勁"=>"劲", "動"=>"动", "務"=>"务", @@ -5158,11 +4705,11 @@ $zh2CN=array( "叶"=>"叶", "吊"=>"吊", "后"=>"后", -"后"=>"後", "吒"=>"咤", "吳"=>"吴", "吶"=>"呐", "呂"=>"吕", +"呆"=>"獃", "咼"=>"呙", "員"=>"员", "唄"=>"呗", @@ -5200,6 +4747,7 @@ $zh2CN=array( "嘽"=>"啴", "噁"=>"恶", "噓"=>"嘘", +"噚"=>"㖊", "噝"=>"咝", "噠"=>"哒", "噥"=>"哝", @@ -5423,6 +4971,7 @@ $zh2CN=array( "復"=>"复", "徵"=>"征", "徹"=>"彻", +"志"=>"志", "恆"=>"恒", "恥"=>"耻", "悅"=>"悦", @@ -5490,7 +5039,6 @@ $zh2CN=array( "戶"=>"户", "担"=>"担", "拋"=>"抛", -"拾"=>"十", "挩"=>"捝", "挾"=>"挟", "捨"=>"舍", @@ -5700,6 +5248,7 @@ $zh2CN=array( "殫"=>"殚", "殮"=>"殓", "殯"=>"殡", +"殰"=>"㱩", "殲"=>"歼", "殺"=>"杀", "殻"=>"壳", @@ -5803,6 +5352,7 @@ $zh2CN=array( "濾"=>"滤", "瀅"=>"滢", "瀆"=>"渎", +"瀇"=>"㲿", "瀉"=>"泻", "瀋"=>"沈", "瀏"=>"浏", @@ -5839,6 +5389,7 @@ $zh2CN=array( "煥"=>"焕", "煩"=>"烦", "煬"=>"炀", +"煱"=>"㶽", "熅"=>"煴", "熒"=>"荧", "熗"=>"炝", @@ -5855,6 +5406,7 @@ $zh2CN=array( "燦"=>"灿", "燭"=>"烛", "燴"=>"烩", +"燶"=>"㶶", "燼"=>"烬", "燾"=>"焘", "爍"=>"烁", @@ -5877,6 +5429,7 @@ $zh2CN=array( "猶"=>"犹", "猻"=>"狲", "獁"=>"犸", +"獃"=>"呆", "獄"=>"狱", "獅"=>"狮", "獎"=>"奖", @@ -5885,6 +5438,7 @@ $zh2CN=array( "獫"=>"猃", "獮"=>"狝", "獰"=>"狞", +"獱"=>"㺍", "獲"=>"获", "獵"=>"猎", "獷"=>"犷", @@ -5965,6 +5519,7 @@ $zh2CN=array( "監"=>"监", "盤"=>"盘", "盧"=>"卢", +"盪"=>"荡", "眥"=>"眦", "眾"=>"众", "睏"=>"困", @@ -6020,6 +5575,7 @@ $zh2CN=array( "种"=>"种", "稅"=>"税", "稈"=>"秆", +"稏"=>"䅉", "稟"=>"禀", "種"=>"种", "稱"=>"称", @@ -6154,6 +5710,7 @@ $zh2CN=array( "綉"=>"绣", "綌"=>"绤", "綏"=>"绥", +"綐"=>"䌼", "經"=>"经", "綜"=>"综", "綞"=>"缍", @@ -6169,6 +5726,7 @@ $zh2CN=array( "網"=>"网", "綳"=>"绷", "綴"=>"缀", +"綵"=>"䌽", "綸"=>"纶", "綹"=>"绺", "綺"=>"绮", @@ -6254,6 +5812,7 @@ $zh2CN=array( "繼"=>"继", "繽"=>"缤", "繾"=>"缱", +"繿"=>"䍀", "纈"=>"缬", "纊"=>"纩", "續"=>"续", @@ -6336,7 +5895,6 @@ $zh2CN=array( "艱"=>"艰", "艷"=>"艳", "芻"=>"刍", -"苎"=>"苧", "苧"=>"苎", "苹"=>"苹", "范"=>"范", @@ -6395,6 +5953,7 @@ $zh2CN=array( "薟"=>"莶", "薦"=>"荐", "薩"=>"萨", +"薳"=>"䓕", "薴"=>"苧", "薺"=>"荠", "藉"=>"借", @@ -6437,6 +5996,7 @@ $zh2CN=array( "螄"=>"蛳", "螞"=>"蚂", "螢"=>"萤", +"螮"=>"䗖", "螻"=>"蝼", "螿"=>"螀", "蟄"=>"蛰", @@ -6487,6 +6047,7 @@ $zh2CN=array( "襠"=>"裆", "襤"=>"褴", "襪"=>"袜", +"襬"=>"䙓", "襯"=>"衬", "襲"=>"袭", "覆"=>"复", @@ -6650,6 +6211,7 @@ $zh2CN=array( "謳"=>"讴", "謹"=>"谨", "謾"=>"谩", +"譅"=>"䜧", "證"=>"证", "譎"=>"谲", "譏"=>"讥", @@ -6682,6 +6244,7 @@ $zh2CN=array( "豬"=>"猪", "豶"=>"豮", "貓"=>"猫", +"貙"=>"䝙", "貝"=>"贝", "貞"=>"贞", "貟"=>"贠", @@ -6735,6 +6298,7 @@ $zh2CN=array( "賫"=>"赍", "賬"=>"账", "賭"=>"赌", +"賰"=>"䞐", "賴"=>"赖", "賵"=>"赗", "賺"=>"赚", @@ -7080,7 +6644,9 @@ $zh2CN=array( "鎔"=>"镕", "鎖"=>"锁", "鎘"=>"镉", +"鎚"=>"锤", "鎛"=>"镈", +"鎝"=>"𨱏", "鎡"=>"镃", "鎢"=>"钨", "鎣"=>"蓥", @@ -7251,7 +6817,6 @@ $zh2CN=array( "靈"=>"灵", "靚"=>"靓", "靜"=>"静", -"靦"=>"腼", "靨"=>"靥", "鞀"=>"鼗", "鞏"=>"巩", @@ -7431,6 +6996,7 @@ $zh2CN=array( "騖"=>"骛", "騙"=>"骗", "騤"=>"骙", +"騧"=>"䯄", "騫"=>"骞", "騭"=>"骘", "騮"=>"骝", @@ -7671,6 +7237,7 @@ $zh2CN=array( "鷺"=>"鹭", "鷽"=>"鸴", "鷿"=>"䴙", +"鸂"=>"㶉", "鸇"=>"鹯", "鸌"=>"鹱", "鸏"=>"鹲", @@ -7740,15 +7307,11 @@ $zh2CN=array( "名畫" => "名画", "奇畫" => "奇画", "如畫" => "如画", -"么 " => "幺 ", -"么廝" => "幺厮", -"么爹" => "幺爹", "弱鹼" => "弱碱", "彩畫" => "彩画", "所畫" => "所画", "扉畫" => "扉画", "教畫" => "教画", -"楊么" => "杨幺", "水鹼" => "水碱", "洋鹼" => "洋碱", "炭畫" => "炭画", @@ -7800,7 +7363,6 @@ $zh2CN=array( "策畫" => "策画", "組畫" => "组画", "絹畫" => "绢画", -"老么" => "老幺", "耐鹼" => "耐碱", "肉鹼" => "肉碱", "膠畫" => "胶画", @@ -7808,12 +7370,10 @@ $zh2CN=array( "西畫" => "西画", "貼畫" => "贴画", "返鹼" => "返碱", -"那麼" => "那麽", "鍾鍛" => "锺锻", "鍛鍾" => "锻锺", "雕畫" => "雕画", "鯰 " => "鲶 ", -"麼 " => "麽 ", "三聯畫" => "三联画", "中國畫" => "中国画", "書畫 " => "书画 ", @@ -7889,344 +7449,306 @@ $zh2CN=array( "鍾 " => "锺 ", "靜物畫" => "静物画", "餘 " => "馀 ", -"記憶體"=>"内存", -"預設"=>"默认", -"預設"=>"缺省", -"串列"=>"串行", -"乙太網"=>"以太网", -"點陣圖"=>"位图", -"常式"=>"例程", -"通道"=>"信道", -"游標"=>"光标", -"光碟"=>"光盘", -"光碟機"=>"光驱", -"全形"=>"全角", -"共用"=>"共享", -"相容"=>"兼容", -"首碼"=>"前缀", -"尾碼"=>"后缀", -"載入"=>"加载", -"半形"=>"半角", -"變數"=>"变量", -"雜訊"=>"噪声", -"因數"=>"因子", -"線上"=>"在线", -"離線"=>"脱机", -"功能變數名稱"=>"域名", -"音效卡"=>"声卡", -"字型大小"=>"字号", -"字型檔"=>"字库", -"欄位"=>"字段", -"字元"=>"字符", -"存檔"=>"存盘", -"定址"=>"寻址", -"章節附註"=>"尾注", -"非同步"=>"异步", -"匯流排"=>"总线", -"括弧"=>"括号", -"介面"=>"接口", -"控制項"=>"控件", -"許可權"=>"权限", -"碟片"=>"盘片", -"矽片"=>"硅片", -"矽谷"=>"硅谷", -"硬碟"=>"硬盘", -"磁碟"=>"磁盘", -"磁軌"=>"磁道", -"程式控制"=>"程控", -"埠"=>"端口", -"運算元"=>"算子", -"演算法"=>"算法", -"晶片"=>"芯片", -"晶元"=>"芯片", -"片語"=>"词组", -"解碼"=>"译码", -"軟碟機"=>"软驱", -"快閃記憶體"=>"闪存", -"滑鼠"=>"鼠标", -"進位"=>"进制", -"互動式"=>"交互式", -"模擬"=>"仿真", -"優先順序"=>"优先级", -"感測"=>"传感", -"攜帶型"=>"便携式", -"資訊理論"=>"信息论", -"迴圈"=>"循环", -"防寫"=>"写保护", -"分散式"=>"分布式", -"解析度"=>"分辨率", -"程式"=>"程序", -"伺服器"=>"服务器", -"等於"=>"等于", -"區域網"=>"局域网", -"上傳"=>"上载", -"電腦"=>"计算机", -"巨集"=>"宏", -"掃瞄器"=>"扫瞄仪", -"寬頻"=>"宽带", -"視窗"=>"窗口", -"資料庫"=>"数据库", -"西曆"=>"公历", -"乳酪"=>"奶酪", -"鉅賈"=>"巨商", -"手電筒"=>"手电", -"萬曆"=>"万历", -"永曆"=>"永历", -"辭彙"=>"词汇", -"保全"=>"保安", -"慣用"=>"习用", -"母音"=>"元音", -"自由球"=>"任意球", -"頭槌"=>"头球", -"進球"=>"入球", -"顆進球"=>"粒入球", -"射門"=>"打门", -"蓋火鍋"=>"火锅盖帽", -"印表機"=>"打印机", -"打印機"=>"打印机", -"位元組"=>"字节", -"字節"=>"字节", -"列印"=>"打印", -"打印"=>"打印", -"硬體"=>"硬件", -"二極體"=>"二极管", -"二極管"=>"二极管", -"三極體"=>"三极管", -"三極管"=>"三极管", -"數位"=>"数码", -"數碼"=>"数码", -"軟體"=>"软件", -"軟件"=>"软件", -"網路"=>"网络", -"網絡"=>"网络", -"人工智慧"=>"人工智能", -"太空梭"=>"航天飞机", -"穿梭機"=>"航天飞机", -"網際網路"=>"因特网", -"互聯網"=>"因特网", -"機械人"=>"机器人", -"機器人"=>"机器人", -"行動電話"=>"移动电话", -"流動電話"=>"移动电话", -"調制解調器"=>"调制解调器", -"數據機"=>"调制解调器", -"短訊"=>"短信", -"簡訊"=>"短信", -"烏茲別克"=>"乌兹别克斯坦", -"查德"=>"乍得", -"乍得"=>"乍得", -"也門"=>"", -"葉門"=>"也门", -"伯利茲"=>"伯利兹", -"貝里斯"=>"伯利兹", -"維德角"=>"佛得角", -"佛得角"=>"佛得角", -"克羅地亞"=>"克罗地亚", -"克羅埃西亞"=>"克罗地亚", -"岡比亞"=>"冈比亚", -"甘比亞"=>"冈比亚", -"幾內亞比紹"=>"几内亚比绍", -"幾內亞比索"=>"几内亚比绍", -"列支敦斯登"=>"列支敦士登", -"列支敦士登"=>"列支敦士登", -"利比里亞"=>"利比里亚", -"賴比瑞亞"=>"利比里亚", -"加納"=>"加纳", -"迦納"=>"加纳", -"加彭"=>"加蓬", -"加蓬"=>"加蓬", -"博茨瓦納"=>"博茨瓦纳", -"波札那"=>"博茨瓦纳", -"卡塔爾"=>"卡塔尔", -"卡達"=>"卡塔尔", -"盧旺達"=>"卢旺达", -"盧安達"=>"卢旺达", -"危地馬拉"=>"危地马拉", -"瓜地馬拉"=>"危地马拉", -"厄瓜多爾"=>"厄瓜多尔", -"厄瓜多"=>"厄瓜多尔", -"厄立特里亞"=>"厄立特里亚", -"厄利垂亞"=>"厄立特里亚", -"吉布堤"=>"吉布提", -"吉布地"=>"吉布提", -"哈薩克"=>"哈萨克斯坦", -"哥斯達黎加"=>"哥斯达黎加", -"哥斯大黎加"=>"哥斯达黎加", -"圖瓦盧"=>"图瓦卢", -"吐瓦魯"=>"图瓦卢", -"土庫曼"=>"土库曼斯坦", -"聖盧西亞"=>"圣卢西亚", -"聖露西亞"=>"圣卢西亚", -"聖吉斯納域斯"=>"圣基茨和尼维斯", -"聖克里斯多福及尼維斯"=>"圣基茨和尼维斯", -"聖文森特和格林納丁斯"=>"圣文森特和格林纳丁斯", -"聖文森及格瑞那丁"=>"圣文森特和格林纳丁斯", -"聖馬力諾"=>"圣马力诺", -"聖馬利諾"=>"圣马力诺", -"圭亞那"=>"圭亚那", -"蓋亞那"=>"圭亚那", -"坦桑尼亞"=>"坦桑尼亚", -"坦尚尼亞"=>"坦桑尼亚", -"埃塞俄比亞"=>"埃塞俄比亚", -"衣索比亞"=>"埃塞俄比亚", -"吉里巴斯"=>"基里巴斯", -"基里巴斯"=>"基里巴斯", -"塔吉克"=>"塔吉克斯坦", -"獅子山"=>"塞拉利昂", -"塞拉利昂"=>"塞拉利昂", -"塞普勒斯"=>"塞浦路斯", -"塞浦路斯"=>"塞浦路斯", -"塞舌爾"=>"塞舌尔", -"塞席爾"=>"塞舌尔", -"多明尼加共和國"=>"多米尼加", -"多明尼加"=>"多米尼加", -"多明尼加聯邦"=>"多米尼加联邦", -"多米尼克"=>"多米尼加联邦", -"安提瓜和巴布達"=>"安提瓜和巴布达", -"安地卡及巴布達"=>"安提瓜和巴布达", -"尼日利亞"=>"尼日利亚", -"奈及利亞"=>"尼日利亚", -"尼日爾"=>"尼日尔", -"尼日"=>"尼日尔", -"巴貝多"=>"巴巴多斯", -"巴巴多斯"=>"巴巴多斯", -"巴布亞新畿內亞"=>"巴布亚新几内亚", -"巴布亞紐幾內亞"=>"巴布亚新几内亚", -"布基納法索"=>"布基纳法索", -"布吉納法索"=>"布基纳法索", -"蒲隆地"=>"布隆迪", -"布隆迪"=>"布隆迪", -"希臘"=>"希腊", -"帛琉"=>"帕劳", -"義大利"=>"意大利", -"意大利"=>"意大利", -"所羅門群島"=>"所罗门群岛", -"索羅門群島"=>"所罗门群岛", -"汶萊"=>"文莱", -"斯威士蘭"=>"斯威士兰", -"史瓦濟蘭"=>"斯威士兰", -"斯洛文尼亞"=>"斯洛文尼亚", -"斯洛維尼亞"=>"斯洛文尼亚", -"新西蘭"=>"新西兰", -"紐西蘭"=>"新西兰", -"北韓"=>"朝鲜", -"格林納達"=>"格林纳达", -"格瑞那達"=>"格林纳达", -"格魯吉亞"=>"格鲁吉亚", -"喬治亞"=>"格鲁吉亚", -"梵蒂岡"=>"梵蒂冈", -"教廷"=>"梵蒂冈", -"毛里塔尼亞"=>"毛里塔尼亚", -"茅利塔尼亞"=>"毛里塔尼亚", -"毛里裘斯"=>"毛里求斯", -"模里西斯"=>"毛里求斯", -"沙地阿拉伯"=>"沙特阿拉伯", -"沙烏地阿拉伯"=>"沙特阿拉伯", -"波斯尼亞黑塞哥維那"=>"波斯尼亚和黑塞哥维那", -"波士尼亞赫塞哥維納"=>"波斯尼亚和黑塞哥维那", -"津巴布韋"=>"津巴布韦", -"辛巴威"=>"津巴布韦", -"宏都拉斯"=>"洪都拉斯", -"洪都拉斯"=>"洪都拉斯", -"特立尼達和多巴哥"=>"特立尼达和托巴哥", -"千里達托貝哥"=>"特立尼达和托巴哥", -"瑙魯"=>"瑙鲁", -"諾魯"=>"瑙鲁", -"瓦努阿圖"=>"瓦努阿图", -"萬那杜"=>"瓦努阿图", -"溫納圖"=>"瓦努阿图", -"科摩羅"=>"科摩罗", -"葛摩"=>"科摩罗", -"象牙海岸"=>"科特迪瓦", -"突尼西亞"=>"突尼斯", -"索馬里"=>"索马里", -"索馬利亞"=>"索马里", -"老撾"=>"老挝", -"寮國"=>"老挝", -"肯雅"=>"肯尼亚", -"肯亞"=>"肯尼亚", -"蘇利南"=>"苏里南", -"莫三比克"=>"莫桑比克", -"莫桑比克"=>"莫桑比克", -"萊索托"=>"莱索托", -"賴索托"=>"莱索托", -"貝寧"=>"贝宁", -"貝南"=>"贝宁", -"贊比亞"=>"赞比亚", -"尚比亞"=>"赞比亚", -"亞塞拜然"=>"阿塞拜疆", -"阿塞拜疆"=>"阿塞拜疆", -"阿拉伯聯合酋長國"=>"阿拉伯联合酋长国", -"阿拉伯聯合大公國"=>"阿拉伯联合酋长国", -"南韓"=>"韩国", -"馬爾代夫"=>"马尔代夫", -"馬爾地夫"=>"马尔代夫", -"馬爾他"=>"马耳他", -"馬里"=>"马里", -"馬利"=>"马里", -"即食麵"=>"方便面", -"快速面"=>"方便面", -"速食麵"=>"方便面", -"泡麵"=>"方便面", -"笨豬跳"=>"蹦极跳", -"绑紧跳"=>"蹦极跳", -"冷盤 "=>"凉菜", -"冷菜"=>"凉菜", -"散钱"=>"零钱", -"谐星"=>"笑星 ", -"夜学"=>"夜校", -"华乐"=>"民乐", -"中樂"=>"民乐", -"住屋"=>"住房", -"屋价"=>"房价", -"的士"=>"出租车", -"計程車"=>"出租车", -"巴士"=>"公共汽车", -"公車"=>"公共汽车", -"單車"=>"自行车", -"節慶"=>"节日", -"芝士"=>"乾酪", -"狗隻"=>"犬只", -"士多啤梨"=>"草莓", -"忌廉"=>"奶油", -"桌球"=>"台球", -"撞球"=>"台球", -"雪糕"=>"冰淇淋", -"衞生"=>"卫生", -"衛生"=>"卫生", -"賓士"=>"奔驰", -"平治"=>"奔驰", -"捷豹"=>"美洲虎", -"積架"=>"美洲虎", -"福斯"=>"大众", -"福士"=>"大众", -"雪鐵龍"=>"雪铁龙", -"萬事得"=>"马自达", -"馬自達"=>"马自达", -"寶獅"=>"标志", -"布殊"=>"布什", -"布希"=>"布什", -"柯林頓"=>"克林顿", -"克林頓"=>"克林顿", -"薩達姆"=>"萨达姆", -"海珊"=>"萨达姆", -"梵谷"=>"凡高", -"大衛碧咸"=>"大卫·贝克汉姆", -"米高奧雲"=>"迈克尔·欧文", -"卡佩雅蒂"=>"珍妮弗·卡普里亚蒂", -"沙芬"=>"马拉特·萨芬", -"舒麥加"=>"迈克尔·舒马赫", -"希特拉"=>"希特勒", -"戴安娜"=>"狄安娜", -"黛安娜"=>"狄安娜", -"希拉"=>"赫拉", ); -$zh2HK=array( +$zh2TW = array( +"缺省" => "預設", +"串行" => "串列", +"以太网" => "乙太網", +"位图" => "點陣圖", +"例程" => "常式", +"信道" => "通道", +"光标" => "游標", +"光盘" => "光碟", +"光驱" => "光碟機", +"全角" => "全形", +"加载" => "載入", +"半角" => "半形", +"变量" => "變數", +"噪声" => "雜訊", +"脱机" => "離線", +"声卡" => "音效卡", +"老字号" => "老字號", +"字号" => "字型大小", +"字库" => "字型檔", +"字段" => "欄位", +"字符" => "字元", +"存盘" => "存檔", +"寻址" => "定址", +"尾注" => "章節附註", +"异步" => "非同步", +"总线" => "匯流排", +"括号" => "括弧", +"接口" => "介面", +"控件" => "控制項", +"权限" => "許可權", +"盘片" => "碟片", +"硅片" => "矽片", +"硅谷" => "矽谷", +"硬盘" => "硬碟", +"磁盘" => "磁碟", +"磁道" => "磁軌", +"程控" => "程式控制", +"端口" => "埠", +"算子" => "運算元", +"算法" => "演算法", +"芯片" => "晶片", +"芯片" => "晶元", +"词组" => "片語", +"译码" => "解碼", +"软驱" => "軟碟機", +"快闪存储器" => "快閃記憶體", +"闪存" => "快閃記憶體", +"鼠标" => "滑鼠", +"进制" => "進位", +"交互式" => "互動式", +"仿真" => "模擬", +"优先级" => "優先順序", +"传感" => "感測", +"便携式" => "攜帶型", +"信息论" => "資訊理論", +"写保护" => "防寫", +"分布式" => "分散式", +"分辨率" => "解析度", +"服务器" => "伺服器", +"等于" => "等於", +"局域网" => "區域網", +"计算机" => "電腦", +"扫瞄仪" => "掃瞄器", +"宽带" => "寬頻", +"数据库" => "資料庫", +"奶酪" => "乳酪", +"巨商" => "鉅賈", +"手电" => "手電筒", +"万历" => "萬曆", +"永历" => "永曆", +"词汇" => "辭彙", +"习用" => "慣用", +"元音" => "母音", +"任意球" => "自由球", +"头球" => "頭槌", +"入球" => "進球", +"粒入球" => "顆進球", +"打门" => "射門", +"火锅盖帽" => "蓋火鍋", +"打印机" => "印表機", +"打印機" => "印表機", +"字节" => "位元組", +"字節" => "位元組", +"打印" => "列印", +"打印" => "列印", +"硬件" => "硬體", +"硬件" => "硬體", +"二极管" => "二極體", +"二極管" => "二極體", +"三极管" => "三極體", +"三極管" => "三極體", +"软件" => "軟體", +"軟件" => "軟體", +"网络" => "網路", +"網絡" => "網路", +"人工智能" => "人工智慧", +"航天飞机" => "太空梭", +"穿梭機" => "太空梭", +"因特网" => "網際網路", +"互聯網" => "網際網路", +"机器人" => "機器人", +"機械人" => "機器人", +"移动电话" => "行動電話", +"流動電話" => "行動電話", +"调制解调器" => "數據機", +"調制解調器" => "數據機", +"短信" => "簡訊", +"短訊" => "簡訊", +"乌兹别克斯坦" => "烏茲別克", +"乍得" => "查德", +"乍得" => "查德", +"也门" => "葉門", +"也門" => "葉門", +"伯利兹" => "貝里斯", +"伯利茲" => "貝里斯", +"佛得角" => "維德角", +"佛得角" => "維德角", +"克罗地亚" => "克羅埃西亞", +"克羅地亞" => "克羅埃西亞", +"冈比亚" => "甘比亞", +"岡比亞" => "甘比亞", +"几内亚比绍" => "幾內亞比索", +"幾內亞比紹" => "幾內亞比索", +"列支敦士登" => "列支敦斯登", +"列支敦士登" => "列支敦斯登", +"利比里亚" => "賴比瑞亞", +"利比里亞" => "賴比瑞亞", +"加纳" => "迦納", +"加納" => "迦納", +"加蓬" => "加彭", +"加蓬" => "加彭", +"博茨瓦纳" => "波札那", +"博茨瓦納" => "波札那", +"卡塔尔" => "卡達", +"卡塔爾" => "卡達", +"卢旺达" => "盧安達", +"盧旺達" => "盧安達", +"危地马拉" => "瓜地馬拉", +"危地馬拉" => "瓜地馬拉", +"厄瓜多尔" => "厄瓜多", +"厄瓜多爾" => "厄瓜多", +"厄立特里亚" => "厄利垂亞", +"厄立特里亞" => "厄利垂亞", +"吉布提" => "吉布地", +"吉布堤" => "吉布地", +"哈萨克斯坦" => "哈薩克", +"哥斯达黎加" => "哥斯大黎加", +"哥斯達黎加" => "哥斯大黎加", +"图瓦卢" => "吐瓦魯", +"圖瓦盧" => "吐瓦魯", +"土库曼斯坦" => "土庫曼", +"圣卢西亚" => "聖露西亞", +"聖盧西亞" => "聖露西亞", +"圣基茨和尼维斯" => "聖克里斯多福及尼維斯", +"聖吉斯納域斯" => "聖克里斯多福及尼維斯", +"圣文森特和格林纳丁斯" => "聖文森及格瑞那丁", +"聖文森特和格林納丁斯" => "聖文森及格瑞那丁", +"圣马力诺" => "聖馬利諾", +"聖馬力諾" => "聖馬利諾", +"圭亚那" => "蓋亞那", +"圭亞那" => "蓋亞那", +"坦桑尼亚" => "坦尚尼亞", +"坦桑尼亞" => "坦尚尼亞", +"埃塞俄比亚" => "衣索比亞", +"埃塞俄比亞" => "衣索比亞", +"基里巴斯" => "吉里巴斯", +"基里巴斯" => "吉里巴斯", +"塔吉克斯坦" => "塔吉克", +"塞拉利昂" => "獅子山", +"塞拉利昂" => "獅子山", +"塞浦路斯" => "塞普勒斯", +"塞浦路斯" => "塞普勒斯", +"塞舌尔" => "塞席爾", +"塞舌爾" => "塞席爾", +"多米尼加" => "多明尼加", +"多明尼加共和國" => "多明尼加", +"多米尼加联邦" => "多米尼克", +"多明尼加聯邦" => "多米尼克", +"安提瓜和巴布达" => "安地卡及巴布達", +"安提瓜和巴布達" => "安地卡及巴布達", +"尼日利亚" => "奈及利亞", +"尼日利亞" => "奈及利亞", +"尼日尔" => "尼日", +"尼日爾" => "尼日", +"巴巴多斯" => "巴貝多", +"巴巴多斯" => "巴貝多", +"巴布亚新几内亚" => "巴布亞紐幾內亞", +"巴布亞新畿內亞" => "巴布亞紐幾內亞", +"布基纳法索" => "布吉納法索", +"布基納法索" => "布吉納法索", +"布隆迪" => "蒲隆地", +"布隆迪" => "蒲隆地", +"希腊" => "希臘", +"帕劳" => "帛琉", +"意大利" => "義大利", +"意大利" => "義大利", +"所罗门群岛" => "索羅門群島", +"所羅門群島" => "索羅門群島", +"文莱" => "汶萊", +"斯威士兰" => "史瓦濟蘭", +"斯威士蘭" => "史瓦濟蘭", +"斯洛文尼亚" => "斯洛維尼亞", +"斯洛文尼亞" => "斯洛維尼亞", +"新西兰" => "紐西蘭", +"新西蘭" => "紐西蘭", +"格林纳达" => "格瑞那達", +"格林納達" => "格瑞那達", +"格鲁吉亚" => "喬治亞", +"格魯吉亞" => "喬治亞", +"佐治亚" => "喬治亞", +"佐治亞" => "喬治亞", +"毛里塔尼亚" => "茅利塔尼亞", +"毛里塔尼亞" => "茅利塔尼亞", +"毛里求斯" => "模里西斯", +"毛里裘斯" => "模里西斯", +"沙特阿拉伯" => "沙烏地阿拉伯", +"沙地阿拉伯" => "沙烏地阿拉伯", +"波斯尼亚和黑塞哥维那" => "波士尼亞赫塞哥維納", +"波斯尼亞黑塞哥維那" => "波士尼亞赫塞哥維納", +"津巴布韦" => "辛巴威", +"津巴布韋" => "辛巴威", +"洪都拉斯" => "宏都拉斯", +"洪都拉斯" => "宏都拉斯", +"特立尼达和托巴哥" => "千里達托貝哥", +"特立尼達和多巴哥" => "千里達托貝哥", +"瑙鲁" => "諾魯", +"瑙魯" => "諾魯", +"瓦努阿图" => "萬那杜", +"瓦努阿圖" => "萬那杜", +"溫納圖萬" => "那杜", +"科摩罗" => "葛摩", +"科摩羅" => "葛摩", +"科特迪瓦" => "象牙海岸", +"突尼斯" => "突尼西亞", +"索马里" => "索馬利亞", +"索馬里" => "索馬利亞", +"老挝" => "寮國", +"老撾" => "寮國", +"肯尼亚" => "肯亞", +"肯雅" => "肯亞", +"苏里南" => "蘇利南", +"莫桑比克" => "莫三比克", +"莱索托" => "賴索托", +"萊索托" => "賴索托", +"贝宁" => "貝南", +"貝寧" => "貝南", +"赞比亚" => "尚比亞", +"贊比亞" => "尚比亞", +"阿塞拜疆" => "亞塞拜然", +"阿塞拜疆" => "亞塞拜然", +"阿拉伯联合酋长国" => "阿拉伯聯合大公國", +"阿拉伯聯合酋長國" => "阿拉伯聯合大公國", +"马尔代夫" => "馬爾地夫", +"馬爾代夫" => "馬爾地夫", +"马耳他" => "馬爾他", +"马里共和国" => "馬利共和國", +"馬里共和國" => "馬利共和國", +"方便面" => "速食麵", +"快速面" => "速食麵", +"即食麵" => "速食麵", +"薯仔" => "土豆", +"蹦极跳" => "笨豬跳", +"绑紧跳" => "笨豬跳", +"冷菜" => "冷盤", +"凉菜" => "冷盤", +"出租车" => "計程車", +"台球" => "撞球", +"桌球" => "撞球", +"雪糕" => "冰淇淋", +"卫生" => "衛生", +"衞生" => "衛生", +"平治" => "賓士", +"奔驰" => "賓士", +"積架" => "捷豹", +"福士" => "福斯", +"雪铁龙" => "雪鐵龍", +"马自达" => "馬自達", +"萬事得" => "馬自達", +"拿破仑" => "拿破崙", +"拿破侖" => "拿破崙", +"布什" => "布希", +"布殊" => "布希", +"克林顿" => "柯林頓", +"克林頓" => "柯林頓", +"侯赛因" => "海珊", +"侯賽因" => "海珊", +"凡高" => "梵谷", +"狄安娜" => "黛安娜", +"戴安娜" => "黛安娜", +"赫拉" => "希拉", +); + +$zh2HK = array( "打印机" => "打印機", "印表機" => "打印機", -"字节" => "字節", -"位元組" => "字節", +"字节" => "位元組", +"字節" => "位元組", "打印" => "打印", "列印" => "打印", "硬件" => "硬件", @@ -8308,10 +7830,11 @@ $zh2HK=array( "坦桑尼亚" => "坦桑尼亞", "坦尚尼亞" => "坦桑尼亞", "埃塞俄比亚" => "埃塞俄比亞", +"衣索匹亞" => "埃塞俄比亞", "衣索比亞" => "埃塞俄比亞", "基里巴斯" => "基里巴斯", "吉里巴斯" => "基里巴斯", -"獅子山" => "塞拉利昂", +"狮子山" => "獅子山", "塞普勒斯" => "塞浦路斯", "塞舌尔" => "塞舌爾", "塞席爾" => "塞舌爾", @@ -8344,16 +7867,14 @@ $zh2HK=array( "紐西蘭" => "新西蘭", "格林纳达" => "格林納達", "格瑞那達" => "格林納達", -"格鲁吉亚" => "格魯吉亞", -"喬治亞" => "格魯吉亞", +"格鲁吉亚" => "喬治亞", +"格魯吉亞" => "喬治亞", "梵蒂冈" => "梵蒂岡", -"教廷" => "梵蒂岡", "毛里塔尼亚" => "毛里塔尼亞", "茅利塔尼亞" => "毛里塔尼亞", "毛里求斯" => "毛里裘斯", "模里西斯" => "毛里裘斯", -"沙特阿拉伯" => "沙地阿拉伯", -"沙烏地阿拉伯" => "沙地阿拉伯", +"沙烏地阿拉伯" => "沙特阿拉伯", "波斯尼亚和黑塞哥维那" => "波斯尼亞黑塞哥維那", "波士尼亞赫塞哥維納" => "波斯尼亞黑塞哥維那", "津巴布韦" => "津巴布韋", @@ -8388,29 +7909,22 @@ $zh2HK=array( "阿拉伯聯合大公國" => "阿拉伯聯合酋長國", "马尔代夫" => "馬爾代夫", "馬爾地夫" => "馬爾代夫", -"马里" => "馬里", -"馬利" => "馬里", +"馬利共和國" => "馬里共和國", "方便面" => "即食麵", "快速面" => "即食麵", "速食麵" => "即食麵", "泡麵" => "即食麵", -"土豆" => "薯仔", +"土豆" => "馬鈴薯", "华乐" => "中樂", "民乐" => "中樂", -"計程車 " => "的士", +"計程車" => "的士", "出租车" => "的士", "公車" => "巴士", -"公共汽车" => "巴士", "自行车" => "單車", -"节日" => "節慶", "犬只" => "狗隻", "台球" => "桌球", "撞球" => "桌球", "冰淇淋" => "雪糕", -"冰淇淋" => "雪糕", -"卫生" => "衞生", -"衛生" => "衞生", -"老人" => "長者", "賓士" => "平治", "捷豹" => "積架", "福斯" => "福士", @@ -8420,12 +7934,14 @@ $zh2HK=array( "马自达" => "萬事得", "馬自達" => "萬事得", "寶獅" => "標致", +"拿破崙" => "拿破侖", "布什" => "布殊", "布希" => "布殊", "克林顿" => "克林頓", "柯林頓" => "克林頓", "萨达姆" => "薩達姆", -"海珊" => "薩達姆", +"海珊" => "侯賽因", +"侯赛因" => "侯賽因", "大卫·贝克汉姆" => "大衛碧咸", "迈克尔·欧文" => "米高奧雲", "珍妮弗·卡普里亚蒂" => "卡佩雅蒂", @@ -8436,7 +7952,318 @@ $zh2HK=array( "黛安娜" => "戴安娜", ); -$zh2SG=array( +$zh2CN = array( +"記憶體" => "内存", +"預設" => "默认", +"串列" => "串行", +"乙太網" => "以太网", +"點陣圖" => "位图", +"常式" => "例程", +"游標" => "光标", +"光碟" => "光盘", +"光碟機" => "光驱", +"全形" => "全角", +"共用" => "共享", +"載入" => "加载", +"半形" => "半角", +"變數" => "变量", +"雜訊" => "噪声", +"因數" => "因子", +"功能變數名稱" => "域名", +"音效卡" => "声卡", +"字型大小" => "字号", +"字型檔" => "字库", +"欄位" => "字段", +"字元" => "字符", +"存檔" => "存盘", +"定址" => "寻址", +"章節附註" => "尾注", +"非同步" => "异步", +"匯流排" => "总线", +"括弧" => "括号", +"介面" => "接口", +"控制項" => "控件", +"許可權" => "权限", +"碟片" => "盘片", +"矽片" => "硅片", +"矽谷" => "硅谷", +"硬碟" => "硬盘", +"磁碟" => "磁盘", +"磁軌" => "磁道", +"程式控制" => "程控", +"運算元" => "算子", +"演算法" => "算法", +"晶片" => "芯片", +"晶元" => "芯片", +"片語" => "词组", +"軟碟機" => "软驱", +"快閃記憶體" => "快闪存储器", +"滑鼠" => "鼠标", +"進位" => "进制", +"互動式" => "交互式", +"優先順序" => "优先级", +"感測" => "传感", +"攜帶型" => "便携式", +"資訊理論" => "信息论", +"迴圈" => "循环", +"防寫" => "写保护", +"分散式" => "分布式", +"解析度" => "分辨率", +"伺服器" => "服务器", +"等於" => "等于", +"區域網" => "局域网", +"巨集" => "宏", +"掃瞄器" => "扫瞄仪", +"寬頻" => "宽带", +"資料庫" => "数据库", +"乳酪" => "奶酪", +"鉅賈" => "巨商", +"手電筒" => "手电", +"萬曆" => "万历", +"永曆" => "永历", +"辭彙" => "词汇", +"母音" => "元音", +"自由球" => "任意球", +"頭槌" => "头球", +"進球" => "入球", +"顆進球" => "粒入球", +"射門" => "打门", +"蓋火鍋" => "火锅盖帽", +"印表機" => "打印机", +"打印機" => "打印机", +"位元組" => "字节", +"字節" => "字节", +"列印" => "打印", +"打印" => "打印", +"硬體" => "硬件", +"二極體" => "二极管", +"二極管" => "二极管", +"三極體" => "三极管", +"三極管" => "三极管", +"數位" => "数码", +"數碼" => "数码", +"軟體" => "软件", +"軟件" => "软件", +"網路" => "网络", +"網絡" => "网络", +"人工智慧" => "人工智能", +"太空梭" => "航天飞机", +"穿梭機" => "航天飞机", +"網際網路" => "因特网", +"互聯網" => "因特网", +"機械人" => "机器人", +"機器人" => "机器人", +"行動電話" => "移动电话", +"流動電話" => "移动电话", +"調制解調器" => "调制解调器", +"數據機" => "调制解调器", +"短訊" => "短信", +"簡訊" => "短信", +"烏茲別克" => "乌兹别克斯坦", +"查德" => "乍得", +"乍得" => "乍得", +"也門" => "", +"葉門" => "也门", +"伯利茲" => "伯利兹", +"貝里斯" => "伯利兹", +"維德角" => "佛得角", +"佛得角" => "佛得角", +"克羅地亞" => "克罗地亚", +"克羅埃西亞" => "克罗地亚", +"岡比亞" => "冈比亚", +"甘比亞" => "冈比亚", +"幾內亞比紹" => "几内亚比绍", +"幾內亞比索" => "几内亚比绍", +"列支敦斯登" => "列支敦士登", +"列支敦士登" => "列支敦士登", +"利比里亞" => "利比里亚", +"賴比瑞亞" => "利比里亚", +"加納" => "加纳", +"迦納" => "加纳", +"加彭" => "加蓬", +"加蓬" => "加蓬", +"博茨瓦納" => "博茨瓦纳", +"波札那" => "博茨瓦纳", +"卡塔爾" => "卡塔尔", +"卡達" => "卡塔尔", +"盧旺達" => "卢旺达", +"盧安達" => "卢旺达", +"危地馬拉" => "危地马拉", +"瓜地馬拉" => "危地马拉", +"厄瓜多爾" => "厄瓜多尔", +"厄瓜多" => "厄瓜多尔", +"厄立特里亞" => "厄立特里亚", +"厄利垂亞" => "厄立特里亚", +"吉布堤" => "吉布提", +"吉布地" => "吉布提", +"哈薩克" => "哈萨克斯坦", +"哥斯達黎加" => "哥斯达黎加", +"哥斯大黎加" => "哥斯达黎加", +"圖瓦盧" => "图瓦卢", +"吐瓦魯" => "图瓦卢", +"土庫曼" => "土库曼斯坦", +"聖盧西亞" => "圣卢西亚", +"聖露西亞" => "圣卢西亚", +"聖吉斯納域斯" => "圣基茨和尼维斯", +"聖克里斯多福及尼維斯" => "圣基茨和尼维斯", +"聖文森特和格林納丁斯" => "圣文森特和格林纳丁斯", +"聖文森及格瑞那丁" => "圣文森特和格林纳丁斯", +"聖馬力諾" => "圣马力诺", +"聖馬利諾" => "圣马力诺", +"圭亞那" => "圭亚那", +"蓋亞那" => "圭亚那", +"坦桑尼亞" => "坦桑尼亚", +"坦尚尼亞" => "坦桑尼亚", +"埃塞俄比亞" => "埃塞俄比亚", +"衣索匹亞" => "埃塞俄比亚", +"衣索比亞" => "埃塞俄比亚", +"吉里巴斯" => "基里巴斯", +"基里巴斯" => "基里巴斯", +"塔吉克" => "塔吉克斯坦", +"塞拉利昂" => "塞拉利昂", +"塞普勒斯" => "塞浦路斯", +"塞浦路斯" => "塞浦路斯", +"塞舌爾" => "塞舌尔", +"塞席爾" => "塞舌尔", +"多明尼加共和國" => "多米尼加", +"多明尼加" => "多米尼加", +"多明尼加聯邦" => "多米尼加联邦", +"多米尼克" => "多米尼加联邦", +"安提瓜和巴布達" => "安提瓜和巴布达", +"安地卡及巴布達" => "安提瓜和巴布达", +"尼日利亞" => "尼日利亚", +"奈及利亞" => "尼日利亚", +"尼日爾" => "尼日尔", +"尼日" => "尼日尔", +"巴貝多" => "巴巴多斯", +"巴巴多斯" => "巴巴多斯", +"巴布亞新畿內亞" => "巴布亚新几内亚", +"巴布亞紐幾內亞" => "巴布亚新几内亚", +"布基納法索" => "布基纳法索", +"布吉納法索" => "布基纳法索", +"蒲隆地" => "布隆迪", +"布隆迪" => "布隆迪", +"希臘" => "希腊", +"帛琉" => "帕劳", +"義大利" => "意大利", +"意大利" => "意大利", +"所羅門群島" => "所罗门群岛", +"索羅門群島" => "所罗门群岛", +"汶萊" => "文莱", +"斯威士蘭" => "斯威士兰", +"史瓦濟蘭" => "斯威士兰", +"斯洛文尼亞" => "斯洛文尼亚", +"斯洛維尼亞" => "斯洛文尼亚", +"新西蘭" => "新西兰", +"紐西蘭" => "新西兰", +"格林納達" => "格林纳达", +"格瑞那達" => "格林纳达", +"格魯吉亞" => "乔治亚", +"喬治亞" => "乔治亚", +"梵蒂岡" => "梵蒂冈", +"毛里塔尼亞" => "毛里塔尼亚", +"茅利塔尼亞" => "毛里塔尼亚", +"毛里裘斯" => "毛里求斯", +"模里西斯" => "毛里求斯", +"沙地阿拉伯" => "沙特阿拉伯", +"沙烏地阿拉伯" => "沙特阿拉伯", +"波斯尼亞黑塞哥維那" => "波斯尼亚和黑塞哥维那", +"波士尼亞赫塞哥維納" => "波斯尼亚和黑塞哥维那", +"津巴布韋" => "津巴布韦", +"辛巴威" => "津巴布韦", +"宏都拉斯" => "洪都拉斯", +"洪都拉斯" => "洪都拉斯", +"特立尼達和多巴哥" => "特立尼达和托巴哥", +"千里達托貝哥" => "特立尼达和托巴哥", +"瑙魯" => "瑙鲁", +"諾魯" => "瑙鲁", +"瓦努阿圖" => "瓦努阿图", +"萬那杜" => "瓦努阿图", +"溫納圖" => "瓦努阿图", +"科摩羅" => "科摩罗", +"葛摩" => "科摩罗", +"象牙海岸" => "科特迪瓦", +"突尼西亞" => "突尼斯", +"索馬里" => "索马里", +"索馬利亞" => "索马里", +"老撾" => "老挝", +"寮國" => "老挝", +"肯雅" => "肯尼亚", +"肯亞" => "肯尼亚", +"蘇利南" => "苏里南", +"莫三比克" => "莫桑比克", +"莫桑比克" => "莫桑比克", +"萊索托" => "莱索托", +"賴索托" => "莱索托", +"貝寧" => "贝宁", +"貝南" => "贝宁", +"贊比亞" => "赞比亚", +"尚比亞" => "赞比亚", +"亞塞拜然" => "阿塞拜疆", +"阿塞拜疆" => "阿塞拜疆", +"阿拉伯聯合酋長國" => "阿拉伯联合酋长国", +"阿拉伯聯合大公國" => "阿拉伯联合酋长国", +"南韓" => "韩国", +"馬爾代夫" => "马尔代夫", +"馬爾地夫" => "马尔代夫", +"馬爾他" => "马耳他", +"馬利共和國" => "马里共和国", +"即食麵" => "方便面", +"快速面" => "方便面", +"速食麵" => "方便面", +"泡麵" => "方便面", +"笨豬跳" => "蹦极跳", +"绑紧跳" => "蹦极跳", +"冷盤 " => "凉菜", +"冷菜" => "凉菜", +"散钱" => "零钱", +"谐星" => "笑星 ", +"夜学" => "夜校", +"华乐" => "民乐", +"中樂" => "民乐", +"屋价" => "房价", +"的士" => "出租车", +"計程車" => "出租车", +"公車" => "公共汽车", +"單車" => "自行车", +"節慶" => "节日", +"芝士" => "乾酪", +"狗隻" => "犬只", +"士多啤梨" => "草莓", +"忌廉" => "奶油", +"桌球" => "台球", +"撞球" => "台球", +"雪糕" => "冰淇淋", +"衞生" => "卫生", +"衛生" => "卫生", +"賓士" => "奔驰", +"平治" => "奔驰", +"積架" => "捷豹", +"福斯" => "大众", +"福士" => "大众", +"雪鐵龍" => "雪铁龙", +"萬事得" => "马自达", +"馬自達" => "马自达", +"寶獅" => "标志", +"拿破崙" => "拿破仑", +"布殊" => "布什", +"布希" => "布什", +"柯林頓" => "克林顿", +"克林頓" => "克林顿", +"薩達姆" => "萨达姆", +"海珊" => "萨达姆", +"梵谷" => "凡高", +"大衛碧咸" => "大卫·贝克汉姆", +"米高奧雲" => "迈克尔·欧文", +"卡佩雅蒂" => "珍妮弗·卡普里亚蒂", +"沙芬" => "马拉特·萨芬", +"舒麥加" => "迈克尔·舒马赫", +"希特拉" => "希特勒", +"黛安娜" => "戴安娜", +"希拉" => "赫拉", +); + +$zh2SG = array( "方便面" => "快速面", "速食麵" => "快速面", "即食麵" => "快速面", @@ -8452,4 +8279,4 @@ $zh2SG=array( "住房" => "住屋", "房价" => "屋价", "泡麵" => "快速面", -); +);
\ No newline at end of file diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index b324c52f..3a7b5099 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -63,12 +63,36 @@ abstract class ApiBase { $this->mModulePrefix = $modulePrefix; } - /** - * Executes this module + /***************************************************************************** + * ABSTRACT METHODS * + *****************************************************************************/ + + /** + * Evaluates the parameters, performs the requested query, and sets up the + * result. Concrete implementations of ApiBase must override this method to + * provide whatever functionality their module offers. Implementations must + * not produce any output on their own and are not expected to handle any + * errors. + * + * The execute method will be invoked directly by ApiMain immediately before + * the result of the module is output. Aside from the constructor, implementations + * should assume that no other methods will be called externally on the module + * before the result is processed. + * + * The result data should be stored in the result object referred to by + * "getResult()". Refer to ApiResult.php for details on populating a result + * object. */ public abstract function execute(); /** + * Returns a String that identifies the version of the extending class. Typically + * includes the class name, the svn revision, timestamp, and last author. May + * be severely incorrect in many implementations! + */ + public abstract function getVersion(); + + /** * Get the name of the module being executed by this instance */ public function getModuleName() { @@ -100,14 +124,16 @@ abstract class ApiBase { } /** - * If this module's $this is the same as $this->mMainModule, its the root, otherwise no + * Returns true if this module is the main module ($this === $this->mMainModule), + * false otherwise. */ public function isMain() { return $this === $this->mMainModule; } /** - * Get result object + * Get the result object. Please refer to the documentation in ApiResult.php + * for details on populating and accessing data in a result object. */ public function getResult() { // Main module has getResult() method overriden @@ -125,7 +151,8 @@ abstract class ApiBase { } /** - * Set warning section for this module. Users should monitor this section to notice any changes in API. + * Set warning section for this module. Users should monitor this section to + * notice any changes in API. */ public function setWarning($warning) { $msg = array(); @@ -196,6 +223,10 @@ abstract class ApiBase { return $msg; } + /** + * Generates the parameter descriptions for this module, to be displayed in the + * module's help. + */ public function makeHelpMsgParameters() { $params = $this->getAllowedParams(); if ($params !== false) { @@ -208,7 +239,7 @@ abstract class ApiBase { if (is_array($desc)) $desc = implode($paramPrefix, $desc); - @ $type = $paramSettings[self :: PARAM_TYPE]; + $type = $paramSettings[self :: PARAM_TYPE]; if (isset ($type)) { if (isset ($paramSettings[self :: PARAM_ISMULTI])) $prompt = 'Values (separate with \'|\'): '; @@ -303,13 +334,15 @@ abstract class ApiBase { * Using getAllowedParams(), makes an array of the values provided by the user, * with key being the name of the variable, and value - validated value from user or default. * This method can be used to generate local variables using extract(). + * limit=max will not be parsed if $parseMaxLimit is set to false; use this + * when the max limit is not definite, e.g. when getting revisions. */ - public function extractRequestParams() { + public function extractRequestParams($parseMaxLimit = true) { $params = $this->getAllowedParams(); $results = array (); foreach ($params as $paramName => $paramSettings) - $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings); + $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit); return $results; } @@ -323,6 +356,10 @@ abstract class ApiBase { return $this->getParameterFromSettings($paramName, $paramSettings); } + /** + * Returns an array of the namespaces (by integer id) that exist on the + * wiki. Used primarily in help documentation. + */ public static function getValidNamespaces() { static $mValidNamespaces = null; if (is_null($mValidNamespaces)) { @@ -339,10 +376,12 @@ abstract class ApiBase { /** * Using the settings determine the value for the given parameter + * * @param $paramName String: parameter name * @param $paramSettings Mixed: default value or an array of settings using PARAM_* constants. + * @param $parseMaxLimit Boolean: parse limit when max is given? */ - protected function getParameterFromSettings($paramName, $paramSettings) { + protected function getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit) { // Some classes may decide to change parameter names $encParamName = $this->encodeParamName($paramName); @@ -410,8 +449,17 @@ abstract class ApiBase { if ($multi) ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName"); $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; - $value = intval($value); - $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]); + if( $value == 'max' ) { + if( $parseMaxLimit ) { + $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self :: PARAM_MAX2] : $paramSettings[self :: PARAM_MAX]; + $this->getResult()->addValue( 'limits', $this->getModuleName(), $value ); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]); + } + } + else { + $value = intval($value); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]); + } break; case 'boolean' : if ($multi) @@ -485,7 +533,7 @@ abstract class ApiBase { // Optimization: do not check user's bot status unless really needed -- skips db query // assumes $botMax >= $max if (!is_null($max) && $value > $max) { - if (!is_null($botMax) && ($this->getMain()->isBot() || $this->getMain()->isSysop())) { + if (!is_null($botMax) && $this->getMain()->canApiHighLimits()) { if ($value > $botMax) { $this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName); } @@ -501,6 +549,90 @@ abstract class ApiBase { public function dieUsage($description, $errorCode, $httpRespCode = 0) { throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode); } + + /** + * Array that maps message keys to error messages. $1 and friends are replaced. + */ + public static $messageMap = array( + // This one MUST be present, or dieUsageMsg() will recurse infinitely + 'unknownerror' => array('code' => 'unknownerror', 'info' => "Unknown error: ``\$1''"), + 'unknownerror-nocode' => array('code' => 'unknownerror', 'info' => 'Unknown error'), + + // Messages from Title::getUserPermissionsErrors() + 'ns-specialprotected' => array('code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited"), + 'protectedinterface' => array('code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages"), + 'namespaceprotected' => array('code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the ``\$1'' namespace"), + 'customcssjsprotected' => array('code' => 'customcssjsprotected', 'info' => "You're not allowed to edit custom CSS and JavaScript pages"), + 'cascadeprotected' => array('code' => 'cascadeprotected', 'info' =>"The page you're trying to edit is protected because it's included in a cascade-protected page"), + 'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"), + 'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"), + 'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message + 'badaccess-group1' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Can't use the parameter 'cause it's wikilinked + 'badaccess-group2' => array('code' => 'permissiondenied', 'info' => "Permission denied"), + 'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"), + 'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"), + 'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"), + 'nocreatetext' => array('code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages"), + 'movenologintext' => array('code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages"), + 'movenotallowed' => array('code' => 'cantmove', 'info' => "You don't have permission to move pages"), + 'confirmedittext' => array('code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit"), + 'blockedtext' => array('code' => 'blocked', 'info' => "You have been blocked from editing"), + 'autoblockedtext' => array('code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"), + + // Miscellaneous interface messages + 'actionthrottledtext' => array('code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again"), + 'alreadyrolled' => array('code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back"), + 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"), + 'readonlytext' => array('code' => 'readonly', 'info' => "The wiki is currently in read-only mode"), + 'sessionfailure' => array('code' => 'badtoken', 'info' => "Invalid token"), + 'cannotdelete' => array('code' => 'cantdelete', 'info' => "Couldn't delete ``\$1''. Maybe it was deleted already by someone else"), + 'notanarticle' => array('code' => 'missingtitle', 'info' => "The page you requested doesn't exist"), + 'selfmove' => array('code' => 'selfmove', 'info' => "Can't move a page to itself"), + 'immobile_namespace' => array('code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving"), + 'articleexists' => array('code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article"), + 'protectedpage' => array('code' => 'protectedpage', 'info' => "You don't have permission to perform this move"), + 'hookaborted' => array('code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook"), + 'cantmove-titleprotected' => array('code' => 'protectedtitle', 'info' => "The destination article has been protected from creation"), + // 'badarticleerror' => shouldn't happen + // 'badtitletext' => shouldn't happen + 'ip_range_invalid' => array('code' => 'invalidrange', 'info' => "Invalid IP range"), + 'range_block_disabled' => array('code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled"), + 'nosuchusershort' => array('code' => 'nosuchuser', 'info' => "The user you specified doesn't exist"), + 'badipaddress' => array('code' => 'invalidip', 'info' => "Invalid IP address specified"), + 'ipb_expiry_invalid' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"), + 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"), + 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."), + 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"), + + // API-specific messages + 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"), + 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"), + 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"), + 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"), + 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time is in the past"), + 'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"), + 'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"), + 'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"), + 'canthide' => array('code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log"), + 'cantblock-email' => array('code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki"), + 'unblock-notarget' => array('code' => 'notarget', 'info' => "Either the id or the user parameter must be set"), + 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can\'t be used together"), + 'cantunblock' => array('code' => 'permissiondenied', 'info' => "You don't have permission to unblock users"), + 'cannotundelete' => array('code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"), + 'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"), + ); + + /** + * Output the error message related to a certain array + * @param array $error Element of a getUserPermissionsErrors() + */ + public function dieUsageMsg($error) { + $key = array_shift($error); + if(isset(self::$messageMap[$key])) + $this->dieUsage(wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error), wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error)); + // If the key isn't present, throw an "unknown error" + $this->dieUsageMsg(array('unknownerror', $key)); + } /** * Internal code errors should be reported with this method @@ -510,6 +642,28 @@ abstract class ApiBase { } /** + * Indicates if API needs to check maxlag + */ + public function shouldCheckMaxlag() { + return true; + } + + /** + * Indicates if this module requires edit mode + */ + public function isEditMode() { + return false; + } + + /** + * Indicates whether this module must be called with a POST request + */ + public function mustBePosted() { + return false; + } + + + /** * Profiling: total module execution time */ private $mTimeIn = 0, $mModuleTime = 0; @@ -610,10 +764,12 @@ abstract class ApiBase { print "\n</pre>\n"; } - public abstract function getVersion(); + /** + * Returns a String that identifies the version of this class. + */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiBase.php 24934 2007-08-20 08:04:12Z nickj $'; - } + return __CLASS__ . ': $Id: ApiBase.php 31259 2008-02-25 14:14:55Z catrope $'; + } } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php new file mode 100644 index 00000000..e5c238ae --- /dev/null +++ b/includes/api/ApiBlock.php @@ -0,0 +1,164 @@ +<?php + +/* + * Created on Sep 4, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** +* API module that facilitates the blocking of users. Requires API write mode +* to be enabled. +* + * @addtogroup API + */ +class ApiBlock extends ApiBase { + + /** + * Std ctor. + */ + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Blocks the user specified in the parameters for the given expiry, with the + * given reason, and with all other settings provided in the params. If the block + * succeeds, produces a result containing the details of the block and notice + * of success. If it fails, the result will specify the nature of the error. + */ + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + if($params['gettoken']) + { + $res['blocktoken'] = $wgUser->editToken(); + $this->getResult()->addValue(null, $this->getModuleName(), $res); + return; + } + + if(is_null($params['user'])) + $this->dieUsageMsg(array('missingparam', 'user')); + if(is_null($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + if(!$wgUser->isAllowed('block')) + $this->dieUsageMsg(array('cantblock')); + if($params['hidename'] && !$wgUser->isAllowed('hideuser')) + $this->dieUsageMsg(array('canthide')); + if($params['noemail'] && !$wgUser->isAllowed('blockemail')) + $this->dieUsageMsg(array('cantblock-email')); + if(wfReadOnly()) + $this->dieUsageMsg(array('readonlytext')); + + $form = new IPBlockForm(''); + $form->BlockAddress = $params['user']; + $form->BlockReason = (is_null($params['reason']) ? '' : $params['reason']); + $form->BlockReasonList = 'other'; + $form->BlockExpiry = ($params['expiry'] == 'never' ? 'infinite' : $params['expiry']); + $form->BlockOther = ''; + $form->BlockAnonOnly = $params['anononly']; + $form->BlockCreateAccount = $params['nocreate']; + $form->BlockEnableAutoBlock = $params['autoblock']; + $form->BlockEmail = $params['noemail']; + $form->BlockHideName = $params['hidename']; + + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $retval = $form->doBlock($userID, $expiry); + if(!empty($retval)) + // We don't care about multiple errors, just report one of them + $this->dieUsageMsg($retval); + + $dbw->commit(); + $res['user'] = $params['user']; + $res['userID'] = $userID; + $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : $expiry); + $res['reason'] = $params['reason']; + if($params['anononly']) + $res['anononly'] = ''; + if($params['nocreate']) + $res['nocreate'] = ''; + if($params['autoblock']) + $res['autoblock'] = ''; + if($params['noemail']) + $res['noemail'] = ''; + if($params['hidename']) + $res['hidename'] = ''; + + $this->getResult()->addValue(null, $this->getModuleName(), $res); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'user' => null, + 'token' => null, + 'gettoken' => false, + 'expiry' => 'never', + 'reason' => null, + 'anononly' => false, + 'nocreate' => false, + 'autoblock' => false, + 'noemail' => false, + 'hidename' => false, + ); + } + + public function getParamDescription() { + return array ( + 'user' => 'Username, IP address or IP range you want to block', + 'token' => 'A block token previously obtained through the gettoken parameter', + 'gettoken' => 'If set, a block token will be returned, and no other action will be taken', + 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', + 'reason' => 'Reason for block (optional)', + 'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)', + 'nocreate' => 'Prevent account creation', + 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from', + 'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)', + 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)' + ); + } + + public function getDescription() { + return array( + 'Block a user.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike', + 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate&autoblock&noemail' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiBlock.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiChangeRights.php b/includes/api/ApiChangeRights.php new file mode 100644 index 00000000..647a5194 --- /dev/null +++ b/includes/api/ApiChangeRights.php @@ -0,0 +1,155 @@ +<?php + +/* + * Created on Sep 11, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * API module that facilitates the changing of user rights. The API eqivalent of + * Special:Userrights. Requires API write mode to be enabled. + * + * @addtogroup API + */ +class ApiChangeRights extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser, $wgRequest; + $this->getMain()->requestWriteMode(); + + if(wfReadOnly()) + $this->dieUsage('The wiki is in read-only mode', 'readonly'); + $params = $this->extractRequestParams(); + + $ur = new UserrightsPage($wgRequest); + $allowed = $ur->changeableGroups(); + $res = array(); + + $u = $ur->fetchUser_real($params['user']); + if(is_array($u)) + switch($u[0]) + { + case UserrightsPage::FETCHUSER_NO_INTERWIKI: + $this->dieUsage("You don't have permission to change users' rights on other wikis", 'nointerwiki'); + case UserrightsPage::FETCHUSER_NO_DATABASE: + $this->dieUsage("Database ``{$u[1]}'' does not exist or is not local", 'nosuchdatabase'); + case UserrightsPage::FETCHUSER_NO_USER: + $this->dieUsage("You specified an empty username, or none at all", 'emptyuser'); + case UserrightsPage::FETCHUSER_NOSUCH_USERID: + $this->dieUsage("There is no user with ID ``{$u[1]}''", 'nosuchuserid'); + case UserrightsPage::FETCHUSER_NOSUCH_USERNAME: + $this->dieUsage("There is no user with username ``{$u[1]}''", 'nosuchusername'); + default: + $this->dieDebug(__METHOD__, "UserrightsPage::fetchUser_real() returned an unknown error ({$u[0]})"); + } + + $curgroups = $u->getGroups(); + if($params['listgroups']) + { + $res['user'] = $u->getName(); + $res['allowedgroups'] = $allowed; + $res['ingroups'] = $curgroups; + $this->getResult()->setIndexedTagName($res['ingroups'], 'group'); + $this->getResult()->setIndexedTagName($res['allowedgroups']['add'], 'group'); + $this->getResult()->setIndexedTagName($res['allowedgroups']['remove'], 'group'); + } +; + if($params['gettoken']) + { + $res['changerightstoken'] = $wgUser->editToken($u->getName()); + $this->getResult()->addValue(null, $this->getModuleName(), $res); + return; + } + + if(empty($params['addto']) && empty($params['rmfrom'])) + $this->dieUsage('At least one of the addto and rmfrom parameters must be set', 'noaddrm'); + if(is_null($params['token'])) + $this->dieUsage('The token parameter must be set', 'notoken'); + if(!$wgUser->matchEditToken($params['token'], $u->getName())) + $this->dieUsage('Invalid token', 'badtoken'); + + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $ur->saveUserGroups($u, $params['rmfrom'], $params['addto'], $params['reason']); + $dbw->commit(); + $res['user'] = $u->getName(); + $res['addedto'] = (array)$params['addto']; + $res['removedfrom'] = (array)$params['rmfrom']; + $res['reason'] = $params['reason']; + + $this->getResult()->setIndexedTagName($res['addedto'], 'group'); + $this->getResult()->setIndexedTagName($res['removedfrom'], 'group'); + $this->getResult()->addValue(null, $this->getModuleName(), $res); + } + + public function getAllowedParams() { + return array ( + 'user' => null, + 'token' => null, + 'gettoken' => false, + 'listgroups' => false, + 'addto' => array( + ApiBase :: PARAM_ISMULTI => true, + ), + 'rmfrom' => array( + ApiBase :: PARAM_ISMULTI => true, + ), + 'reason' => '' + ); + } + + public function getParamDescription() { + return array ( + 'user' => 'The user you want to add to or remove from groups.', + 'token' => 'A changerights token previously obtained through the gettoken parameter.', + 'gettoken' => 'Output a token. Note that the user parameter still has to be set.', + 'listgroups' => 'List the groups the user is in, and the ones you can add them to and remove them from.', + 'addto' => 'Pipe-separated list of groups to add this user to', + 'rmfrom' => 'Pipe-separated list of groups to remove this user from', + 'reason' => 'Reason for change (optional)' + ); + } + + public function getDescription() { + return array( + 'Add or remove a user from certain groups.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=changerights&user=Bob&gettoken&listgroups', + 'api.php?action=changerights&user=Bob&token=123ABC&addto=sysop&reason=Promoting%20per%20RFA' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiChangeRights.php 28216 2007-12-06 18:33:18Z vasilievvv $'; + } +} diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php new file mode 100644 index 00000000..cd747e7e --- /dev/null +++ b/includes/api/ApiDelete.php @@ -0,0 +1,155 @@ +<?php + +/* + * Created on Jun 30, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + + +/** + * API module that facilitates deleting pages. The API eqivalent of action=delete. + * Requires API write mode to be enabled. + * + * @addtogroup API + */ +class ApiDelete extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Extracts the title, token, and reason from the request parameters and invokes + * the local delete() function with these as arguments. It does not make use of + * the delete function specified by Article.php. If the deletion succeeds, the + * details of the article deleted and the reason for deletion are added to the + * result object. + */ + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + $titleObj = NULL; + if(!isset($params['title'])) + $this->dieUsageMsg(array('missingparam', 'title')); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + if(!$titleObj->exists()) + $this->dieUsageMsg(array('notanarticle')); + + $articleObj = new Article($titleObj); + $reason = (isset($params['reason']) ? $params['reason'] : NULL); + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $retval = self::delete($articleObj, $params['token'], $reason); + + if(!empty($retval)) + // We don't care about multiple errors, just report one of them + $this->dieUsageMsg(current($retval)); + + $dbw->commit(); + $r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason); + $this->getResult()->addValue(null, $this->getModuleName(), $r); + } + + /** + * We have our own delete() function, since Article.php's implementation is split in two phases + * + * @param Article $article - Article object to work on + * @param string $token - Delete token (same as edit token) + * @param string $reason - Reason for the deletion. Autogenerated if NULL + * @return Title::getUserPermissionsErrors()-like array + */ + public static function delete(&$article, $token, &$reason = NULL) + { + global $wgUser; + + // Check permissions + $errors = $article->mTitle->getUserPermissionsErrors('delete', $wgUser); + if(!empty($errors)) + return $errors; + if(wfReadOnly()) + return array(array('readonlytext')); + if($wgUser->isBlocked()) + return array(array('blocked')); + + // Check token + if(!$wgUser->matchEditToken($token)) + return array(array('sessionfailure')); + + // Auto-generate a summary, if necessary + if(is_null($reason)) + { + $reason = $article->generateReason($hasHistory); + if($reason === false) + return array(array('cannotdelete')); + } + + // Luckily, Article.php provides a reusable delete function that does the hard work for us + if($article->doDeleteArticle($reason)) + return array(); + return array(array('cannotdelete', $article->mTitle->getPrefixedText())); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'title' => null, + 'token' => null, + 'reason' => null, + ); + } + + public function getParamDescription() { + return array ( + 'title' => 'Title of the page you want to delete.', + 'token' => 'A delete token previously retrieved through prop=info', + 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.' + ); + } + + public function getDescription() { + return array( + 'Deletes a page. You need to be logged in as a sysop to use this function, see also action=login.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=delete&title=Main%20Page&token=123ABC', + 'api.php?action=delete&title=Main%20Page&token=123ABC&reason=Preparing%20for%20move' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiDelete.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php new file mode 100644 index 00000000..278896fa --- /dev/null +++ b/includes/api/ApiExpandTemplates.php @@ -0,0 +1,97 @@ +<?php + +/* + * Created on Oct 05, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * API module that functions as a shortcut to the wikitext preprocessor. Expands + * any templates in a provided string, and returns the result of this expansion + * to the caller. + * + * @addtogroup API + */ +class ApiExpandTemplates extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + // Get parameters + $params = $this->extractRequestParams(); + $text = $params['text']; + $title = $params['title']; + $retval = ''; + + //Create title for parser + $title_obj = Title :: newFromText($params['title']); + if(!$title_obj) + $title_obj = Title :: newFromText("API"); // Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it + + // Parse text + global $wgParser; + $retval = $wgParser->preprocess( $text, $title_obj, new ParserOptions() ); + + // Return result + $result = $this->getResult(); + $retval_array = array(); + $result->setContent( $retval_array, $retval ); + $result->addValue( null, $this->getModuleName(), $retval_array ); + } + + public function getAllowedParams() { + return array ( + 'title' => array( + ApiBase :: PARAM_DFLT => 'API', + ), + 'text' => null + ); + } + + public function getParamDescription() { + return array ( + 'text' => 'Wikitext to convert', + 'title' => 'Title of page', + ); + } + + public function getDescription() { + return 'This module expand all templates in wikitext'; + } + + protected function getExamples() { + return array ( + 'api.php?action=expandtemplates&text={{Project:Sandbox}}' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiExpandTemplates.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} + diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index b2f6ceff..9b17b9d3 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -62,17 +62,24 @@ class ApiFeedWatchlist extends ApiBase { // limit to the number of hours going from now back $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60)); - // Prepare nested request - $fauxReq = new FauxRequest(array ( + $dbr = wfGetDB( DB_SLAVE ); + // Prepare parameters for nested request + $fauxReqArr = array ( 'action' => 'query', 'meta' => 'siteinfo', 'siprop' => 'general', 'list' => 'watchlist', 'wlprop' => 'title|user|comment|timestamp', 'wldir' => 'older', // reverse order - from newest to oldest - 'wlend' => $endTime, // stop at this time + 'wlend' => $dbr->timestamp($endTime), // stop at this time 'wllimit' => 50 - )); + ); + + // Check for 'allrev' parameter, and if found, show all revisions to each page on wl. + if ( ! is_null ( $params['allrev'] ) ) $fauxReqArr['wlallrev'] = ''; + + // Create the request + $fauxReq = new FauxRequest ( $fauxReqArr ); // Execute $module = new ApiMain($fauxReq); @@ -131,7 +138,7 @@ class ApiFeedWatchlist extends ApiBase { return new FeedItem($titleStr, $completeText, $titleUrl, $timestamp, $user); } - protected function getAllowedParams() { + public function getAllowedParams() { global $wgFeedClasses; $feedFormatNames = array_keys($wgFeedClasses); return array ( @@ -144,18 +151,20 @@ class ApiFeedWatchlist extends ApiBase { ApiBase :: PARAM_TYPE => 'integer', ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => 72, - ) + ), + 'allrev' => null ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'feedformat' => 'The format of the feed', - 'hours' => 'List pages modified within this many hours from now' + 'hours' => 'List pages modified within this many hours from now', + 'allrev' => 'Include multiple revisions of the same page within given timeframe.' ); } - protected function getDescription() { + public function getDescription() { return 'This module returns a watchlist feed'; } @@ -166,7 +175,7 @@ class ApiFeedWatchlist extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFeedWatchlist.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFeedWatchlist.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 861310d2..768a18ac 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -35,7 +35,7 @@ if (!defined('MEDIAWIKI')) { */ abstract class ApiFormatBase extends ApiBase { - private $mIsHtml, $mFormat; + private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp; /** * Create a new instance of the formatter. @@ -69,6 +69,18 @@ abstract class ApiFormatBase extends ApiBase { } /** + * Specify whether or not ampersands should be escaped to '&' when rendering. This + * should only be set to true for the help message when rendered in the default (xmlfm) + * format. This is a temporary special-case fix that should be removed once the help + * has been reworked to use a fully html interface. + * + * @param boolean Whether or not ampersands should be escaped. + */ + public function setUnescapeAmps ( $b ) { + $this->mUnescapeAmps = $b; + } + + /** * Returns true when an HTML filtering printer should be used. * The default implementation assumes that formats ending with 'fm' * should be formatted in HTML. @@ -99,7 +111,11 @@ abstract class ApiFormatBase extends ApiBase { <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> - <title>MediaWiki API</title> +<?php if ($this->mUnescapeAmps) { +?> <title>MediaWiki API</title> +<?php } else { +?> <title>MediaWiki API Result</title> +<?php } ?> </head> <body> <?php @@ -154,13 +170,20 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or } /** + * Says pretty-printer that it should use *bold* and $italics$ formatting + */ + public function setHelp( $help = true ) { + $this->mHelp = true; + } + + /** * Prety-print various elements in HTML format, such as xml tags and URLs. * This method also replaces any '<' with < */ protected function formatHTML($text) { // Escape everything first for full coverage $text = htmlspecialchars($text); - + // encode all comments or tags as safe blue strings $text = preg_replace('/\<(!--.*?--|.*?)\>/', '<span style="color:blue;"><\1></span>', $text); // identify URLs @@ -168,10 +191,19 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or $text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '<a href="\\0">\\0</a>', $text); // identify requests to api.php $text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '<a href="\\0">\\0</a>', $text); - // make strings inside * bold - $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text); - // make strings inside $ italic - $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text); + if( $this->mHelp ) { + // make strings inside * bold + $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text); + // make strings inside $ italic + $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text); + } + + /* Temporary fix for bad links in help messages. As a special case, + * XML-escaped metachars are de-escaped one level in the help message + * for legibility. Should be removed once we have completed a fully-html + * version of the help message. */ + if ( $this->mUnescapeAmps ) + $text = preg_replace( '/&(amp|quot|lt|gt);/', '&\1;', $text ); return $text; } @@ -183,12 +215,12 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or return 'api.php?action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName(); } - protected function getDescription() { + public function getDescription() { return $this->getIsHtml() ? ' (pretty-print in HTML)' : ''; } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $'; } } @@ -250,6 +282,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php new file mode 100644 index 00000000..f0fc5e91 --- /dev/null +++ b/includes/api/ApiFormatDbg.php @@ -0,0 +1,59 @@ +<?php + +/* + * Created on Oct 22, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +/** + * @addtogroup API + */ +class ApiFormatDbg extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + # This looks like it should be text/plain, but IE7 is so + # brain-damaged it tries to parse text/plain as HTML if it + # contains HTML tags. Using MIME text/text works around this bug + return 'text/text'; + } + + public function execute() { + $this->printText(var_export($this->getResultData(), true)); + } + + public function getDescription() { + return 'Output data in PHP\'s var_export() format' . parent :: getDescription(); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; + } +} + diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 59f3b492..852a64b6 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -66,19 +66,19 @@ class ApiFormatJson extends ApiFormatBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'callback' => null ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'callback' => 'If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.', ); } - protected function getDescription() { + public function getDescription() { if ($this->mIsRaw) return 'Output data with the debuging elements in JSON format' . parent :: getDescription(); else @@ -86,7 +86,7 @@ class ApiFormatJson extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 31484 2008-03-03 05:46:20Z brion $'; } } diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index 766d7041..f830d8e1 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -45,12 +45,12 @@ class ApiFormatPhp extends ApiFormatBase { $this->printText(serialize($this->getResultData())); } - protected function getDescription() { + public function getDescription() { return 'Output data in serialized PHP format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatPhp.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php new file mode 100644 index 00000000..c4c45f68 --- /dev/null +++ b/includes/api/ApiFormatTxt.php @@ -0,0 +1,59 @@ +<?php + +/* + * Created on Oct 22, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +/** + * @addtogroup API + */ +class ApiFormatTxt extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + # This looks like it should be text/plain, but IE7 is so + # brain-damaged it tries to parse text/plain as HTML if it + # contains HTML tags. Using MIME text/text works around this bug + return 'text/text'; + } + + public function execute() { + $this->printText(print_r($this->getResultData(), true)); + } + + public function getDescription() { + return 'Output data in PHP\'s print_r() format' . parent :: getDescription(); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; + } +} + diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 0ddfac73..22a0e482 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -80,12 +80,12 @@ class ApiFormatWddx extends ApiFormatBase { } } - protected function getDescription() { + public function getDescription() { return 'Output data in WDDX format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatWddx.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatWddx.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 02647923..d39e8049 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -136,12 +136,12 @@ class ApiFormatXml extends ApiFormatBase { break; } } - protected function getDescription() { + public function getDescription() { return 'Output data in XML format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 400c0a4b..5e15aee6 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -45,12 +45,12 @@ class ApiFormatYaml extends ApiFormatBase { $this->printText(Spyc :: YAMLDump($this->getResultData())); } - protected function getDescription() { + public function getDescription() { return 'Output data in YAML format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatYaml.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatYaml.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php index b3ccff0f..b2973b8c 100644 --- a/includes/api/ApiFormatYaml_spyc.php +++ b/includes/api/ApiFormatYaml_spyc.php @@ -385,6 +385,18 @@ return false; } } + + /** + * Find out whether a string needs to be output as a literal rather than in plain style. + * Added by Roan Kattouw 13-03-2008 + * @param $value The string to check + * @return bool + */ + function _needLiteral($value) { + # Check whether the string contains # or : or begins with any of: + # [ - ? , [ ] { } ! * & | > ' " % @ ` ] + return (bool)(preg_match("/[#:]/", $value) || preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value)); + } /** * Returns YAML from a key and a value @@ -396,7 +408,7 @@ */ function _dumpNode($key,$value,$indent) { // do some folding here, for blocks - if (strpos($value,"\n")) { + if (strpos($value,"\n") || $this->_needLiteral($value)) { $value = $this->_doLiteralBlock($value,$indent); } else { $value = $this->_doFolding($value,$indent); diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 9f1e88ea..47a45ea1 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -46,14 +46,18 @@ class ApiHelp extends ApiBase { $this->dieUsage('', 'help'); } - protected function getDescription() { + public function shouldCheckMaxlag() { + return false; + } + + public function getDescription() { return array ( 'Display this help screen.' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiHelp.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiHelp.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index af68b29d..3e66ed79 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -40,7 +40,7 @@ class ApiLogin extends ApiBase { * Time (in seconds) a user must wait after submitting * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt) */ - const THROTTLE_TIME = 1; + const THROTTLE_TIME = 5; /** * The factor by which the wait-time in between authentication @@ -91,10 +91,15 @@ class ApiLogin extends ApiBase { 'wpRemember' => '' )); + // Init session if necessary + if( session_id() == '' ) { + wfSetupSession(); + } + $loginForm = new LoginForm($params); switch ($loginForm->authenticateUserData()) { case LoginForm :: SUCCESS : - global $wgUser; + global $wgUser, $wgCookiePrefix; $wgUser->setOption('rememberpassword', 1); $wgUser->setCookies(); @@ -103,6 +108,8 @@ class ApiLogin extends ApiBase { $result['lguserid'] = $_SESSION['wsUserID']; $result['lgusername'] = $_SESSION['wsUserName']; $result['lgtoken'] = $_SESSION['wsToken']; + $result['cookieprefix'] = $wgCookiePrefix; + $result['sessionid'] = session_id(); break; case LoginForm :: NO_NAME : @@ -129,6 +136,7 @@ class ApiLogin extends ApiBase { if ($result['result'] != 'Success') { $result['wait'] = $this->cacheBadLogin(); + $result['details'] = "Please wait " . self::THROTTLE_TIME . " seconds before next log-in attempt"; } // if we were allowed to try to login, memcache is fine @@ -209,8 +217,10 @@ class ApiLogin extends ApiBase { private function getMemCacheKey() { return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() ); } + + public function mustBePosted() { return true; } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'name' => null, 'password' => null, @@ -218,7 +228,7 @@ class ApiLogin extends ApiBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'name' => 'User Name', 'password' => 'Password', @@ -226,7 +236,7 @@ class ApiLogin extends ApiBase { ); } - protected function getDescription() { + public function getDescription() { return array ( 'This module is used to login and get the authentication tokens. ', 'In the event of a successful log-in, a cookie will be attached', @@ -243,7 +253,7 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 24695 2007-08-09 09:53:05Z yurik $'; + return __CLASS__ . ': $Id: ApiLogin.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php new file mode 100644 index 00000000..d578acf3 --- /dev/null +++ b/includes/api/ApiLogout.php @@ -0,0 +1,71 @@ +<?php + +/* + * Created on Jan 4, 2008 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +/** + * API module to allow users to log out of the wiki. API equivalent of + * Special:Userlogout. + * + * @addtogroup API + */ +class ApiLogout extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $wgUser->logout(); + } + + public function getAllowedParams() { + return array (); + } + + public function getParamDescription() { + return array (); + } + + public function getDescription() { + return array ( + 'This module is used to logout and clear session data' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=logout' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 00b3f63f..874e531c 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -53,10 +53,26 @@ class ApiMain extends ApiBase { */ private static $Modules = array ( 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', 'query' => 'ApiQuery', + 'expandtemplates' => 'ApiExpandTemplates', + 'parse' => 'ApiParse', 'opensearch' => 'ApiOpenSearch', 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', + 'paraminfo' => 'ApiParamInfo', + ); + + private static $WriteModules = array ( + 'rollback' => 'ApiRollback', + 'delete' => 'ApiDelete', + 'undelete' => 'ApiUndelete', + 'protect' => 'ApiProtect', + 'block' => 'ApiBlock', + 'unblock' => 'ApiUnblock', + 'move' => 'ApiMove', + #'changerights' => 'ApiChangeRights' + # Disabled for now ); /** @@ -73,7 +89,11 @@ class ApiMain extends ApiBase { 'xmlfm' => 'ApiFormatXml', 'yaml' => 'ApiFormatYaml', 'yamlfm' => 'ApiFormatYaml', - 'rawfm' => 'ApiFormatJson' + 'rawfm' => 'ApiFormatJson', + 'txt' => 'ApiFormatTxt', + 'txtfm' => 'ApiFormatTxt', + 'dbg' => 'ApiFormatDbg', + 'dbgfm' => 'ApiFormatDbg' ); private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; @@ -108,14 +128,17 @@ class ApiMain extends ApiBase { if (!$wgUser->isAllowed('read')) { self::$Modules = array( - 'login' => self::$Modules['login'], - 'help' => self::$Modules['help'] + 'login' => self::$Modules['login'], + 'logout' => self::$Modules['logout'], + 'help' => self::$Modules['help'], ); } } - global $wgAPIModules; // extension modules + global $wgAPIModules, $wgEnableWriteAPI; // extension modules $this->mModules = $wgAPIModules + self :: $Modules; + if($wgEnableWriteAPI) + $this->mModules += self::$WriteModules; $this->mModuleNames = array_keys($this->mModules); // todo: optimize $this->mFormats = self :: $Formats; @@ -157,7 +180,7 @@ class ApiMain extends ApiBase { public function requestWriteMode() { if (!$this->mEnableWrite) $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' . - 'statement is included in the site\'s LocalSettings.php file', 'readonly'); + 'statement is included in the site\'s LocalSettings.php file', 'noapiwrite'); } /** @@ -271,7 +294,7 @@ class ApiMain extends ApiBase { // Something is seriously wrong // $errMessage = array ( - 'code' => 'internal_api_error', + 'code' => 'internal_api_error_'. get_class($e), 'info' => "Exception Caught: {$e->getMessage()}" ); ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n"); @@ -287,16 +310,34 @@ class ApiMain extends ApiBase { * Execute the actual module, without any error handling */ protected function executeAction() { - + $params = $this->extractRequestParams(); - + $this->mShowVersions = $params['version']; $this->mAction = $params['action']; // Instantiate the module requested by the user $module = new $this->mModules[$this->mAction] ($this, $this->mAction); - + + if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { + // Check for maxlag + global $wgLoadBalancer, $wgShowHostnames; + $maxLag = $params['maxlag']; + list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); + if ( $lag > $maxLag ) { + if( $wgShowHostnames ) { + ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); + } else { + ApiBase :: dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); + } + return; + } + } + if (!$this->mInternalMode) { + // Ignore mustBePosted() for internal calls + if($module->mustBePosted() && !$this->mRequest->wasPosted()) + $this->dieUsage("The {$this->mAction} module requires a POST request", 'mustbeposted'); // See if custom printer is used $this->mPrinter = $module->getCustomPrinter(); @@ -326,7 +367,16 @@ class ApiMain extends ApiBase { protected function printResult($isError) { $printer = $this->mPrinter; $printer->profileIn(); + + /* If the help message is requested in the default (xmlfm) format, + * tell the printer not to escape ampersands so that our links do + * not break. */ + $params = $this->extractRequestParams(); + $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError ) + && $params['format'] == ApiMain::API_DEFAULT_FORMAT ); + $printer->initPrinter($isError); + $printer->execute(); $printer->closePrinter(); $printer->profileOut(); @@ -335,7 +385,7 @@ class ApiMain extends ApiBase { /** * See ApiBase for description. */ - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'format' => array ( ApiBase :: PARAM_DFLT => ApiMain :: API_DEFAULT_FORMAT, @@ -345,25 +395,29 @@ class ApiMain extends ApiBase { ApiBase :: PARAM_DFLT => 'help', ApiBase :: PARAM_TYPE => $this->mModuleNames ), - 'version' => false + 'version' => false, + 'maxlag' => array ( + ApiBase :: PARAM_TYPE => 'integer' + ), ); } /** * See ApiBase for description. */ - protected function getParamDescription() { + public function getParamDescription() { return array ( 'format' => 'The format of the output', 'action' => 'What action you would like to perform', - 'version' => 'When showing help, include version for each module' + 'version' => 'When showing help, include version for each module', + 'maxlag' => 'Maximum lag' ); } /** * See ApiBase for description. */ - protected function getDescription() { + public function getDescription() { return array ( '', '', @@ -396,8 +450,9 @@ class ApiMain extends ApiBase { */ protected function getCredits() { return array( - 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / <Firstname><Lastname>@gmail.com', - 'Please leave your comments and suggestions at http://www.mediawiki.org/wiki/API' + 'This API is being implemented by Roan Kattouw <Firstname>.<Lastname>@home.nl', + 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', + 'or file a bug report at http://bugzilla.wikimedia.org/' ); } @@ -405,6 +460,8 @@ class ApiMain extends ApiBase { * Override the parent to generate help messages for all available modules. */ public function makeHelpMsg() { + + $this->mPrinter->setHelp(); // Use parent to make default message for the main module $msg = parent :: makeHelpMsg(); @@ -445,11 +502,12 @@ class ApiMain extends ApiBase { } private $mIsBot = null; - private $mIsSysop = null; + private $mCanApiHighLimits = null; /** * Returns true if the currently logged in user is a bot, false otherwise + * OBSOLETE, use canApiHighLimits() instead */ public function isBot() { if (!isset ($this->mIsBot)) { @@ -462,6 +520,7 @@ class ApiMain extends ApiBase { /** * Similar to isBot(), this method returns true if the logged in user is * a sysop, and false if not. + * OBSOLETE, use canApiHighLimits() instead */ public function isSysop() { if (!isset ($this->mIsSysop)) { @@ -471,6 +530,15 @@ class ApiMain extends ApiBase { return $this->mIsSysop; } + + public function canApiHighLimits() { + if (!isset($this->mCanApiHighLimits)) { + global $wgUser; + $this->mCanApiHighLimits = $wgUser->isAllowed('apihighlimits'); + } + + return $this->mCanApiHighLimits; + } public function getShowVersions() { return $this->mShowVersions; @@ -483,7 +551,7 @@ class ApiMain extends ApiBase { public function getVersion() { $vers = array (); $vers[] = 'MediaWiki ' . SpecialVersion::getVersion(); - $vers[] = __CLASS__ . ': $Id: ApiMain.php 25364 2007-08-31 15:23:48Z tstarling $'; + $vers[] = __CLASS__ . ': $Id: ApiMain.php 31484 2008-03-03 05:46:20Z brion $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); @@ -515,6 +583,13 @@ class ApiMain extends ApiBase { protected function addFormat( $fmtName, $fmtClass ) { $this->mFormats[$fmtName] = $fmtClass; } + + /** + * Get the array mapping module names to class names + */ + function getModules() { + return $this->mModules; + } } /** @@ -539,3 +614,4 @@ class UsageException extends Exception { } } + diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php new file mode 100644 index 00000000..a8c39c9a --- /dev/null +++ b/includes/api/ApiMove.php @@ -0,0 +1,152 @@ +<?php + +/* + * Created on Oct 31, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + + +/** + * @addtogroup API + */ +class ApiMove extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + if(is_null($params['reason'])) + $params['reason'] = ''; + + $titleObj = NULL; + if(!isset($params['from'])) + $this->dieUsageMsg(array('missingparam', 'from')); + if(!isset($params['to'])) + $this->dieUsageMsg(array('missingparam', 'to')); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + + $fromTitle = Title::newFromText($params['from']); + if(!$fromTitle) + $this->dieUsageMsg(array('invalidtitle', $params['from'])); + if(!$fromTitle->exists()) + $this->dieUsageMsg(array('notanarticle')); + $fromTalk = $fromTitle->getTalkPage(); + + $toTitle = Title::newFromText($params['to']); + if(!$toTitle) + $this->dieUsageMsg(array('invalidtitle', $params['to'])); + $toTalk = $toTitle->getTalkPage(); + + // Run getUserPermissionsErrors() here so we get message arguments too, + // rather than just a message key. The latter is troublesome for messages + // that use arguments. + // FIXME: moveTo() should really return an array, requires some + // refactoring of other code, though (mainly SpecialMovepage.php) + $errors = array_merge($fromTitle->getUserPermissionsErrors('move', $wgUser), + $fromTitle->getUserPermissionsErrors('edit', $wgUser), + $toTitle->getUserPermissionsErrors('move', $wgUser), + $toTitle->getUserPermissionsErrors('edit', $wgUser)); + if(!empty($errors)) + // We don't care about multiple errors, just report one of them + $this->dieUsageMsg(current($errors)); + + $dbw = wfGetDB(DB_MASTER); + $dbw->begin(); + $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']); + if($retval !== true) + $this->dieUsageMsg(array($retval)); + + $r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']); + if(!$params['noredirect']) + $r['redirectcreated'] = ''; + + if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage()) + { + // We need to move the talk page as well + $toTalk = $toTitle->getTalkPage(); + $retval = $fromTalk->moveTo($toTalk, true, $params['reason'], !$params['noredirect']); + if($retval === true) + { + $r['talkfrom'] = $fromTalk->getPrefixedText(); + $r['talkto'] = $toTalk->getPrefixedText(); + } + // We're not gonna dieUsage() on failure, since we already changed something + else + { + $r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code']; + $r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info']; + } + } + $dbw->commit(); // Make sure all changes are really written to the DB + $this->getResult()->addValue(null, $this->getModuleName(), $r); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'from' => null, + 'to' => null, + 'token' => null, + 'reason' => null, + 'movetalk' => false, + 'noredirect' => false + ); + } + + public function getParamDescription() { + return array ( + 'from' => 'Title of the page you want to move.', + 'to' => 'Title you want to rename the page to.', + 'token' => 'A move token previously retrieved through prop=info', + 'reason' => 'Reason for the move (optional).', + 'movetalk' => 'Move the talk page, if it exists.', + 'noredirect' => 'Don\'t create a redirect' + ); + } + + public function getDescription() { + return array( + 'Moves a page.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=move&from=Exampel&to=Example&token=123ABC&reason=Misspelled%20title&movetalk&noredirect' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiMove.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 8484b163..f4b600fe 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -44,37 +44,12 @@ class ApiOpenSearch extends ApiBase { public function execute() { $params = $this->extractRequestParams(); $search = $params['search']; + $limit = $params['limit']; // Open search results may be stored for a very long time $this->getMain()->setCacheMaxAge(1200); - - $title = Title :: newFromText($search); - if(!$title) - return; // Return empty result - - // Prepare nested request - $req = new FauxRequest(array ( - 'action' => 'query', - 'list' => 'allpages', - 'apnamespace' => $title->getNamespace(), - 'aplimit' => 10, - 'apprefix' => $title->getDBkey() - )); - - // Execute - $module = new ApiMain($req); - $module->execute(); - - // Get resulting data - $data = $module->getResultData(); - - // Reformat useful data for future printing by JSON engine - $srchres = array (); - foreach ($data['query']['allpages'] as & $pageinfo) { - // Note: this data will no be printable by the xml engine - // because it does not support lists of unnamed items - $srchres[] = $pageinfo['title']; - } + + $srchres = PrefixSearch::titleSearch( $search, $limit ); // Set top level elements $result = $this->getResult(); @@ -82,19 +57,27 @@ class ApiOpenSearch extends ApiBase { $result->addValue(null, 1, $srchres); } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( - 'search' => null + 'search' => null, + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => 100, + ApiBase :: PARAM_MAX2 => 100 + ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( - 'search' => 'Search string' + 'search' => 'Search string', + 'limit' => 'Maximum amount of results to return' ); } - protected function getDescription() { + public function getDescription() { return 'This module implements OpenSearch protocol'; } @@ -105,7 +88,7 @@ class ApiOpenSearch extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiOpenSearch.php 24099 2007-07-15 00:52:35Z yurik $'; + return __CLASS__ . ': $Id: ApiOpenSearch.php 30275 2008-01-30 01:07:49Z brion $'; } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php new file mode 100644 index 00000000..7de22252 --- /dev/null +++ b/includes/api/ApiParamInfo.php @@ -0,0 +1,167 @@ +<?php + +/* + * Created on Dec 01, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiParamInfo extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + // Get parameters + $params = $this->extractRequestParams(); + $result = $this->getResult(); + $r = array(); + if(is_array($params['modules'])) + { + $modArr = $this->getMain()->getModules(); + foreach($params['modules'] as $m) + { + if(!isset($modArr[$m])) + { + $r['modules'][] = array('name' => $m, 'missing' => ''); + continue; + } + $obj = new $modArr[$m]($this->getMain(), $m); + $a = $this->getClassInfo($obj); + $a['name'] = $m; + $r['modules'][] = $a; + } + $result->setIndexedTagName($r['modules'], 'module'); + } + if(is_array($params['querymodules'])) + { + $queryObj = new ApiQuery($this->getMain(), 'query'); + $qmodArr = $queryObj->getModules(); + foreach($params['querymodules'] as $qm) + { + if(!isset($qmodArr[$qm])) + { + $r['querymodules'][] = array('name' => $qm, 'missing' => ''); + continue; + } + $obj = new $qmodArr[$qm]($this, $qm); + $a = $this->getClassInfo($obj); + $a['name'] = $qm; + $r['querymodules'][] = $a; + } + $result->setIndexedTagName($r['querymodules'], 'module'); + } + $result->addValue(null, $this->getModuleName(), $r); + } + + function getClassInfo($obj) + { + $result = $this->getResult(); + $retval['classname'] = get_class($obj); + $retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription()); + $retval['prefix'] = $obj->getModulePrefix(); + $allowedParams = $obj->getAllowedParams(); + if(!is_array($allowedParams)) + return $retval; + $retval['parameters'] = array(); + $paramDesc = $obj->getParamDescription(); + foreach($obj->getAllowedParams() as $n => $p) + { + $a = array('name' => $n); + if(!is_array($p)) + { + if(is_bool($p)) + { + $a['type'] = 'bool'; + $a['default'] = ($p ? 'true' : 'false'); + } + if(is_string($p)) + $a['default'] = $p; + $retval['parameters'][] = $a; + continue; + } + + if(isset($p[ApiBase::PARAM_DFLT])) + $a['default'] = $p[ApiBase::PARAM_DFLT]; + if(isset($p[ApiBase::PARAM_ISMULTI])) + if($p[ApiBase::PARAM_ISMULTI]) + $a['multi'] = ''; + if(isset($p[ApiBase::PARAM_TYPE])) + { + $a['type'] = $p[ApiBase::PARAM_TYPE]; + if(is_array($a['type'])) + $result->setIndexedTagName($a['type'], 't'); + } + if(isset($p[ApiBase::PARAM_MAX])) + $a['max'] = $p[ApiBase::PARAM_MAX]; + if(isset($p[ApiBase::PARAM_MAX2])) + $a['highmax'] = $p[ApiBase::PARAM_MAX2]; + if(isset($p[ApiBase::PARAM_MIN])) + $a['min'] = $p[ApiBase::PARAM_MIN]; + if(isset($paramDesc[$n])) + $a['description'] = (is_array($paramDesc[$n]) ? implode("\n", $paramDesc[$n]) : $paramDesc[$n]); + $retval['parameters'][] = $a; + } + $result->setIndexedTagName($retval['parameters'], 'param'); + return $retval; + } + + public function getAllowedParams() { + return array ( + 'modules' => array( + ApiBase :: PARAM_ISMULTI => true + ), + 'querymodules' => array( + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'modules' => 'List of module names (value of the action= parameter)', + 'querymodules' => 'List of query module names (value of prop=, meta= or list= parameter)', + ); + } + + public function getDescription() { + return 'Obtain information about certain API parameters'; + } + + protected function getExamples() { + return array ( + 'api.php?action=paraminfo&modules=parse&querymodules=allpages|siteinfo' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiParse.php 29810 2008-01-15 21:33:08Z catrope $'; + } +} + diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php new file mode 100644 index 00000000..21a21e8d --- /dev/null +++ b/includes/api/ApiParse.php @@ -0,0 +1,202 @@ +<?php + +/* + * Created on Dec 01, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiParse extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + // Get parameters + $params = $this->extractRequestParams(); + $text = $params['text']; + $title = $params['title']; + $page = $params['page']; + if(!is_null($page) && (!is_null($text) || $title != "API")) + $this->dieUsage("The page parameter cannot be used together with the text and title parameters", 'params'); + $prop = array_flip($params['prop']); + + global $wgParser, $wgUser; + if(!is_null($page)) { + $titleObj = Title::newFromText($page); + if(!$titleObj) + $this->dieUsageMsg(array('missingtitle', $page)); + + // Try the parser cache first + $articleObj = new Article($titleObj); + $pcache =& ParserCache::singleton(); + $p_result = $pcache->get($articleObj, $wgUser); + if(!$p_result) { + $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, new ParserOptions()); + global $wgUseParserCache; + if($wgUseParserCache) + $pcache->save($p_result, $articleObj, $wgUser); + } + } else { + $titleObj = Title::newFromText($title); + if(!$titleObj) + $titleObj = Title::newFromText("API"); + $p_result = $wgParser->parse($text, $titleObj, new ParserOptions()); + } + + // Return result + $result = $this->getResult(); + $result_array = array(); + if(isset($prop['text'])) { + $result_array['text'] = array(); + $result->setContent($result_array['text'], $p_result->getText()); + } + if(isset($prop['langlinks'])) + $result_array['langlinks'] = $this->formatLangLinks($p_result->getLanguageLinks()); + if(isset($prop['categories'])) + $result_array['categories'] = $this->formatCategoryLinks($p_result->getCategories()); + if(isset($prop['links'])) + $result_array['links'] = $this->formatLinks($p_result->getLinks()); + if(isset($prop['templates'])) + $result_array['templates'] = $this->formatLinks($p_result->getTemplates()); + if(isset($prop['images'])) + $result_array['images'] = array_keys($p_result->getImages()); + if(isset($prop['externallinks'])) + $result_array['externallinks'] = array_keys($p_result->getExternalLinks()); + if(isset($prop['sections'])) + $result_array['sections'] = $p_result->getSections(); + + $result_mapping = array( + 'langlinks' => 'll', + 'categories' => 'cl', + 'links' => 'pl', + 'templates' => 'tl', + 'images' => 'img', + 'externallinks' => 'el', + 'sections' => 's', + ); + $this->setIndexedTagNames( $result_array, $result_mapping ); + $result->addValue( null, $this->getModuleName(), $result_array ); + } + + private function formatLangLinks( $links ) { + $result = array(); + foreach( $links as $link ) { + $entry = array(); + $bits = split( ':', $link, 2 ); + $entry['lang'] = $bits[0]; + $this->getResult()->setContent( $entry, $bits[1] ); + $result[] = $entry; + } + return $result; + } + + private function formatCategoryLinks( $links ) { + $result = array(); + foreach( $links as $link => $sortkey ) { + $entry = array(); + $entry['sortkey'] = $sortkey; + $this->getResult()->setContent( $entry, $link ); + $result[] = $entry; + } + return $result; + } + + private function formatLinks( $links ) { + $result = array(); + foreach( $links as $ns => $nslinks ) { + foreach( $nslinks as $title => $id ) { + $entry = array(); + $entry['ns'] = $ns; + $this->getResult()->setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() ); + if( $id != 0 ) + $entry['exists'] = ''; + $result[] = $entry; + } + } + return $result; + } + + private function setIndexedTagNames( &$array, $mapping ) { + foreach( $mapping as $key => $name ) { + if( isset( $array[$key] ) ) + $this->getResult()->setIndexedTagName( $array[$key], $name ); + } + } + + public function getAllowedParams() { + return array ( + 'title' => array( + ApiBase :: PARAM_DFLT => 'API', + ), + 'text' => null, + 'page' => null, + 'prop' => array( + ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections', + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array( + 'text', + 'langlinks', + 'categories', + 'links', + 'templates', + 'images', + 'externallinks', + 'sections' + ) + ) + ); + } + + public function getParamDescription() { + return array ( + 'text' => 'Wikitext to parse', + 'title' => 'Title of page the text belongs to', + 'page' => 'Parse the content of this page. Cannot be used together with text and title', + 'prop' => array('Which pieces of information to get.', + 'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present' + ), + ); + } + + public function getDescription() { + return 'This module parses wikitext and returns parser output'; + } + + protected function getExamples() { + return array ( + 'api.php?action=parse&text={{Project:Sandbox}}' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiParse.php 30262 2008-01-29 14:47:27Z catrope $'; + } +} + diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php new file mode 100644 index 00000000..40a4b73d --- /dev/null +++ b/includes/api/ApiProtect.php @@ -0,0 +1,154 @@ +<?php + +/* + * Created on Sep 1, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiProtect extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + $titleObj = NULL; + if(!isset($params['title'])) + $this->dieUsageMsg(array('missingparam', 'title')); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!isset($params['protections']) || empty($params['protections'])) + $this->dieUsageMsg(array('missingparam', 'protections')); + + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + + $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser); + if(!empty($errors)) + // We don't care about multiple errors, just report one of them + $this->dieUsageMsg(current($errors)); + + if(in_array($params['expiry'], array('infinite', 'indefinite', 'never'))) + $expiry = Block::infinity(); + else + { + $expiry = strtotime($params['expiry']); + if($expiry < 0 || $expiry == false) + $this->dieUsageMsg(array('invalidexpiry')); + + $expiry = wfTimestamp(TS_MW, $expiry); + if($expiry < wfTimestampNow()) + $this->dieUsageMsg(array('pastexpiry')); + } + + $protections = array(); + foreach($params['protections'] as $prot) + { + $p = explode('=', $prot); + $protections[$p[0]] = ($p[1] == 'all' ? '' : $p[1]); + if($titleObj->exists() && $p[0] == 'create') + $this->dieUsageMsg(array('create-titleexists')); + if(!$titleObj->exists() && $p[0] != 'create') + $this->dieUsageMsg(array('missingtitles-createonly')); + } + + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + if($titleObj->exists()) { + $articleObj = new Article($titleObj); + $ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry); + } else + $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiry); + if(!$ok) + // This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime? + // Just throw an unknown error in this case, as it's very likely to be a race condition + $this->dieUsageMsg(array()); + $dbw->commit(); + $res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']); + if($expiry == Block::infinity()) + $res['expiry'] = 'infinity'; + else + $res['expiry'] = wfTimestamp(TS_ISO_8601, $expiry); + + if($params['cascade']) + $res['cascade'] = ''; + $res['protections'] = $protections; + $this->getResult()->addValue(null, $this->getModuleName(), $res); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'title' => null, + 'token' => null, + 'protections' => array( + ApiBase :: PARAM_ISMULTI => true + ), + 'expiry' => 'infinite', + 'reason' => '', + 'cascade' => false + ); + } + + public function getParamDescription() { + return array ( + 'title' => 'Title of the page you want to restore.', + 'token' => 'A protect token previously retrieved through prop=info', + 'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)', + 'expiry' => 'Expiry timestamp. If set to \'infinite\', \'indefinite\' or \'never\', the protection will never expire.', + 'reason' => 'Reason for (un)protecting (optional)', + 'cascade' => 'Enable cascading protection (i.e. protect pages included in this page)' + ); + } + + public function getDescription() { + return array( + 'Change the protection level of a page.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000', + 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=all|move=all&reason=Lifting%20restrictions' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiProtect.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 76dbb338..29abd859 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -60,9 +60,12 @@ class ApiQuery extends ApiBase { private $mQueryListModules = array ( 'allpages' => 'ApiQueryAllpages', 'alllinks' => 'ApiQueryAllLinks', + 'allcategories' => 'ApiQueryAllCategories', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', + 'blocks' => 'ApiQueryBlocks', 'categorymembers' => 'ApiQueryCategoryMembers', + 'deletedrevs' => 'ApiQueryDeletedrevs', 'embeddedin' => 'ApiQueryBacklinks', 'imageusage' => 'ApiQueryBacklinks', 'logevents' => 'ApiQueryLogEvents', @@ -71,11 +74,14 @@ class ApiQuery extends ApiBase { 'usercontribs' => 'ApiQueryContributions', 'watchlist' => 'ApiQueryWatchlist', 'exturlusage' => 'ApiQueryExtLinksUsage', + 'users' => 'ApiQueryUsers', + 'random' => 'ApiQueryRandom', ); private $mQueryMetaModules = array ( 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', + 'allmessages' => 'ApiQueryAllmessages', ); private $mSlaveDB = null; @@ -143,6 +149,13 @@ class ApiQuery extends ApiBase { public function getPageSet() { return $this->mPageSet; } + + /** + * Get the array mapping module names to class names + */ + function getModules() { + return array_merge($this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules); + } /** * Query execution happens in the following steps: @@ -375,7 +388,7 @@ class ApiQuery extends ApiBase { * Returns the list of allowed parameters for this module. * Qurey module also lists all ApiPageSet parameters as its own. */ - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, @@ -456,8 +469,13 @@ class ApiQuery extends ApiBase { $psModule = new ApiPageSet($this); return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters(); } + + // @todo should work correctly + public function shouldCheckMaxlag() { + return true; + } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => 'Which properties to get for the titles/revisions/pageids', 'list' => 'Which lists to get', @@ -468,7 +486,7 @@ class ApiQuery extends ApiBase { ); } - protected function getDescription() { + public function getDescription() { return array ( 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,', 'and is loosely based on the Query API interface currently available on all MediaWiki servers.', @@ -485,7 +503,7 @@ class ApiQuery extends ApiBase { public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 24494 2007-07-31 17:53:37Z yurik $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 30222 2008-01-28 19:05:26Z catrope $'; $vers[] = $psModule->getVersion(); return $vers; } diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php new file mode 100644 index 00000000..84494876 --- /dev/null +++ b/includes/api/ApiQueryAllCategories.php @@ -0,0 +1,142 @@ +<?php + +/* + * Created on December 12, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to enumerate all categories, even the ones that don't have + * category pages. + * + * @addtogroup API + */ +class ApiQueryAllCategories extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ac'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $db = $this->getDB(); + $params = $this->extractRequestParams(); + + $this->addTables('categorylinks'); + $this->addFields('cl_to'); + + if (!is_null($params['from'])) + $this->addWhere('cl_to>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + if (isset ($params['prefix'])) + $this->addWhere("cl_to LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + + $this->addOption('LIMIT', $params['limit']+1); + $this->addOption('ORDER BY', 'cl_to' . ($params['dir'] == 'descending' ? ' DESC' : '')); + $this->addOption('DISTINCT'); + + $res = $this->select(__METHOD__); + + $pages = array(); + $count = 0; + while ($row = $db->fetchObject($res)) { + if (++ $count > $params['limit']) { + // We've reached the one extra which shows that there are additional cats to be had. Stop here... + // TODO: Security issue - if the user has no right to view next title, it will still be shown + $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->cl_to)); + break; + } + + // Normalize titles + $titleObj = Title::makeTitle(NS_CATEGORY, $row->cl_to); + if(!is_null($resultPageSet)) + $pages[] = $titleObj->getPrefixedText(); + else + // Don't show "Category:" everywhere in non-generator mode + $pages[] = $titleObj->getText(); + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($pages, 'c'); + $result->addValue('query', $this->getModuleName(), $pages); + } else { + $resultPageSet->populateFromTitles($pages); + } + } + + public function getAllowedParams() { + return array ( + 'from' => null, + 'prefix' => null, + 'dir' => array( + ApiBase :: PARAM_DFLT => 'ascending', + ApiBase :: PARAM_TYPE => array( + 'ascending', + 'descending' + ), + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + public function getParamDescription() { + return array ( + 'from' => 'The category to start enumerating from.', + 'prefix' => 'Search for all category titles that begin with this value.', + 'dir' => 'Direction to sort in.', + 'limit' => 'How many categories to return.' + ); + } + + public function getDescription() { + return 'Enumerate all categories'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&generator=allcategories&gacprefix=List&prop=info', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 28216 2007-12-06 18:33:18Z vasilievvv $'; + } +} diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 17f24b65..d5b80644 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -125,7 +125,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'from' => null, 'prefix' => null, @@ -152,7 +152,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'from' => 'The page title to start enumerating from.', 'prefix' => 'Search for all page titles that begin with this value.', @@ -163,7 +163,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ); } - protected function getDescription() { + public function getDescription() { return 'Enumerate all links that point to a given namespace'; } @@ -174,6 +174,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllLinks.php 24453 2007-07-30 08:09:15Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 92bcc1a1..e055b3c5 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -48,8 +48,9 @@ class ApiQueryAllUsers extends ApiQueryBase { $prop = array_flip($prop); $fld_editcount = isset($prop['editcount']); $fld_groups = isset($prop['groups']); + $fld_registration = isset($prop['registration']); } else { - $fld_editcount = $fld_groups = false; + $fld_editcount = $fld_groups = $fld_registration = false; } $limit = $params['limit']; @@ -80,6 +81,9 @@ class ApiQueryAllUsers extends ApiQueryBase { } else { $sqlLimit = $limit+1; } + + if ($fld_registration) + $this->addFields('user_registration'); $this->addOption('LIMIT', $sqlLimit); $this->addTables($tables); @@ -129,6 +133,8 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData = array( 'name' => $lastUser ); if ($fld_editcount) $lastUserData['editcount'] = intval($row->user_editcount); + if ($fld_registration) + $lastUserData['registration'] = wfTimestamp(TS_ISO_8601, $row->user_registration); } @@ -152,7 +158,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $result->addValue('query', $this->getModuleName(), $data); } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'from' => null, 'prefix' => null, @@ -164,6 +170,7 @@ class ApiQueryAllUsers extends ApiQueryBase { ApiBase :: PARAM_TYPE => array ( 'editcount', 'groups', + 'registration', ) ), 'limit' => array ( @@ -176,7 +183,7 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'from' => 'The user name to start enumerating from.', 'prefix' => 'Search for all page titles that begin with this value.', @@ -188,7 +195,7 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } - protected function getDescription() { + public function getDescription() { return 'Enumerate all registered users'; } @@ -199,6 +206,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllUsers.php 24870 2007-08-17 13:01:35Z robchurch $'; + return __CLASS__ . ': $Id: ApiQueryAllUsers.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php new file mode 100644 index 00000000..b7c86a91 --- /dev/null +++ b/includes/api/ApiQueryAllmessages.php @@ -0,0 +1,129 @@ +<?php + +/* + * Created on Dec 1, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * A query action to return messages from site message cache + * + * @addtogroup API + */ +class ApiQueryAllmessages extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'am'); + } + + public function execute() { + global $wgMessageCache; + $params = $this->extractRequestParams(); + + if(!is_null($params['lang'])) + { + global $wgLang; + $wgLang = Language::factory($params['lang']); + } + + + //Determine which messages should we print + $messages_target = array(); + if( $params['messages'] == '*' ) { + $wgMessageCache->loadAllMessages(); + $message_names = array_keys( array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ) ); + sort( $message_names ); + $messages_target = $message_names; + } else { + $messages_target = explode( '|', $params['messages'] ); + } + + //Filter messages + if( isset( $params['filter'] ) ) { + $messages_filtered = array(); + foreach( $messages_target as $message ) { + if( strpos( $message, $params['filter'] ) !== false ) { //!== is used because filter can be at the beginnig of the string + $messages_filtered[] = $message; + } + } + $messages_target = $messages_filtered; + } + + $wgMessageCache->disableTransform(); + + //Get all requested messages + $messages = array(); + foreach( $messages_target as $message ) { + $message = trim( $message ); //Message list can be formatted like "msg1 | msg2 | msg3", so let's trim() it + $messages[$message] = wfMsg( $message ); + } + + //Print the result + $result = $this->getResult(); + $messages_out = array(); + foreach( $messages as $name => $value ) { + $message = array(); + $message['name'] = $name; + $result->setContent( $message, $value ); + $messages_out[] = $message; + } + $result->setIndexedTagName( $messages_out, 'message' ); + $result->addValue( 'query', $this->getModuleName(), $messages_out ); + } + + public function getAllowedParams() { + return array ( + 'messages' => array ( + ApiBase :: PARAM_DFLT => '*', + ), + 'filter' => array(), + 'lang' => null, + ); + } + + public function getParamDescription() { + return array ( + 'messages' => 'Which messages to output. "*" means all messages', + 'filter' => 'Return only messages that contains specified string', + 'lang' => 'Language code', + ); + } + + public function getDescription() { + return 'Return messages from this site.'; + } + + protected function getExamples() { + return array( + 'api.php?action=query&meta=allmessages&amfilter=ipb-', + 'api.php?action=query&meta=allmessages&ammessages=august|mainpage&amlang=de', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllmessages.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index d9715b1a..280d1de2 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -86,6 +86,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $prlevel = $params['prlevel']; if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*') $this->addWhereFld('pr_level', $prlevel); + + $this->addOption('DISTINCT'); $forceNameTitleIndex = false; @@ -93,10 +95,22 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->dieUsage('prlevel may not be used without prtype', 'params'); } - $this->addTables('page'); + if($params['filterlanglinks'] == 'withoutlanglinks') { + $pageName = $this->getDB()->tableName('page'); + $llName = $this->getDB()->tableName('langlinks'); + $tables = "$pageName LEFT JOIN $llName ON page_id=ll_from"; + $this->addWhere('ll_from IS NULL'); + $this->addTables($tables); + $forceNameTitleIndex = false; + } else if($params['filterlanglinks'] == 'withlanglinks') { + $this->addTables(array('page', 'langlinks')); + $this->addWhere('page_id=ll_from'); + $forceNameTitleIndex = false; + } else { + $this->addTables('page'); + } if ($forceNameTitleIndex) $this->addOption('USE INDEX', 'name_title'); - if (is_null($resultPageSet)) { $this->addFields(array ( @@ -110,7 +124,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $limit = $params['limit']; $this->addOption('LIMIT', $limit+1); - $this->addOption('ORDER BY', 'page_namespace, page_title'); + $this->addOption('ORDER BY', 'page_namespace, page_title' . + ($params['dir'] == 'descending' ? ' DESC' : '')); $res = $this->select(__METHOD__); @@ -143,7 +158,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { global $wgRestrictionTypes, $wgRestrictionLevels; return array ( @@ -169,9 +184,11 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ), 'prtype' => array ( ApiBase :: PARAM_TYPE => $wgRestrictionTypes, + ApiBase :: PARAM_ISMULTI => true ), 'prlevel' => array ( ApiBase :: PARAM_TYPE => $wgRestrictionLevels, + ApiBase :: PARAM_ISMULTI => true ), 'limit' => array ( ApiBase :: PARAM_DFLT => 10, @@ -179,25 +196,42 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'ascending', + ApiBase :: PARAM_TYPE => array ( + 'ascending', + 'descending' + ) + ), + 'filterlanglinks' => array( + ApiBase :: PARAM_TYPE => array( + 'withlanglinks', + 'withoutlanglinks', + 'all' + ), + ApiBase :: PARAM_DFLT => 'all' ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'from' => 'The page title to start enumerating from.', 'prefix' => 'Search for all page titles that begin with this value.', 'namespace' => 'The namespace to enumerate.', 'filterredir' => 'Which pages to list.', + 'dir' => 'The direction in which to list', 'minsize' => 'Limit to pages with at least this many bytes', 'maxsize' => 'Limit to pages with at most this many bytes', 'prtype' => 'Limit to protected pages only', 'prlevel' => 'The protection level (must be used with apprtype= parameter)', + 'filterlanglinks' => 'Filter based on whether a page has langlinks', 'limit' => 'How many total pages to return.' ); } - protected function getDescription() { + public function getDescription() { return 'Enumerate all pages sequentially in a given namespace'; } @@ -215,7 +249,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 24694 2007-08-09 08:41:58Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index a676b4bf..1ca5c33a 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -179,7 +179,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $db->freeResult($res); - if (is_null($resultPageSet) && !empty($data)) { + if (is_null($resultPageSet)) { $result = $this->getResult(); $result->setIndexedTagName($data, $this->bl_code); $result->addValue('query', $this->getModuleName(), $data); @@ -315,7 +315,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { '|' . $lastPageID; } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'title' => null, @@ -343,7 +343,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.', 'continue' => 'When more results are available, use this to continue.', @@ -354,7 +354,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ); } - protected function getDescription() { + public function getDescription() { switch ($this->getModuleName()) { case 'backlinks' : return 'Find all pages that link to the given page'; @@ -387,7 +387,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBacklinks.php 25476 2007-09-04 14:44:46Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index c810cfa7..031e3c02 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -110,8 +110,9 @@ abstract class ApiQueryBase extends ApiBase { if (!is_null($end)) $this->addWhere($field . $before . $db->addQuotes($end)); - - $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC')); + + if (!isset($this->options['ORDER BY'])) + $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC')); } protected function addOption($name, $value = null) { @@ -230,7 +231,7 @@ abstract class ApiQueryBase extends ApiBase { } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 24533 2007-08-01 22:46:22Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 31484 2008-03-03 05:46:20Z brion $'; } } diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php new file mode 100644 index 00000000..165792b5 --- /dev/null +++ b/includes/api/ApiQueryBlocks.php @@ -0,0 +1,239 @@ +<?php + +/* + * Created on Sep 10, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to enumerate all available pages. + * + * @addtogroup API + */ +class ApiQueryBlocks extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'bk'); + } + + public function execute() { + $this->run(); + } + + private function run() { + global $wgUser; + + $params = $this->extractRequestParams(); + $prop = array_flip($params['prop']); + $fld_id = isset($prop['id']); + $fld_user = isset($prop['user']); + $fld_by = isset($prop['by']); + $fld_timestamp = isset($prop['timestamp']); + $fld_expiry = isset($prop['expiry']); + $fld_reason = isset($prop['reason']); + $fld_range = isset($prop['range']); + $fld_flags = isset($prop['flags']); + + $result = $this->getResult(); + $pageSet = $this->getPageSet(); + $titles = $pageSet->getTitles(); + $data = array(); + + $this->addTables('ipblocks'); + if($fld_id) + $this->addFields('ipb_id'); + if($fld_user) + $this->addFields(array('ipb_address', 'ipb_user')); + if($fld_by) + { + $this->addTables('user'); + $this->addFields(array('ipb_by', 'user_name')); + $this->addWhere('user_id = ipb_by'); + } + if($fld_timestamp) + $this->addFields('ipb_timestamp'); + if($fld_expiry) + $this->addFields('ipb_expiry'); + if($fld_reason) + $this->addFields('ipb_reason'); + if($fld_range) + $this->addFields(array('ipb_range_start', 'ipb_range_end')); + if($fld_flags) + $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted')); + + $this->addOption('LIMIT', $params['limit'] + 1); + $this->addWhereRange('ipb_timestamp', $params['dir'], $params['start'], $params['end']); + if(isset($params['ids'])) + $this->addWhere(array('ipb_id' => $params['ids'])); + if(isset($params['users'])) + $this->addWhere(array('ipb_address' => $params['users'])); + if(!$wgUser->isAllowed('oversight')) + $this->addWhere(array('ipb_deleted' => 0)); + + // Purge expired entries on one in every 10 queries + if(!mt_rand(0, 10)) + Block::purgeExpired(); + + $res = $this->select(__METHOD__); + $db = wfGetDB(); + + $count = 0; + while($row = $db->fetchObject($res)) + { + if($count++ == $params['limit']) + { + // We've had enough + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp)); + break; + } + $block = array(); + if($fld_id) + $block['id'] = $row->ipb_id; + if($fld_user && !$row->ipb_auto) + { + $block['user'] = $row->ipb_address; + } + if($fld_by) + { + $block['by'] = $row->user_name; + } + if($fld_timestamp) + $block['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ipb_timestamp); + if($fld_expiry) + $block['expiry'] = Block::decodeExpiry($row->ipb_expiry, TS_ISO_8601); + if($fld_reason) + $block['reason'] = $row->ipb_reason; + if($fld_range) + { + $block['rangestart'] = $this->convertHexIP($row->ipb_range_start); + $block['rangeend'] = $this->convertHexIP($row->ipb_range_end); + } + if($fld_flags) + { + // For clarity, these flags use the same names as their action=block counterparts + if($row->ipb_auto) + $block['automatic'] = ''; + if($row->ipb_anon_only) + $block['anononly'] = ''; + if($row->ipb_create_account) + $block['nocreate'] = ''; + if($row->ipb_enable_autoblock) + $block['autoblock'] = ''; + if($row->ipb_block_email) + $block['noemail'] = ''; + if($row->ipb_deleted) + $block['hidden'] = ''; + } + $data[] = $block; + } + $result->setIndexedTagName($data, 'block'); + $result->addValue('query', $this->getModuleName(), $data); + } + + protected function convertHexIP($ip) + { + // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format + $dec = wfBaseConvert($ip, 16, 10); + $parts[0] = (int)($dec / (256*256*256)); + $dec %= 256*256*256; + $parts[1] = (int)($dec / (256*256)); + $dec %= 256*256; + $parts[2] = (int)($dec / 256); + $parts[3] = $dec % 256; + return implode('.', $parts); + } + + public function getAllowedParams() { + return array ( + 'start' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase :: PARAM_TYPE => 'timestamp', + ), + 'dir' => array( + ApiBase :: PARAM_TYPE => array( + 'newer', + 'older' + ), + ApiBase :: PARAM_DFLT => 'older' + ), + 'ids' => array( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_ISMULTI => true + ), + 'users' => array( + ApiBase :: PARAM_ISMULTI => true + ), + 'limit' => array( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'prop' => array( + ApiBase :: PARAM_DFLT => 'id|user|by|timestamp|expiry|reason|flags', + ApiBase :: PARAM_TYPE => array( + 'id', + 'user', + 'by', + 'timestamp', + 'expiry', + 'reason', + 'range', + 'flags' + ), + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'start' => 'The timestamp to start enumerating from', + 'end' => 'The timestamp to stop enumerating at', + 'dir' => 'The direction in which to enumerate', + 'ids' => 'Pipe-separated list of block IDs to list (optional)', + 'users' => 'Pipe-separated list of users to search for (optional)', + 'limit' => 'The maximum amount of blocks to list', + 'prop' => 'Which properties to get', + ); + } + + public function getDescription() { + return 'List all blocked users and IP addresses.'; + } + + protected function getExamples() { + return array ( + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryBlocks.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 42bc1c38..63d42bfa 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -120,7 +120,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $db->freeResult($res); } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, @@ -131,13 +131,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => 'Which additional properties to get for each category.', ); } - protected function getDescription() { + public function getDescription() { return 'List all categories the page(s) belong to'; } @@ -151,7 +151,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategories.php 24092 2007-07-14 19:04:31Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryCategories.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 58a454a5..e831f291 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -51,12 +51,18 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); - $category = $params['category']; - if (is_null($category)) - $this->dieUsage("Category parameter is required", 'param_category'); - $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category ); - if ( is_null( $categoryTitle ) ) - $this->dieUsage("Category name $category is not valid", 'param_category'); + if (is_null($params['category'])) { + if (is_null($params['title'])) + $this->dieUsage("Either the cmcategory or the cmtitle parameter is required", 'notitle'); + else + $categoryTitle = Title::newFromText($params['title']); + } else if(is_null($params['title'])) + $categoryTitle = Title::makeTitleSafe(NS_CATEGORY, $params['category']); + else + $this->dieUsage("The cmcategory and cmtitle parameters can't be used together", 'titleandcategory'); + + if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) + $this->dieUsage("The category name you entered is not valid", 'invalidcategory'); $prop = array_flip($params['prop']); $fld_ids = isset($prop['ids']); @@ -78,18 +84,19 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if($params['sort'] == 'timestamp') { $this->addOption('USE INDEX', 'cl_timestamp'); - $this->addOption('ORDER BY', 'cl_to, cl_timestamp'); + $this->addOption('ORDER BY', 'cl_to, cl_timestamp' . ($params['dir'] == 'desc' ? ' DESC' : '')); } else { $this->addOption('USE INDEX', 'cl_sortkey'); - $this->addOption('ORDER BY', 'cl_to, cl_sortkey, cl_from'); + $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . ($params['dir'] == 'desc' ? ' DESC' : '') . ', cl_from'); } $this->addWhere('cl_from=page_id'); $this->setContinuation($params['continue']); $this->addWhereFld('cl_to', $categoryTitle->getDBkey()); $this->addWhereFld('page_namespace', $params['namespace']); + $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']); $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); @@ -172,9 +179,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( - 'category' => null, + 'title' => null, + 'category' => null, // DEPRECATED, will be removed in early March 'prop' => array ( ApiBase :: PARAM_DFLT => 'ids|title', ApiBase :: PARAM_ISMULTI => true, @@ -203,36 +211,53 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'sortkey', 'timestamp' ) + ), + 'dir' => array( + ApiBase :: PARAM_DFLT => 'asc', + ApiBase :: PARAM_TYPE => array( + 'asc', + 'desc' + ) + ), + 'start' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase :: PARAM_TYPE => 'timestamp' ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( - 'category' => 'Which category to enumerate (required)', + 'title' => 'Which category to enumerate (required). Must include Category: prefix', 'prop' => 'What pieces of information to include', 'namespace' => 'Only include pages in these namespaces', 'sort' => 'Property to sort by', + 'dir' => 'In which direction to sort', + 'start' => 'Timestamp to start listing from', + 'end' => 'Timestamp to end listing at', 'continue' => 'For large categories, give the value retured from previous query', 'limit' => 'The maximum number of pages to return.', + 'category' => 'DEPRECATED. Like title, but without the Category: prefix.', ); } - protected function getDescription() { + public function getDescription() { return 'List all pages in a given category'; } protected function getExamples() { return array ( - "Get first 10 pages in the categories [[Physics]]:", - " api.php?action=query&list=categorymembers&cmcategory=Physics", - "Get page info about first 10 pages in the categories [[Physics]]:", - " api.php?action=query&generator=categorymembers&gcmcategory=Physics&prop=info", + "Get first 10 pages in [[Category:Physics]]:", + " api.php?action=query&list=categorymembers&cmtitle=Category:Physics", + "Get page info about first 10 pages in [[Category:Physics]]:", + " api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info", ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 25474 2007-09-04 14:30:31Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 30670 2008-02-07 15:17:42Z catrope $'; } } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php new file mode 100644 index 00000000..1b7fbdb0 --- /dev/null +++ b/includes/api/ApiQueryDeletedrevs.php @@ -0,0 +1,235 @@ +<?php + +/* + * Created on Jul 2, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to enumerate all available pages. + * + * @addtogroup API + */ +class ApiQueryDeletedrevs extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'dr'); + } + + public function execute() { + + global $wgUser; + // Before doing anything at all, let's check permissions + if(!$wgUser->isAllowed('deletedhistory')) + $this->dieUsage('You don\'t have permission to view deleted revision information', 'permissiondenied'); + + $db = $this->getDB(); + $params = $this->extractRequestParams(false); + $prop = array_flip($params['prop']); + $fld_revid = isset($prop['revid']); + $fld_user = isset($prop['user']); + $fld_comment = isset($prop['comment']); + $fld_minor = isset($prop['minor']); + $fld_len = isset($prop['len']); + $fld_content = isset($prop['content']); + $fld_token = isset($prop['token']); + + $result = $this->getResult(); + $pageSet = $this->getPageSet(); + $titles = $pageSet->getTitles(); + $data = array(); + + $this->addTables('archive'); + $this->addFields(array('ar_title', 'ar_namespace', 'ar_timestamp')); + if($fld_revid) + $this->addFields('ar_rev_id'); + if($fld_user) + $this->addFields('ar_user_text'); + if($fld_comment) + $this->addFields('ar_comment'); + if($fld_minor) + $this->addFields('ar_minor_edit'); + if($fld_len) + $this->addFields('ar_len'); + if($fld_content) + { + $this->addTables('text'); + $this->addFields(array('ar_text', 'ar_text_id', 'old_text', 'old_flags')); + $this->addWhere('ar_text_id = old_id'); + + // This also means stricter restrictions + if(!$wgUser->isAllowed('undelete')) + $this->dieUsage('You don\'t have permission to view deleted revision content', 'permissiondenied'); + } + // Check limits + $userMax = $fld_content ? ApiBase :: LIMIT_SML1 : ApiBase :: LIMIT_BIG1; + $botMax = $fld_content ? ApiBase :: LIMIT_SML2 : ApiBase :: LIMIT_BIG2; + if( $limit == 'max' ) { + $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; + $this->getResult()->addValue( 'limits', 'limit', $limit ); + } + $this->validateLimit('limit', $params['limit'], 1, $userMax, $botMax); + if($fld_token) + // Undelete tokens are identical for all pages, so we cache one here + $token = $wgUser->editToken(); + + // We need a custom WHERE clause that matches all titles. + if(count($titles) > 0) + { + $lb = new LinkBatch($titles); + $where = $lb->constructSet('ar', $db); + $this->addWhere($where); + } + + $this->addOption('LIMIT', $params['limit'] + 1); + $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']); + if(isset($params['namespace'])) + $this->addWhereFld('ar_namespace', $params['namespace']); + $res = $this->select(__METHOD__); + $pages = array(); + $count = 0; + // First populate the $pages array + while($row = $db->fetchObject($res)) + { + if($count++ == $params['limit']) + { + // We've had enough + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp)); + break; + } + + $rev = array(); + $rev['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ar_timestamp); + if($fld_revid) + $rev['revid'] = $row->ar_rev_id; + if($fld_user) + $rev['user'] = $row->ar_user_text; + if($fld_comment) + $rev['comment'] = $row->ar_comment; + if($fld_minor) + if($row->ar_minor_edit == 1) + $rev['minor'] = ''; + if($fld_len) + $rev['len'] = $row->ar_len; + if($fld_content) + ApiResult::setContent($rev, Revision::getRevisionText($row)); + + $t = Title::makeTitle($row->ar_namespace, $row->ar_title); + if(!isset($pages[$t->getPrefixedText()])) + { + $pages[$t->getPrefixedText()] = array( + 'title' => $t->getPrefixedText(), + 'ns' => intval($row->ar_namespace), + 'revisions' => array($rev) + ); + if($fld_token) + $pages[$t->getPrefixedText()]['token'] = $token; + } + else + $pages[$t->getPrefixedText()]['revisions'][] = $rev; + } + $db->freeResult($res); + + // We don't want entire pagenames as keys, so let's make this array indexed + foreach($pages as $page) + { + $result->setIndexedTagName($page['revisions'], 'rev'); + $data[] = $page; + } + $result->setIndexedTagName($data, 'page'); + $result->addValue('query', $this->getModuleName(), $data); + } + + public function getAllowedParams() { + return array ( + 'start' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase :: PARAM_TYPE => 'timestamp', + ), + 'dir' => array( + ApiBase :: PARAM_TYPE => array( + 'newer', + 'older' + ), + ApiBase :: PARAM_DFLT => 'older' + ), + 'namespace' => array( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'limit' => array( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'prop' => array( + ApiBase :: PARAM_DFLT => 'user|comment', + ApiBase :: PARAM_TYPE => array( + 'revid', + 'user', + 'comment', + 'minor', + 'len', + 'content', + 'token' + ), + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'start' => 'The timestamp to start enumerating from', + 'end' => 'The timestamp to stop enumerating at', + 'dir' => 'The direction in which to enumerate', + 'namespace' => 'The namespaces to search in', + 'limit' => 'The maximum amount of revisions to list', + 'prop' => 'Which properties to get' + ); + } + + public function getDescription() { + return 'List deleted revisions.'; + } + + protected function getExamples() { + return array ( + 'List the first 50 deleted revisions in the Category and Category talk namespaces', + ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=14|15', + 'List the last deleted revisions of Main Page and Talk:Main Page, with content:', + ' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 385ae65b..896a0171 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -134,7 +134,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { global $wgUrlProtocols; $protocols = array(); foreach ($wgUrlProtocols as $p) { @@ -173,7 +173,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => 'What pieces of information to include', 'offset' => 'Used for paging. Use the value returned for "continue"', @@ -184,7 +184,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ); } - protected function getDescription() { + public function getDescription() { return 'Enumerate pages that contain a given URL'; } @@ -195,6 +195,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 24694 2007-08-09 08:41:58Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 440b31d6..07183910 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -75,7 +75,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { $db->freeResult($res); } - protected function getDescription() { + public function getDescription() { return 'Returns all external urls (not interwikies) from the given page(s)'; } @@ -87,7 +87,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 23819 2007-07-07 03:05:09Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 3d568ba1..3714ccf6 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -42,15 +42,20 @@ class ApiQueryImageInfo extends ApiQueryBase { public function execute() { $params = $this->extractRequestParams(); - $history = $params['history']; - $prop = array_flip($params['prop']); - $fld_timestamp = isset($prop['timestamp']); - $fld_user = isset($prop['user']); - $fld_comment = isset($prop['comment']); - $fld_url = isset($prop['url']); - $fld_size = isset($prop['size']); - $fld_sha1 = isset($prop['sha1']); + $this->fld_timestamp = isset($prop['timestamp']); + $this->fld_user = isset($prop['user']); + $this->fld_comment = isset($prop['comment']); + $this->fld_url = isset($prop['url']); + $this->fld_size = isset($prop['size']); + $this->fld_sha1 = isset($prop['sha1']); + $this->fld_metadata = isset($prop['metadata']); + + if($params['urlheight'] != -1 && $params['urlwidth'] == -1) + $this->dieUsage("iiurlheight cannot be used without iiurlwidth", 'iiurlwidth'); + $this->scale = ($params['urlwidth'] != -1); + $this->urlwidth = $params['urlwidth']; + $this->urlheight = $params['urlheight']; $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); if (!empty($pageIds[NS_IMAGE])) { @@ -65,54 +70,84 @@ class ApiQueryImageInfo extends ApiQueryBase { } else { $repository = $img->getRepoName(); - - $isCur = true; - while($line = $img->nextHistoryLine()) { // assignment - $row = get_object_vars( $line ); - $vals = array(); - $prefix = $isCur ? 'img' : 'oi'; - - if ($fld_timestamp) - $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row["${prefix}_timestamp"]); - if ($fld_user) { - $vals['user'] = $row["${prefix}_user_text"]; - if(!$row["${prefix}_user"]) - $vals['anon'] = ''; - } - if ($fld_size) { - $vals['size'] = intval($row["{$prefix}_size"]); - $vals['width'] = intval($row["{$prefix}_width"]); - $vals['height'] = intval($row["{$prefix}_height"]); - } - if ($fld_url) - $vals['url'] = $isCur ? $img->getURL() : $img->getArchiveUrl($row["oi_archive_name"]); - if ($fld_comment) - $vals['comment'] = $row["{$prefix}_description"]; - - if ($fld_sha1) - $vals['sha1'] = wfBaseConvert($row["{$prefix}_sha1"], 36, 16, 40); - - $data[] = $vals; - - if (!$history) // Stop after the first line. - break; - - $isCur = false; + + // Get information about the current version first + // Check that the current version is within the start-end boundaries + if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) && + (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) { + $data[] = $this->getInfo($img); } - $img->resetHistory(); + // Now get the old revisions + // Get one more to facilitate query-continue functionality + $count = count($data); + $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']); + foreach($oldies as $oldie) { + if(++$count > $params['limit']) { + // We've reached the extra one which shows that there are additional pages to be had. Stop here... + // Only set a query-continue if there was only one title + if(count($pageIds[NS_IMAGE]) == 1) + $this->setContinueEnumParameter('start', $oldie->getTimestamp()); + break; + } + $data[] = $this->getInfo($oldie); + } } - $this->getResult()->addValue(array ('query', 'pages', intval($pageId)), - 'imagerepository', - $repository); - if (!empty($data)) - $this->addPageSubItems($pageId, $data); + $this->getResult()->addValue(array( + 'query', 'pages', intval($pageId)), + 'imagerepository', $repository + ); + if (!empty($data)) + $this->addPageSubItems($pageId, $data); } } } - protected function getAllowedParams() { + /** + * Get result information for an image revision + * @param File f The image + * @return array Result array + */ + protected function getInfo($f) { + $vals = array(); + if($this->fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $f->getTimestamp()); + if($this->fld_user) { + $vals['user'] = $f->getUser(); + if(!$f->getUser('id')) + $vals['anon'] = ''; + } + if($this->fld_size) { + $vals['size'] = intval($f->getSize()); + $vals['width'] = intval($f->getWidth()); + $vals['height'] = intval($f->getHeight()); + } + if($this->fld_url) { + if($this->scale && !$f->isOld()) { + $thumb = $f->getThumbnail($this->urlwidth, $this->urlheight); + if($thumb) + { + $vals['thumburl'] = $thumb->getURL(); + $vals['thumbwidth'] = $thumb->getWidth(); + $vals['thumbheight'] = $thumb->getHeight(); + } + } + $vals['url'] = $f->getURL(); + } + if($this->fld_comment) + $vals['comment'] = $f->getDescription(); + if($this->fld_sha1) + $vals['sha1'] = wfBaseConvert($f->getSha1(), 36, 16, 40); + if($this->fld_metadata) { + $metadata = unserialize($f->getMetadata()); + $vals['metadata'] = $metadata ? $metadata : null; + $this->getResult()->setIndexedTagName_recursive($vals['metadata'], 'meta'); + } + return $vals; + } + + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, @@ -123,21 +158,46 @@ class ApiQueryImageInfo extends ApiQueryBase { 'comment', 'url', 'size', - 'sha1' + 'sha1', + 'metadata' ) ), - 'history' => false, + 'limit' => array( + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_DFLT => 1, + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'start' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'urlwidth' => array( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_DFLT => -1 + ), + 'urlheight' => array( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_DFLT => -1 + ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => 'What image information to get.', - 'history' => 'Include upload history', + 'limit' => 'How many image revisions to return', + 'start' => 'Timestamp to start listing from', + 'end' => 'Timestamp to stop listing at', + 'urlwidth' => 'If iiprop=url is set, a URL to an image scaled to this width will be returned. Only the current version of the image can be scaled.', + 'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth', ); } - protected function getDescription() { + public function getDescription() { return array ( 'Returns image information and upload history' ); @@ -146,11 +206,11 @@ class ApiQueryImageInfo extends ApiQueryBase { protected function getExamples() { return array ( 'api.php?action=query&titles=Image:Albert%20Einstein%20Head.jpg&prop=imageinfo', - 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iihistory&iiprop=timestamp|user|url', + 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iilimit=50&iiend=20071231235959&iiprop=timestamp|user|url', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImageInfo.php 25456 2007-09-03 19:58:05Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 30665 2008-02-07 12:21:48Z catrope $'; } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index d64a653b..f7405374 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -98,7 +98,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $db->freeResult($res); } - protected function getDescription() { + public function getDescription() { return 'Returns all images contained on the given page(s)'; } @@ -112,7 +112,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImages.php 24092 2007-07-14 19:04:31Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryImages.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index bebf4006..2dee22b0 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -40,6 +40,7 @@ class ApiQueryInfo extends ApiQueryBase { } public function requestExtraData($pageSet) { + $pageSet->requestField('page_restrictions'); $pageSet->requestField('page_is_redirect'); $pageSet->requestField('page_is_new'); $pageSet->requestField('page_counter'); @@ -65,11 +66,16 @@ class ApiQueryInfo extends ApiQueryBase { $tok_protect = $this->getTokenFlag($token, 'protect'); $tok_move = $this->getTokenFlag($token, 'move'); } + else + // Fix E_NOTICEs about unset variables + $token = $tok_edit = $tok_delete = $tok_protect = $tok_move = null; $pageSet = $this->getPageSet(); $titles = $pageSet->getGoodTitles(); + $missing = $pageSet->getMissingTitles(); $result = $this->getResult(); + $pageRestrictions = $pageSet->getCustomField('page_restrictions'); $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); $pageIsNew = $pageSet->getCustomField('page_is_new'); $pageCounter = $pageSet->getCustomField('page_counter'); @@ -77,23 +83,46 @@ class ApiQueryInfo extends ApiQueryBase { $pageLatest = $pageSet->getCustomField('page_latest'); $pageLength = $pageSet->getCustomField('page_len'); - if ($fld_protection && count($titles) > 0) { + $db = $this->getDB(); + if ($fld_protection && !empty($titles)) { $this->addTables('page_restrictions'); - $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry')); + $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade')); $this->addWhereFld('pr_page', array_keys($titles)); - $db = $this->getDB(); $res = $this->select(__METHOD__); while($row = $db->fetchObject($res)) { - $protections[$row->pr_page][] = array( - 'type' => $row->pr_type, - 'level' => $row->pr_level, - 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ) - ); + $a = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ) + ); + if($row->pr_cascade) + $a['cascade'] = ''; + $protections[$row->pr_page][] = $a; } $db->freeResult($res); } - + // We don't need to check for pt stuff if there are no nonexistent titles + if($fld_protection && !empty($missing)) + { + $this->resetQueryParams(); + // Construct a custom WHERE clause that matches all titles in $missing + $lb = new LinkBatch($missing); + $this->addTables('protected_titles'); + $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry')); + $this->addWhere($lb->constructSet('pt', $db)); + $res = $this->select(__METHOD__); + $prottitles = array(); + while($row = $db->fetchObject($res)) { + $prottitles[$row->pt_namespace][$row->pt_title] = array( + 'type' => 'create', + 'level' => $row->pt_create_perm, + 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601) + ); + } + $db->freeResult($res); + } + foreach ( $titles as $pageid => $title ) { $pageInfo = array ( 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]), @@ -125,7 +154,36 @@ class ApiQueryInfo extends ApiQueryBase { $pageInfo['protection'] = $protections[$pageid]; $result->setIndexedTagName($pageInfo['protection'], 'pr'); } else { - $pageInfo['protection'] = array(); + # Also check old restrictions + if( $pageRestrictions[$pageid] ) { + foreach( explode( ':', trim( $pageRestrictions[$pageid] ) ) as $restrict ) { + $temp = explode( '=', trim( $restrict ) ); + if(count($temp) == 1) { + // old old format should be treated as edit/move restriction + $restriction = trim( $temp[0] ); + $pageInfo['protection'][] = array( + 'type' => 'edit', + 'level' => $restriction, + 'expiry' => 'infinity', + ); + $pageInfo['protection'][] = array( + 'type' => 'move', + 'level' => $restriction, + 'expiry' => 'infinity', + ); + } else { + $restriction = trim( $temp[1] ); + $pageInfo['protection'][] = array( + 'type' => $temp[0], + 'level' => $restriction, + 'expiry' => 'infinity', + ); + } + } + $result->setIndexedTagName($pageInfo['protection'], 'pr'); + } else { + $pageInfo['protection'] = array(); + } } } @@ -135,18 +193,31 @@ class ApiQueryInfo extends ApiQueryBase { ), $pageid, $pageInfo); } - // Get edit tokens for missing titles if requested - // Delete, protect and move tokens are N/A for missing titles anyway - if($tok_edit) + // Get edit/protect tokens and protection data for missing titles if requested + // Delete and move tokens are N/A for missing titles anyway + if($tok_edit || $tok_protect || $fld_protection) { - $missing = $pageSet->getMissingTitles(); - $res = $result->getData(); - foreach($missing as $pageid => $title) - $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken(); + $res = &$result->getData(); + foreach($missing as $pageid => $title) { + if($tok_edit) + $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken(); + if($tok_protect) + $res['query']['pages'][$pageid]['protecttoken'] = $wgUser->editToken(); + if($fld_protection) + { + // Apparently the XML formatting code doesn't like array(null) + // This is painful to fix, so we'll just work around it + if(isset($prottitles[$title->getNamespace()][$title->getDBkey()])) + $res['query']['pages'][$pageid]['protection'][] = $prottitles[$title->getNamespace()][$title->getDBkey()]; + else + $res['query']['pages'][$pageid]['protection'] = array(); + $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr'); + } + } } } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_DFLT => NULL, @@ -166,7 +237,7 @@ class ApiQueryInfo extends ApiQueryBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => array ( 'Which additional properties to get:', @@ -177,7 +248,7 @@ class ApiQueryInfo extends ApiQueryBase { } - protected function getDescription() { + public function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } @@ -189,7 +260,7 @@ class ApiQueryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 25457 2007-09-03 20:17:53Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index ae5ff790..04a930db 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -76,7 +76,7 @@ class ApiQueryLangLinks extends ApiQueryBase { $db->freeResult($res); } - protected function getDescription() { + public function getDescription() { return 'Returns all interlanguage links from the given page(s)'; } @@ -88,7 +88,7 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLangLinks.php 23819 2007-07-07 03:05:09Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryLangLinks.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 7ec20f44..d77e627a 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -123,7 +123,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $db->freeResult($res); } - protected function getAllowedParams() + public function getAllowedParams() { return array( 'namespace' => array( @@ -133,14 +133,14 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() + public function getParamDescription() { return array( 'namespace' => "Show {$this->description}s in this namespace(s) only" ); } - protected function getDescription() { + public function getDescription() { return "Returns all {$this->description}s from the given page(s)"; } @@ -156,7 +156,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLinks.php 24092 2007-07-14 19:04:31Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryLinks.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 0f143658..e25e5275 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -198,7 +198,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } - protected function getAllowedParams() { + public function getAllowedParams() { global $wgLogTypes; return array ( 'prop' => array ( @@ -243,8 +243,9 @@ class ApiQueryLogEvents extends ApiQueryBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( + 'prop' => 'Which properties to get', 'type' => 'Filter log entries to only this type(s)', 'start' => 'The timestamp to start enumerating from.', 'end' => 'The timestamp to end enumerating.', @@ -255,7 +256,7 @@ class ApiQueryLogEvents extends ApiQueryBase { ); } - protected function getDescription() { + public function getDescription() { return 'Get events from logs.'; } @@ -266,7 +267,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLogEvents.php 24256 2007-07-18 21:47:09Z robchurch $'; + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php new file mode 100644 index 00000000..b8282098 --- /dev/null +++ b/includes/api/ApiQueryRandom.php @@ -0,0 +1,157 @@ +<?php + +/* + * Created on Monday, January 28, 2008 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Brent Garber + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to get list of random pages + * + * @addtogroup API + */ + + class ApiQueryRandom extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'rn'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) { + $this->resetQueryParams(); + $this->addTables('page'); + $this->addOption('LIMIT', $limit); + $this->addWhereFld('page_namespace', $namespace); + $this->addWhereRange('page_random', 'newer', $randstr, null); + $this->addWhere(array('page_is_redirect' => 0)); + $this->addOption('USE INDEX', 'page_random'); + if(is_null($resultPageSet)) + $this->addFields(array('page_id', 'page_title', 'page_namespace')); + else + $this->addFields($resultPageSet->getPageTableFields()); + } + + protected function runQuery(&$data, &$resultPageSet) { + $db = $this->getDB(); + $res = $this->select(__METHOD__); + $count = 0; + while($row = $db->fetchObject($res)) { + $count++; + if(is_null($resultPageSet)) + { + // Prevent duplicates + if(!in_array($row->page_id, $this->pageIDs)) + { + $data[] = $this->extractRowInfo($row); + $this->pageIDs[] = $row->page_id; + } + } + else + $resultPageSet->processDbRow($row); + } + $db->freeResult($res); + return $count; + } + + public function run($resultPageSet = null) { + $params = $this->extractRequestParams(); + $result = $this->getResult(); + $data = array(); + $this->pageIDs = array(); + $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet); + $count = $this->runQuery($data, $resultPageSet); + if($count < $params['limit']) + { + /* We got too few pages, we probably picked a high value + * for page_random. We'll just take the lowest ones, see + * also the comment in Title::getRandomTitle() + */ + $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet); + $this->runQuery($data, $resultPageSet); + } + + if(is_null($resultPageSet)) { + $result->setIndexedTagName($data, 'page'); + $result->addValue('query', $this->getModuleName(), $data); + } + } + + private function extractRowInfo($row) { + $title = Title::makeTitle($row->page_namespace, $row->page_title); + $vals = array(); + $vals['title'] = $title->getPrefixedText(); + $vals['ns'] = $row->page_namespace; + $vals['id'] = $row->page_id; + return $vals; + } + + public function getAllowedParams() { + return array ( + 'namespace' => array( + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_ISMULTI => true + ), + 'limit' => array ( + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_DFLT => 1, + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => 10, + ApiBase :: PARAM_MAX2 => 20 + ), + ); + } + + public function getParamDescription() { + return array ( + 'namespace' => 'Return pages in these namespaces only', + 'limit' => 'Limit how many random pages will be returned' + ); + } + + public function getDescription() { + return array( 'Get a set of random pages', + 'NOTE: Pages are listed in a fixed sequence, only the starting point is random. This means that if, for example, "Main Page" is the first ', + ' random page on your list, "List of fictional monkeys" will *always* be second, "List of people on stamps of Vanuatu" third, etc.', + 'NOTE: If the number of pages in the namespace is lower than rnlimit, you will get fewer pages. You will not get the same page twice.' + ); + } + + protected function getExamples() { + return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$'; + } +} diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 309beaf9..44093854 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -44,20 +44,40 @@ class ApiQueryRecentChanges extends ApiQueryBase { $fld_timestamp = false, $fld_title = false, $fld_ids = false, $fld_sizes = false; + /** + * Generates and outputs the result of this query based upon the provided parameters. + */ public function execute() { - $limit = $prop = $namespace = $show = $dir = $start = $end = null; + /* Initialize vars */ + $limit = $prop = $namespace = $show = $type = $dir = $start = $end = null; + + /* Get the parameters of the request. */ extract($this->extractRequestParams()); + /* Build our basic query. Namely, something along the lines of: + * SELECT * from recentchanges WHERE rc_timestamp > $start + * AND rc_timestamp < $end AND rc_namespace = $namespace + * AND rc_deleted = '0' + */ $this->addTables('recentchanges'); $this->addWhereRange('rc_timestamp', $dir, $start, $end); $this->addWhereFld('rc_namespace', $namespace); $this->addWhereFld('rc_deleted', 0); + if(!is_null($type)) + $this->addWhereFld('rc_type', $this->parseRCType($type)); if (!is_null($show)) { $show = array_flip($show); - if ((isset ($show['minor']) && isset ($show['!minor'])) || (isset ($show['bot']) && isset ($show['!bot'])) || (isset ($show['anon']) && isset ($show['!anon']))) + + /* Check for conflicting parameters. */ + if ((isset ($show['minor']) && isset ($show['!minor'])) + || (isset ($show['bot']) && isset ($show['!bot'])) + || (isset ($show['anon']) && isset ($show['!anon']))) { + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + } + /* Add additional conditions to query depending upon parameters. */ $this->addWhereIf('rc_minor = 0', isset ($show['!minor'])); $this->addWhereIf('rc_minor != 0', isset ($show['minor'])); $this->addWhereIf('rc_bot = 0', isset ($show['!bot'])); @@ -66,6 +86,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); } + /* Add the fields we're concerned with to out query. */ $this->addFields(array ( 'rc_timestamp', 'rc_namespace', @@ -75,9 +96,11 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'rc_moved_to_title' )); + /* Determine what properties we need to display. */ if (!is_null($prop)) { $prop = array_flip($prop); + /* Set up internal members based upon params. */ $this->fld_comment = isset ($prop['comment']); $this->fld_user = isset ($prop['user']); $this->fld_flags = isset ($prop['flags']); @@ -85,7 +108,8 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->fld_title = isset ($prop['title']); $this->fld_ids = isset ($prop['ids']); $this->fld_sizes = isset ($prop['sizes']); - + + /* Add fields to our query if they are specified as a needed parameter. */ $this->addFieldsIf('rc_id', $this->fld_ids); $this->addFieldsIf('rc_cur_id', $this->fld_ids); $this->addFieldsIf('rc_this_oldid', $this->fld_ids); @@ -100,14 +124,21 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addFieldsIf('rc_new_len', $this->fld_sizes); } + /* Specify the limit for our query. It's $limit+1 because we (possibly) need to + * generate a "continue" parameter, to allow paging. */ $this->addOption('LIMIT', $limit +1); + + /* Specify the index to use in the query as rc_timestamp, instead of rc_revid (default). */ $this->addOption('USE INDEX', 'rc_timestamp'); $data = array (); $count = 0; + + /* Perform the actual query. */ $db = $this->getDB(); $res = $this->select(__METHOD__); - + + /* Iterate through the rows, adding data extracted from them to our query result. */ while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... @@ -115,33 +146,61 @@ class ApiQueryRecentChanges extends ApiQueryBase { break; } + /* Extract the data from a single row. */ $vals = $this->extractRowInfo($row); + + /* Add that row's data to our final output. */ if($vals) $data[] = $vals; } + $db->freeResult($res); + /* Format the result */ $result = $this->getResult(); $result->setIndexedTagName($data, 'rc'); $result->addValue('query', $this->getModuleName(), $data); } + /** + * Extracts from a single sql row the data needed to describe one recent change. + * + * @param $row The row from which to extract the data. + * @return An array mapping strings (descriptors) to their respective string values. + * @access private + */ private function extractRowInfo($row) { + /* If page was moved somewhere, get the title of the move target. */ $movedToTitle = false; if (!empty($row->rc_moved_to_title)) $movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title); + /* Determine the title of the page that has been changed. */ $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); + + /* Our output data. */ $vals = array (); - $vals['type'] = intval($row->rc_type); + $type = intval ( $row->rc_type ); + /* Determine what kind of change this was. */ + switch ( $type ) { + case RC_EDIT: $vals['type'] = 'edit'; break; + case RC_NEW: $vals['type'] = 'new'; break; + case RC_MOVE: $vals['type'] = 'move'; break; + case RC_LOG: $vals['type'] = 'log'; break; + case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break; + default: $vals['type'] = $type; + } + + /* Create a new entry in the result for the title. */ if ($this->fld_title) { ApiQueryBase :: addTitleInfo($vals, $title); if ($movedToTitle) ApiQueryBase :: addTitleInfo($vals, $movedToTitle, "new_"); } + /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */ if ($this->fld_ids) { $vals['rcid'] = intval($row->rc_id); $vals['pageid'] = intval($row->rc_cur_id); @@ -149,12 +208,14 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals['old_revid'] = intval( $row->rc_last_oldid ); } + /* Add user data and 'anon' flag, if use is anonymous. */ if ($this->fld_user) { $vals['user'] = $row->rc_user_text; if(!$row->rc_user) $vals['anon'] = ''; } + /* Add flags, such as new, minor, bot. */ if ($this->fld_flags) { if ($row->rc_bot) $vals['bot'] = ''; @@ -164,22 +225,42 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals['minor'] = ''; } + /* Add sizes of each revision. (Only available on 1.10+) */ if ($this->fld_sizes) { $vals['oldlen'] = intval($row->rc_old_len); $vals['newlen'] = intval($row->rc_new_len); } - + + /* Add the timestamp. */ if ($this->fld_timestamp) $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); + /* Add edit summary / log summary. */ if ($this->fld_comment && !empty ($row->rc_comment)) { $vals['comment'] = $row->rc_comment; } return $vals; } + + private function parseRCType($type) + { + if(is_array($type)) + { + $retval = array(); + foreach($type as $t) + $retval[] = $this->parseRCType($t); + return $retval; + } + switch($type) + { + case 'edit': return RC_EDIT; + case 'new': return RC_NEW; + case 'log': return RC_LOG; + } + } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'start' => array ( ApiBase :: PARAM_TYPE => 'timestamp' @@ -228,11 +309,19 @@ class ApiQueryRecentChanges extends ApiQueryBase { ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'type' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'edit', + 'new', + 'log' + ) ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'start' => 'The timestamp to start enumerating from.', 'end' => 'The timestamp to end enumerating.', @@ -243,11 +332,12 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'Show only items that meet this criteria.', 'For example, to see only minor edits done by logged-in users, set show=minor|!anon' ), + 'type' => 'Which types of changes to show.', 'limit' => 'How many total pages to return.' ); } - protected function getDescription() { + public function getDescription() { return 'Enumerate recent changes'; } @@ -258,7 +348,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 24100 2007-07-15 01:12:54Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 2672478b..e22d3b30 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -45,14 +45,15 @@ class ApiQueryRevisions extends ApiQueryBase { $fld_comment = false, $fld_user = false, $fld_content = false; public function execute() { - $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = null; - extract($this->extractRequestParams()); + $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $token = null; + extract($this->extractRequestParams(false)); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. - // Enumerating revisions on multiple pages make it extremelly - // difficult to manage continuations and require additional sql indexes + // Enumerating revisions on multiple pages make it extremely + // difficult to manage continuations and require additional SQL indexes $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); + $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); @@ -66,7 +67,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids'); if ($pageCount > 1 && $enumRevMode) - $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start, and end parameters may only be used on a single page.', 'multpages'); + $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages'); $this->addTables('revision'); $this->addWhere('rev_deleted=0'); @@ -84,12 +85,20 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp'])); $this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment'])); $this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size'])); + $this->tok_rollback = false; // Prevent PHP undefined property notice + if(!is_null($token)) + { + $this->tok_rollback = $this->getTokenFlag($token, 'rollback'); + } if (isset ($prop['user'])) { $this->addFields('rev_user'); $this->addFields('rev_user_text'); $this->fld_user = true; } + else if($this->tok_rollback) + $this->addFields('rev_user_text'); + if (isset ($prop['content'])) { // For each page we will request, the user must have read rights for that page @@ -105,15 +114,22 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addFields('old_id'); $this->addFields('old_text'); $this->addFields('old_flags'); + $this->fld_content = true; + + $this->expandTemplates = $expandtemplates; } - $userMax = ($this->fld_content ? 50 : 500); - $botMax = ($this->fld_content ? 200 : 10000); + $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); + $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); + if( $limit == 'max' ) { + $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; + $this->getResult()->addValue( 'limits', $this->getModuleName(), $limit ); + } if ($enumRevMode) { - // This is mostly to prevent parameter errors (and optimize sql?) + // This is mostly to prevent parameter errors (and optimize SQL?) if (!is_null($startid) && !is_null($start)) $this->dieUsage('start and startid cannot be used together', 'badparams'); @@ -130,7 +146,7 @@ class ApiQueryRevisions extends ApiQueryBase { // one row with the same timestamp for the same page. // The order needs to be the same as start parameter to avoid SQL filesort. - if (is_null($startid)) + if (is_null($startid) && is_null($endid)) $this->addWhereRange('rev_timestamp', $dir, $start, $end); else $this->addWhereRange('rev_id', $dir, $startid, $endid); @@ -201,7 +217,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->extractRowInfo($row)); } $db->freeResult($res); - + // Ensure that all revisions are shown as '<rev>' elements $result = $this->getResult(); if ($result->getIsRawMode()) { @@ -244,14 +260,27 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['comment'] = $row->rev_comment; } - if ($this->fld_content) { - ApiResult :: setContent($vals, Revision :: getRevisionText($row)); + if($this->tok_rollback || ($this->fld_content && $this->expandTemplates)) + $title = Title::newFromID($row->rev_page); + + if($this->tok_rollback) { + global $wgUser; + $vals['rollbacktoken'] = $wgUser->editToken(array($title->getPrefixedText(), $row->rev_user_text)); } + + if ($this->fld_content) { + $text = Revision :: getRevisionText($row); + if ($this->expandTemplates) { + global $wgParser; + $text = $wgParser->preprocess( $text, $title, new ParserOptions() ); + } + ApiResult :: setContent($vals, $text); + } return $vals; } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, @@ -269,8 +298,8 @@ class ApiQueryRevisions extends ApiQueryBase { 'limit' => array ( ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX => ApiBase :: LIMIT_SML1, - ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2 + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ), 'startid' => array ( ApiBase :: PARAM_TYPE => 'integer' @@ -296,11 +325,19 @@ class ApiQueryRevisions extends ApiQueryBase { ), 'excludeuser' => array( ApiBase :: PARAM_TYPE => 'user' - ) + ), + + 'expandtemplates' => false, + 'token' => array( + ApiBase :: PARAM_TYPE => array( + 'rollback' + ), + ApiBase :: PARAM_ISMULTI => true + ), ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => 'Which properties to get for each revision.', 'limit' => 'limit how many revisions will be returned (enum)', @@ -311,10 +348,12 @@ class ApiQueryRevisions extends ApiQueryBase { 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)', 'user' => 'only include revisions made by user', 'excludeuser' => 'exclude revisions made by user', + 'expandtemplates' => 'expand templates in revision content', + 'token' => 'Which tokens to obtain for each revision', ); } - protected function getDescription() { + public function getDescription() { return array ( 'Get revision information.', 'This module may be used in several ways:', @@ -343,7 +382,7 @@ class ApiQueryRevisions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 25407 2007-09-02 14:00:11Z tstarling $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 31259 2008-02-25 14:14:55Z catrope $'; } } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 268616b1..b15f36ce 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -94,7 +94,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'search' => null, 'namespace' => array ( @@ -121,7 +121,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'search' => 'Search for all page titles (or content) that has this value.', 'namespace' => 'The namespace(s) to enumerate.', @@ -132,7 +132,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ); } - protected function getDescription() { + public function getDescription() { return 'Perform a full text search'; } @@ -145,7 +145,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySearch.php 24453 2007-07-30 08:09:15Z yurik $'; + return __CLASS__ . ': $Id: ApiQuerySearch.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 1fa3d8fc..81af7997 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -53,6 +53,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'namespaces' : $this->appendNamespaces($p); break; + case 'namespacealiases' : + $this->appendNamespaceAliases($p); + break; case 'interwikimap' : $filteriw = isset($params['filteriw']) ? $params['filteriw'] : false; $this->appendInterwikiMap($p, $filteriw); @@ -68,7 +71,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendGeneralInfo($property) { - global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode; + global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode, $IP; $data = array (); $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); @@ -76,6 +79,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['base'] = $mainPage->getFullUrl(); $data['sitename'] = $wgSitename; $data['generator'] = "MediaWiki $wgVersion"; + + $svn = SpecialVersion::getSvnRevision ( $IP ); + if ( $svn ) $data['rev'] = $svn; + $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future if (isset($wgRightsCode)) $data['rightscode'] = $wgRightsCode; @@ -100,6 +107,22 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->getResult()->addValue('query', $property, $data); } + protected function appendNamespaceAliases($property) { + global $wgNamespaceAliases; + + $data = array (); + foreach ($wgNamespaceAliases as $title => $ns) { + $item = array ( + 'id' => $ns + ); + ApiResult :: setContent($item, strtr($title, '_', ' ')); + $data[] = $item; + } + + $this->getResult()->setIndexedTagName($data, 'ns'); + $this->getResult()->addValue('query', $property, $data); + } + protected function appendInterwikiMap($property, $filter) { $this->resetQueryParams(); @@ -177,7 +200,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->getResult()->addValue('query', $property, $data); } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( @@ -186,6 +209,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ApiBase :: PARAM_TYPE => array ( 'general', 'namespaces', + 'namespacealiases', 'interwikimap', 'dbrepllag', 'statistics', @@ -201,12 +225,13 @@ class ApiQuerySiteinfo extends ApiQueryBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => array ( 'Which sysinfo properties to get:', ' "general" - Overall system information', ' "namespaces" - List of registered namespaces (localized)', + ' "namespacealiases" - List of registered namespace aliases', ' "statistics" - Returns site statistics', ' "interwikimap" - Returns interwiki map (optionally filtered)', ' "dbrepllag" - Returns database server with the highest replication lag', @@ -216,19 +241,19 @@ class ApiQuerySiteinfo extends ApiQueryBase { ); } - protected function getDescription() { + public function getDescription() { return 'Return general information about the site.'; } protected function getExamples() { return array( - 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|statistics', + 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics', 'api.php?action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local', 'api.php?action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 25238 2007-08-28 15:37:31Z robchurch $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 30484 2008-02-03 19:29:59Z btongminh $'; } -}
\ No newline at end of file +} diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 05c3d945..57d51cdb 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -60,7 +60,11 @@ class ApiQueryContributions extends ApiQueryBase { $db = $this->getDB(); // Prepare query - $this->prepareUsername(); + $this->usernames = array(); + if(!is_array($this->params['user'])) + $this->params['user'] = array($this->params['user']); + foreach($this->params['user'] as $u) + $this->prepareUsername($u); $this->prepareQuery(); //Do the actual query. @@ -96,8 +100,7 @@ class ApiQueryContributions extends ApiQueryBase { * Validate the 'user' parameter and set the value to compare * against `revision`.`rev_user_text` */ - private function prepareUsername() { - $user = $this->params['user']; + private function prepareUsername($user) { if( $user ) { $name = User::isIP( $user ) ? $user @@ -105,7 +108,7 @@ class ApiQueryContributions extends ApiQueryBase { if( $name === false ) { $this->dieUsage( "User name {$user} is not valid", 'param_user' ); } else { - $this->username = $name; + $this->usernames[] = $name; } } else { $this->dieUsage( 'User parameter may not be empty', 'param_user' ); @@ -123,14 +126,11 @@ class ApiQueryContributions extends ApiQueryBase { $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page"); $this->addWhereFld('rev_deleted', 0); - - // We only want pages by the specified user. - $this->addWhereFld( 'rev_user_text', $this->username ); - + // We only want pages by the specified users. + $this->addWhereFld( 'rev_user_text', $this->usernames ); // ... and in the specified timeframe. $this->addWhereRange('rev_timestamp', $this->params['dir'], $this->params['start'], $this->params['end'] ); - $this->addWhereFld('page_namespace', $this->params['namespace']); $show = $this->params['show']; @@ -142,15 +142,16 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhereIf('rev_minor_edit = 0', isset ($show['!minor'])); $this->addWhereIf('rev_minor_edit != 0', isset ($show['minor'])); } - $this->addOption('LIMIT', $this->params['limit'] + 1); // Mandatory fields: timestamp allows request continuation - // ns+title checks if the user has access rights for this page + // ns+title checks if the user has access rights for this page + // user_text is necessary if multiple users were specified $this->addFields(array( 'rev_timestamp', 'page_namespace', 'page_title', + 'rev_user_text', )); $this->addFieldsIf('rev_page', $this->fld_ids); @@ -158,8 +159,6 @@ class ApiQueryContributions extends ApiQueryBase { // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed? $this->addFieldsIf('rev_comment', $this->fld_comment); $this->addFieldsIf('rev_minor_edit', $this->fld_flags); - - // These fields depend only work if the page table is joined $this->addFieldsIf('page_is_new', $this->fld_flags); } @@ -170,6 +169,7 @@ class ApiQueryContributions extends ApiQueryBase { $vals = array(); + $vals['user'] = $row->rev_user_text; if ($this->fld_ids) { $vals['pageid'] = intval($row->rev_page); $vals['revid'] = intval($row->rev_id); @@ -196,7 +196,7 @@ class ApiQueryContributions extends ApiQueryBase { return $vals; } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'limit' => array ( ApiBase :: PARAM_DFLT => 10, @@ -212,7 +212,7 @@ class ApiQueryContributions extends ApiQueryBase { ApiBase :: PARAM_TYPE => 'timestamp' ), 'user' => array ( - ApiBase :: PARAM_TYPE => 'user' + ApiBase :: PARAM_ISMULTI => true ), 'dir' => array ( ApiBase :: PARAM_DFLT => 'older', @@ -246,7 +246,7 @@ class ApiQueryContributions extends ApiQueryBase { ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'limit' => 'The maximum number of contributions to return.', 'start' => 'The start timestamp to return from.', @@ -259,7 +259,7 @@ class ApiQueryContributions extends ApiQueryBase { ); } - protected function getDescription() { + public function getDescription() { return 'Get all edits by a user'; } @@ -270,7 +270,7 @@ class ApiQueryContributions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserContributions.php 24754 2007-08-13 18:18:18Z robchurch $'; + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 30578 2008-02-05 15:40:58Z catrope $'; } } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index a41b8679..010d9f4f 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -40,50 +40,90 @@ class ApiQueryUserInfo extends ApiQueryBase { } public function execute() { - - global $wgUser; - $params = $this->extractRequestParams(); $result = $this->getResult(); + $r = array(); + if (!is_null($params['prop'])) { + $this->prop = array_flip($params['prop']); + } else { + $this->prop = array(); + } + $r = $this->getCurrentUserInfo(); + $result->addValue("query", $this->getModuleName(), $r); + } + + protected function getCurrentUserInfo() { + global $wgUser; + $result = $this->getResult(); $vals = array(); + $vals['id'] = $wgUser->getId(); $vals['name'] = $wgUser->getName(); - if( $wgUser->isAnon() ) $vals['anon'] = ''; - - if (!is_null($params['prop'])) { - $prop = array_flip($params['prop']); - if (isset($prop['blockinfo'])) { - if ($wgUser->isBlocked()) { - $vals['blockedby'] = User::whoIs($wgUser->blockedBy()); - $vals['blockreason'] = $wgUser->blockedFor(); - } - } - if (isset($prop['hasmsg']) && $wgUser->getNewtalk()) { - $vals['messages'] = ''; - } - if (isset($prop['groups'])) { - $vals['groups'] = $wgUser->getGroups(); - $result->setIndexedTagName($vals['groups'], 'g'); // even if empty - } - if (isset($prop['rights'])) { - $vals['rights'] = $wgUser->getRights(); - $result->setIndexedTagName($vals['rights'], 'r'); // even if empty + if($wgUser->isAnon()) + $vals['anon'] = ''; + if (isset($this->prop['blockinfo'])) { + if ($wgUser->isBlocked()) { + $vals['blockedby'] = User::whoIs($wgUser->blockedBy()); + $vals['blockreason'] = $wgUser->blockedFor(); } + } + if (isset($this->prop['hasmsg']) && $wgUser->getNewtalk()) { + $vals['messages'] = ''; } - - if (!empty($params['option'])) { - foreach( $params['option'] as $option ) { - if (empty($option)) - $this->dieUsage('Empty value is not allowed for the option parameter', 'option'); - $vals['options'][$option] = $wgUser->getOption($option); - } + if (isset($this->prop['groups'])) { + $vals['groups'] = $wgUser->getGroups(); + $result->setIndexedTagName($vals['groups'], 'g'); // even if empty } - - $result->addValue(null, $this->getModuleName(), $vals); + if (isset($this->prop['rights'])) { + $vals['rights'] = $wgUser->getRights(); + $result->setIndexedTagName($vals['rights'], 'r'); // even if empty + } + if (isset($this->prop['options'])) { + $vals['options'] = (is_null($wgUser->mOptions) ? User::getDefaultOptions() : $wgUser->mOptions); + } + if (isset($this->prop['editcount'])) { + $vals['editcount'] = $wgUser->getEditCount(); + } + if (isset($this->prop['ratelimits'])) { + $vals['ratelimits'] = $this->getRateLimits(); + } + return $vals; } + + protected function getRateLimits() + { + global $wgUser, $wgRateLimits; + if(!$wgUser->isPingLimitable()) + return array(); // No limits + + // Find out which categories we belong to + $categories = array(); + if($wgUser->isAnon()) + $categories[] = 'anon'; + else + $categories[] = 'user'; + if($wgUser->isNewBie()) + { + $categories[] = 'ip'; + $categories[] = 'subnet'; + if(!$wgUser->isAnon()) + $categories[] = 'newbie'; + } + + // Now get the actual limits + $retval = array(); + foreach($wgRateLimits as $action => $limits) + foreach($categories as $cat) + if(isset($limits[$cat]) && !is_null($limits[$cat])) + { + $retval[$action][$cat]['hits'] = $limits[$cat][0]; + $retval[$action][$cat]['seconds'] = $limits[$cat][1]; + } + return $retval; + } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_DFLT => NULL, @@ -93,28 +133,30 @@ class ApiQueryUserInfo extends ApiQueryBase { 'hasmsg', 'groups', 'rights', - )), - 'option' => array ( - ApiBase :: PARAM_DFLT => NULL, - ApiBase :: PARAM_ISMULTI => true, - ), + 'options', + 'editcount', + 'ratelimits' + ) + ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'prop' => array( 'What pieces of information to include', - ' blockinfo - tags if the user is blocked, by whom, and for what reason', - ' hasmsg - adds a tag "message" if user has pending messages', - ' groups - lists all the groups the current user belongs to', - ' rights - lists of all rights the current user has', - ), - 'option' => 'A list of user preference options to get', + ' blockinfo - tags if the current user is blocked, by whom, and for what reason', + ' hasmsg - adds a tag "message" if the current user has pending messages', + ' groups - lists all the groups the current user belongs to', + ' rights - lists of all rights the current user has', + ' options - lists all preferences the current user has set', + ' editcount - adds the current user\'s edit count', + ' ratelimits - lists all rate limits applying to the current user' + ) ); } - protected function getDescription() { + public function getDescription() { return 'Get information about the current user'; } @@ -122,12 +164,10 @@ class ApiQueryUserInfo extends ApiQueryBase { return array ( 'api.php?action=query&meta=userinfo', 'api.php?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg', - 'api.php?action=query&meta=userinfo&uioption=rememberpassword', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserInfo.php 24529 2007-08-01 20:11:29Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30395 2008-02-01 14:46:46Z catrope $'; } } - diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php new file mode 100644 index 00000000..144bfba2 --- /dev/null +++ b/includes/api/ApiQueryUsers.php @@ -0,0 +1,162 @@ +<?php + +/* + * Created on July 30, 2007 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to get information about a list of users + * + * @addtogroup API + */ + + class ApiQueryUsers extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'us'); + } + + public function execute() { + $params = $this->extractRequestParams(); + $result = $this->getResult(); + $r = array(); + + if (!is_null($params['prop'])) { + $this->prop = array_flip($params['prop']); + } else { + $this->prop = array(); + } + + if(is_array($params['users'])) { + $r = $this->getOtherUsersInfo($params['users']); + $result->setIndexedTagName($r, 'user'); + } + $result->addValue("query", $this->getModuleName(), $r); + } + + protected function getOtherUsersInfo($users) { + $goodNames = $retval = array(); + // Canonicalize user names + foreach($users as $u) { + $n = User::getCanonicalName($u); + if($n === false) + $retval[] = array('name' => $u, 'invalid' => ''); + else + $goodNames[] = $n; + } + + $db = $this->getDb(); + $userTable = $db->tableName('user'); + $tables = "$userTable AS u1"; + $this->addFields('u1.user_name'); + $this->addWhereFld('u1.user_name', $goodNames); + $this->addFieldsIf('u1.user_editcount', isset($this->prop['editcount'])); + + if(isset($this->prop['groups'])) { + $ug = $db->tableName('user_groups'); + $tables = "$tables LEFT JOIN $ug ON ug_user=u1.user_id"; + $this->addFields('ug_group'); + } + if(isset($this->prop['blockinfo'])) { + $ipb = $db->tableName('ipblocks'); + $tables = "$tables LEFT JOIN $ipb ON ipb_user=u1.user_id"; + $tables = "$tables LEFT JOIN $userTable AS u2 ON ipb_by=u2.user_id"; + $this->addFields(array('ipb_reason', 'u2.user_name AS blocker_name')); + } + $this->addTables($tables); + + $data = array(); + $res = $this->select(__METHOD__); + while(($r = $db->fetchObject($res))) { + $data[$r->user_name]['name'] = $r->user_name; + if(isset($this->prop['editcount'])) + $data[$r->user_name]['editcount'] = $r->user_editcount; + if(isset($this->prop['groups'])) + // This row contains only one group, others will be added from other rows + if(!is_null($r->ug_group)) + $data[$r->user_name]['groups'][] = $r->ug_group; + if(isset($this->prop['blockinfo'])) + if(!is_null($r->blocker_name)) { + $data[$r->user_name]['blockedby'] = $r->blocker_name; + $data[$r->user_name]['blockreason'] = $r->ipb_reason; + } + } + + // Second pass: add result data to $retval + foreach($goodNames as $u) { + if(!isset($data[$u])) + $retval[] = array('name' => $u, 'missing' => ''); + else { + if(isset($this->prop['groups']) && isset($data[$u]['groups'])) + $this->getResult()->setIndexedTagName($data[$u]['groups'], 'g'); + $retval[] = $data[$u]; + } + } + return $retval; + } + + public function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'blockinfo', + 'groups', + 'editcount' + ) + ), + 'users' => array( + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'prop' => array( + 'What pieces of information to include', + ' blockinfo - tags if the user is blocked, by whom, and for what reason', + ' groups - lists all the groups the user belongs to', + ' editcount - adds the user\'s edit count' + ), + 'users' => 'A list of users to obtain the same information for' + ); + } + + public function getDescription() { + return 'Get information about a list of users'; + } + + protected function getExamples() { + return 'api.php?action=query&list=users&ususers=brion|TimStarling&usprop=groups|editcount'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30128 2008-01-24 17:59:07Z catrope $'; + } +} diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 16586a40..91a0c951 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -59,7 +59,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if (!$wgUser->isLoggedIn()) $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); - $allrev = $start = $end = $namespace = $dir = $limit = $prop = null; + $allrev = $start = $end = $namespace = $dir = $limit = $prop = $show = null; extract($this->extractRequestParams()); if (!is_null($prop) && is_null($resultPageSet)) { @@ -135,7 +135,28 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereFld('wl_namespace', $namespace); $this->addWhereIf('rc_this_oldid=page_latest', !$allrev); - # This is a index optimization for mysql, as done in the Special:Watchlist page + if (!is_null($show)) { + $show = array_flip($show); + + /* Check for conflicting parameters. */ + if ((isset ($show['minor']) && isset ($show['!minor'])) + || (isset ($show['bot']) && isset ($show['!bot'])) + || (isset ($show['anon']) && isset ($show['!anon']))) { + + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + } + + /* Add additional conditions to query depending upon parameters. */ + $this->addWhereIf('rc_minor = 0', isset ($show['!minor'])); + $this->addWhereIf('rc_minor != 0', isset ($show['minor'])); + $this->addWhereIf('rc_bot = 0', isset ($show['!bot'])); + $this->addWhereIf('rc_bot != 0', isset ($show['bot'])); + $this->addWhereIf('rc_user = 0', isset ($show['anon'])); + $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); + } + + + # This is an index optimization for mysql, as done in the Special:Watchlist page $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql'); $this->addOption('LIMIT', $limit +1); @@ -222,7 +243,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { return $vals; } - protected function getAllowedParams() { + public function getAllowedParams() { return array ( 'allrev' => false, 'start' => array ( @@ -262,11 +283,22 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'patrol', 'sizes', ) + ), + 'show' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'minor', + '!minor', + 'bot', + '!bot', + 'anon', + '!anon' + ) ) ); } - protected function getParamDescription() { + public function getParamDescription() { return array ( 'allrev' => 'Include multiple revisions of the same page within given timeframe.', 'start' => 'The timestamp to start enumerating from.', @@ -274,11 +306,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'namespace' => 'Filter changes to only the given namespace(s).', 'dir' => 'In which direction to enumerate pages.', 'limit' => 'How many total pages to return per request.', - 'prop' => 'Which additional items to get (non-generator mode only).' + 'prop' => 'Which additional items to get (non-generator mode only).', + 'show' => array ( + 'Show only items that meet this criteria.', + 'For example, to see only minor edits done by logged-in users, set show=minor|!anon' + ) ); } - protected function getDescription() { + public function getDescription() { return ''; } @@ -293,7 +329,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlist.php 24092 2007-07-14 19:04:31Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 30222 2008-01-28 19:05:26Z catrope $'; } } diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index a318d808..ffab51ef 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -139,6 +139,22 @@ class ApiResult extends ApiBase { // Do not use setElement() as it is ok to call this more than once $arr['_element'] = $tag; } + + /** + * Calls setIndexedTagName() on $arr and each sub-array + */ + public function setIndexedTagName_recursive(&$arr, $tag) + { + if(!is_array($arr)) + return; + foreach($arr as $a) + { + if(!is_array($a)) + continue; + $this->setIndexedTagName($a, $tag); + $this->setIndexedTagName_recursive($a, $tag); + } + } /** * Add value to the output data at the given path. @@ -175,7 +191,34 @@ class ApiResult extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiResult.php 26855 2007-10-20 18:27:39Z catrope $'; } } +/* For compatibility with PHP versions < 5.1.0, define our own array_intersect_key function. */ +if (!function_exists('array_intersect_key')) { + function array_intersect_key($isec, $keys) { + $argc = func_num_args(); + + if ($argc > 2) { + for ($i = 1; !empty($isec) && $i < $argc; $i++) { + $arr = func_get_arg($i); + + foreach (array_keys($isec) as $key) { + if (!isset($arr[$key])) + unset($isec[$key]); + } + } + + return $isec; + } else { + $res = array(); + foreach (array_keys($isec) as $key) { + if (isset($keys[$key])) + $res[$key] = $isec[$key]; + } + + return $res; + } + } +} diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php new file mode 100644 index 00000000..d714f99c --- /dev/null +++ b/includes/api/ApiRollback.php @@ -0,0 +1,128 @@ +<?php + +/* + * Created on Jun 20, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiRollback extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + $titleObj = NULL; + if(!isset($params['title'])) + $this->dieUsageMsg(array('missingparam', 'title')); + if(!isset($params['user'])) + $this->dieUsageMsg(array('missingparam', 'user')); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + if(!$titleObj->exists()) + $this->dieUsageMsg(array('notanarticle')); + + $username = User::getCanonicalName($params['user']); + if(!$username) + $this->dieUsageMsg(array('invaliduser', $params['user'])); + + $articleObj = new Article($titleObj); + $summary = (isset($params['summary']) ? $params['summary'] : ""); + $details = null; + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details); + + if(!empty($retval)) + // We don't care about multiple errors, just report one of them + $this->dieUsageMsg(current($retval)); + + $dbw->commit(); + $current = $target = $summary = NULL; + extract($details); + + $info = array( + 'title' => $titleObj->getPrefixedText(), + 'pageid' => $current->getPage(), + 'summary' => $summary, + 'revid' => $titleObj->getLatestRevID(), + 'old_revid' => $current->getID(), + 'last_revid' => $target->getID() + ); + + $this->getResult()->addValue(null, $this->getModuleName(), $info); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'title' => null, + 'user' => null, + 'token' => null, + 'summary' => null, + 'markbot' => false + ); + } + + public function getParamDescription() { + return array ( + 'title' => 'Title of the page you want to rollback.', + 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.', + 'token' => 'A rollback token previously retrieved through prop=info', + 'summary' => 'Custom edit summary. If not set, default summary will be used.', + 'markbot' => 'Mark the reverted edits and the revert as bot edits' + ); + } + + public function getDescription() { + return array( + 'Undoes the last edit to the page. If the last user who edited the page made multiple edits in a row,', + 'they will all be rolled back. You need to be logged in as a sysop to use this function, see also action=login.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=rollback&title=Main%20Page&user=Catrope&token=123ABC', + 'api.php?action=rollback&title=Main%20Page&user=217.121.114.116&token=123ABC&summary=Reverting%20vandalism&markbot=1' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiRollback.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php new file mode 100644 index 00000000..afbd3f0e --- /dev/null +++ b/includes/api/ApiUnblock.php @@ -0,0 +1,124 @@ +<?php + +/* + * Created on Sep 7, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * API module that facilitates the unblocking of users. Requires API write mode + * to be enabled. + * + * @addtogroup API + */ +class ApiUnblock extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Unblocks the specified user or provides the reason the unblock failed. + */ + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + if($params['gettoken']) + { + $res['unblocktoken'] = $wgUser->editToken(); + $this->getResult()->addValue(null, $this->getModuleName(), $res); + return; + } + + if(is_null($params['id']) && is_null($params['user'])) + $this->dieUsageMsg(array('unblock-notarget')); + if(!is_null($params['id']) && !is_null($params['user'])) + $this->dieUsageMsg(array('unblock-idanduser')); + if(is_null($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + if(!$wgUser->isAllowed('block')) + $this->dieUsageMsg(array('cantunblock')); + if(wfReadOnly()) + $this->dieUsageMsg(array('readonlytext')); + + $id = $params['id']; + $user = $params['user']; + $reason = (is_null($params['reason']) ? '' : $params['reason']); + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $retval = IPUnblockForm::doUnblock($id, $user, $reason, $range); + if(!empty($retval)) + $this->dieUsageMsg($retval); + + $dbw->commit(); + $res['id'] = $id; + $res['user'] = $user; + $res['reason'] = $reason; + $this->getResult()->addValue(null, $this->getModuleName(), $res); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'id' => null, + 'user' => null, + 'token' => null, + 'gettoken' => false, + 'reason' => null, + ); + } + + public function getParamDescription() { + return array ( + 'id' => 'ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with user', + 'user' => 'Username, IP address or IP range you want to unblock. Cannot be used together with id', + 'token' => 'An unblock token previously obtained through the gettoken parameter', + 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken', + 'reason' => 'Reason for unblock (optional)', + ); + } + + public function getDescription() { + return array( + 'Unblock a user.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=unblock&id=105', + 'api.php?action=unblock&user=Bob&reason=Sorry%20Bob' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiUnblock.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php new file mode 100644 index 00000000..b27841a8 --- /dev/null +++ b/includes/api/ApiUndelete.php @@ -0,0 +1,123 @@ +<?php + +/* + * Created on Jul 3, 2007 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiUndelete extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + $titleObj = NULL; + if(!isset($params['title'])) + $this->dieUsageMsg(array('missingparam', 'title')); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + + if(!$wgUser->isAllowed('undelete')) + $this->dieUsageMsg(array('permdenied-undelete')); + if($wgUser->isBlocked()) + $this->dieUsageMsg(array('blockedtext')); + if(wfReadOnly()) + $this->dieUsageMsg(array('readonlytext')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + + // Convert timestamps + if(!is_array($params['timestamps'])) + $params['timestamps'] = array($params['timestamps']); + foreach($params['timestamps'] as $i => $ts) + $params['timestamps'][$i] = wfTimestamp(TS_MW, $ts); + + $pa = new PageArchive($titleObj); + $dbw = wfGetDb(DB_MASTER); + $dbw->begin(); + $retval = $pa->undelete((isset($params['timestamps']) ? $params['timestamps'] : array()), $params['reason']); + if(!is_array($retval)) + $this->dieUsageMsg(array('cannotundelete')); + + $dbw->commit(); + $info['title'] = $titleObj->getPrefixedText(); + $info['revisions'] = $retval[0]; + $info['fileversions'] = $retval[1]; + $info['reason'] = $retval[2]; + $this->getResult()->addValue(null, $this->getModuleName(), $info); + } + + public function mustBePosted() { return true; } + + public function getAllowedParams() { + return array ( + 'title' => null, + 'token' => null, + 'reason' => "", + 'timestamps' => array( + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'title' => 'Title of the page you want to restore.', + 'token' => 'An undelete token previously retrieved through list=deletedrevs', + 'reason' => 'Reason for restoring (optional)', + 'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.' + ); + } + + public function getDescription() { + return array( + 'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be', + 'retrieved through list=deletedrevs' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=undelete&title=Main%20Page&token=123ABC&reason=Restoring%20main%20page', + 'api.php?action=undelete&title=Main%20Page&token=123ABC×tamps=20070703220045|20070702194856' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiUndelete.php 30222 2008-01-28 19:05:26Z catrope $'; + } +} diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index bd9ff633..cc70b26d 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -5,24 +5,70 @@ */ class ArchivedFile { - /** - * Returns a file object from the filearchive table - * @param $title, the corresponding image page title - * @param $id, the image id, a unique key - * @param $key, optional storage key - * @return ResultWrapper + /**#@+ + * @private */ + var $id, # filearchive row ID + $title, # image title + $name, # image name + $group, # FileStore storage group + $key, # FileStore sha1 key + $size, # file dimensions + $bits, # size in bytes + $width, # width + $height, # height + $metadata, # metadata string + $mime, # mime type + $media_type, # media type + $description, # upload description + $user, # user ID of uploader + $user_text, # user name of uploader + $timestamp, # time of upload + $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) + $deleted; # Bitfield akin to rev_deleted + + /**#@-*/ + function ArchivedFile( $title, $id=0, $key='' ) { - if( !is_object( $title ) ) { + if( !is_object($title) ) { throw new MWException( 'ArchivedFile constructor given bogus title.' ); } - $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'"; - if( $title->getNamespace() == NS_IMAGE ) { + $this->id = -1; + $this->title = $title; + $this->name = $title->getDBkey(); + $this->group = ''; + $this->key = ''; + $this->size = 0; + $this->bits = 0; + $this->width = 0; + $this->height = 0; + $this->metadata = ''; + $this->mime = "unknown/unknown"; + $this->media_type = ''; + $this->description = ''; + $this->user = 0; + $this->user_text = ''; + $this->timestamp = NULL; + $this->deleted = 0; + $this->dataLoaded = false; + } + + /** + * Loads a file object from the filearchive table + * @return ResultWrapper + */ + public function load() { + if ( $this->dataLoaded ) { + return true; + } + $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'"; + 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', @@ -39,7 +85,7 @@ class ArchivedFile 'fa_timestamp', 'fa_deleted' ), array( - 'fa_name' => $title->getDbKey(), + 'fa_name' => $this->title->getDBkey(), $conds ), __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); @@ -52,36 +98,229 @@ class ArchivedFile $row = $ret->fetchObject(); // initialize fields for filestore image object - $this->mId = intval($row->fa_id); - $this->mName = $row->fa_name; - $this->mGroup = $row->fa_storage_group; - $this->mKey = $row->fa_storage_key; - $this->mSize = $row->fa_size; - $this->mBits = $row->fa_bits; - $this->mWidth = $row->fa_width; - $this->mHeight = $row->fa_height; - $this->mMetaData = $row->fa_metadata; - $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime"; - $this->mType = $row->fa_media_type; - $this->mDescription = $row->fa_description; - $this->mUser = $row->fa_user; - $this->mUserText = $row->fa_user_text; - $this->mTimestamp = $row->fa_timestamp; - $this->mDeleted = $row->fa_deleted; + $this->id = intval($row->fa_id); + $this->name = $row->fa_name; + $this->archive_name = $row->fa_archive_name; + $this->group = $row->fa_storage_group; + $this->key = $row->fa_storage_key; + $this->size = $row->fa_size; + $this->bits = $row->fa_bits; + $this->width = $row->fa_width; + $this->height = $row->fa_height; + $this->metadata = $row->fa_metadata; + $this->mime = "$row->fa_major_mime/$row->fa_minor_mime"; + $this->media_type = $row->fa_media_type; + $this->description = $row->fa_description; + $this->user = $row->fa_user; + $this->user_text = $row->fa_user_text; + $this->timestamp = $row->fa_timestamp; + $this->deleted = $row->fa_deleted; } else { throw new MWException( 'This title does not correspond to an image page.' ); return; } + $this->dataLoaded = true; + return true; } /** + * Loads a file object from the filearchive table + * @return ResultWrapper + */ + public static function newFromRow( $row ) { + $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) ); + + $file->id = intval($row->fa_id); + $file->name = $row->fa_name; + $file->archive_name = $row->fa_archive_name; + $file->group = $row->fa_storage_group; + $file->key = $row->fa_storage_key; + $file->size = $row->fa_size; + $file->bits = $row->fa_bits; + $file->width = $row->fa_width; + $file->height = $row->fa_height; + $file->metadata = $row->fa_metadata; + $file->mime = "$row->fa_major_mime/$row->fa_minor_mime"; + $file->media_type = $row->fa_media_type; + $file->description = $row->fa_description; + $file->user = $row->fa_user; + $file->user_text = $row->fa_user_text; + $file->timestamp = $row->fa_timestamp; + $file->deleted = $row->fa_deleted; + + return $file; + } + + /** + * Return the associated title object + * @public + */ + public function getTitle() { + return $this->title; + } + + /** + * Return the file name + */ + public function getName() { + return $this->name; + } + + public function getID() { + $this->load(); + return $this->id; + } + + /** + * Return the FileStore key + */ + public function getKey() { + $this->load(); + return $this->key; + } + + /** + * Return the FileStore storage group + */ + public function getGroup() { + return $file->group; + } + + /** + * Return the width of the image + */ + public function getWidth() { + $this->load(); + return $this->width; + } + + /** + * Return the height of the image + */ + public function getHeight() { + $this->load(); + return $this->height; + } + + /** + * Get handler-specific metadata + */ + public function getMetadata() { + $this->load(); + return $this->metadata; + } + + /** + * Return the size of the image file, in bytes + * @public + */ + public function getSize() { + $this->load(); + return $this->size; + } + + /** + * Return the bits of the image file, in bytes + * @public + */ + public function getBits() { + $this->load(); + return $this->bits; + } + + /** + * Returns the mime type of the file. + */ + public function getMimeType() { + $this->load(); + return $this->mime; + } + + /** + * Return the type of the media in the file. + * Use the value returned by this function with the MEDIATYPE_xxx constants. + */ + public function getMediaType() { + $this->load(); + return $this->media_type; + } + + /** + * Return upload timestamp. + */ + public function getTimestamp() { + $this->load(); + return $this->timestamp; + } + + /** + * Return the user ID of the uploader. + */ + public function getUser() { + $this->load(); + if( $this->isDeleted( File::DELETED_USER ) ) { + return 0; + } else { + return $this->user; + } + } + + /** + * Return the user name of the uploader. + */ + public function getUserText() { + $this->load(); + if( $this->isDeleted( File::DELETED_USER ) ) { + return 0; + } else { + return $this->user_text; + } + } + + /** + * Return upload description. + */ + public function getDescription() { + $this->load(); + if( $this->isDeleted( File::DELETED_COMMENT ) ) { + return 0; + } else { + return $this->description; + } + } + + /** + * Return the user ID of the uploader. + */ + public function getRawUser() { + $this->load(); + return $this->user; + } + + /** + * Return the user name of the uploader. + */ + public function getRawUserText() { + $this->load(); + return $this->user_text; + } + + /** + * Return upload description. + */ + public function getRawDescription() { + $this->load(); + return $this->description; + } + + /** * int $field one of DELETED_* bitfield constants * for file or revision rows * @return bool */ - function isDeleted( $field ) { - return ($this->mDeleted & $field) == $field; + public function isDeleted( $field ) { + return ($this->deleted & $field) == $field; } /** @@ -90,19 +329,16 @@ class ArchivedFile * @param int $field * @return bool */ - function userCan( $field ) { - if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) { - // images + public function userCan( $field ) { + if( ($this->deleted & $field) == $field ) { global $wgUser; - $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED + $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED ? 'hiderevision' : 'deleterevision'; - wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + wfDebug( "Checking for $permission due to $field match on $this->deleted\n" ); return $wgUser->isAllowed( $permission ); } else { return true; } } } - - diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 84ec9a27..86887d09 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -422,7 +422,7 @@ class FSRepo extends FileRepo { $status->error( 'filerenameerror', $srcPath, $archivePath ); $good = false; } else { - chmod( $archivePath, 0644 ); + @chmod( $archivePath, 0644 ); } } if ( $good ) { diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 21b7a865..5172ad0f 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -46,7 +46,7 @@ abstract class File { /** * The following member variables are not lazy-initialised */ - var $repo, $title, $lastError; + var $repo, $title, $lastError, $redirected; /** * Call this constructor from child classes @@ -135,20 +135,28 @@ abstract class File { /** * Return the associated title object - * @public */ - function getTitle() { return $this->title; } + public function getTitle() { return $this->title; } /** * Return the URL of the file - * @public */ - function getUrl() { + public function getUrl() { if ( !isset( $this->url ) ) { $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel(); } return $this->url; } + + /** + * Return a fully-qualified URL to the file. + * Upload URL paths _may or may not_ be fully qualified, so + * we check. Local paths are assumed to belong on $wgServer. + * @return string + */ + public function getFullUrl() { + return wfExpandUrl( $this->getUrl() ); + } function getViewURL() { if( $this->mustRender()) { @@ -173,10 +181,8 @@ abstract class File { * or in hashed paths like /images/3/3c. * * May return false if the file is not locally accessible. - * - * @public */ - function getPath() { + public function getPath() { if ( !isset( $this->path ) ) { $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel(); } @@ -185,9 +191,8 @@ abstract class File { /** * Alias for getPath() - * @public */ - function getFullPath() { + public function getFullPath() { return $this->getPath(); } @@ -210,6 +215,14 @@ abstract class File { public function getHeight( $page = 1 ) { return false; } /** + * Returns ID or name of user who uploaded the file + * STUB + * + * @param $type string 'text' or 'id' + */ + public function getUser( $type='text' ) { return null; } + + /** * Get the duration of a media file in seconds */ public function getLength() { @@ -487,9 +500,11 @@ abstract class File { $script = $this->getTransformScript(); if ( $script && !($flags & self::RENDER_NOW) ) { - // Use a script to transform on client request + // Use a script to transform on client request, if possible $thumb = $this->handler->getScriptedTransform( $this, $script, $params ); - break; + if( $thumb ) { + break; + } } $normalisedParams = $params; @@ -497,7 +512,7 @@ abstract class File { $thumbName = $this->thumbName( $normalisedParams ); $thumbPath = $this->getThumbPath( $thumbName ); $thumbUrl = $this->getThumbUrl( $thumbName ); - + if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); break; @@ -585,7 +600,7 @@ abstract class File { * STUB * Overridden by LocalFile */ - function purgeCache( $archiveFiles = array() ) {} + function purgeCache() {} /** * Purge the file description page, but don't go after @@ -618,6 +633,18 @@ abstract class File { } /** + * Return a fragment of the history of file. + * + * STUB + * @param $limit integer Limit of rows to return + * @param $start timestamp Only revisions older than $start will be returned + * @param $end timestamp Only revisions newer than $end will be returned + */ + function getHistory($limit = null, $start = null, $end = null) { + return false; + } + + /** * Return the history of this file, line by line. Starts with current version, * then old versions. Should return an object similar to an image/oldimage * database row. @@ -887,7 +914,7 @@ abstract class File { * STUB * Overridden by LocalFile */ - function delete( $reason, $suppress=false ) { + function delete( $reason ) { $this->readOnlyError(); } @@ -984,6 +1011,14 @@ abstract class File { } /** + * Get discription of file revision + * STUB + */ + function getDescription() { + return null; + } + + /** * Get the 14-character timestamp of the file upload, or false if * it doesn't exist */ @@ -1014,7 +1049,7 @@ abstract class File { } /** - * Get an associative array containing information about a file in the local filesystem\ + * Get an associative array containing information about a file in the local filesystem. * * @param string $path Absolute local filesystem path * @param mixed $ext The file extension, or true to extract it from the filename. @@ -1121,6 +1156,14 @@ abstract class File { return ''; } } + + function getRedirected() { + return $this->redirected; + } + + function redirectedFrom( $from ) { + $this->redirected = $from; + } } /** * Aliases for backwards compatibility with 1.6 diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index cf6d65c2..ee7691a6 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -82,7 +82,7 @@ abstract class FileRepo { if ( !$img ) { return false; } - if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) { + if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { return $img; } # Now try an old version of the file @@ -90,6 +90,19 @@ abstract class FileRepo { if ( $img->exists() ) { return $img; } + + # Now try redirects + $redir = $this->checkRedirect( $title ); + if( $redir && $redir->getNamespace() == NS_IMAGE) { + $img = $this->newFile( $redir ); + if( !$img ) { + return false; + } + if( $img->exists() ) { + $img->redirectedFrom( $title->getText() ); + return $img; + } + } } /** @@ -400,5 +413,15 @@ abstract class FileRepo { * STUB */ function cleanupDeletedBatch( $storageKeys ) {} + + /** + * Checks if there is a redirect named as $title + * STUB + * + * @param Title $title Title of image + */ + function checkRedirect( $title ) { + return false; + } } diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 972b2e46..5dd1dbda 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -135,22 +135,22 @@ class FileRepoStatus { } if ( count( $this->errors ) == 1 ) { $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) ); - $s = wfMsgReal( $this->errors[0]['message'], $params ); + $s = wfMsgReal( $this->errors[0]['message'], $params, true, false, false ); if ( $shortContext ) { - $s = wfMsg( $shortContext, $s ); + $s = wfMsgNoTrans( $shortContext, $s ); } elseif ( $longContext ) { - $s = wfMsg( $longContext, "* $s\n" ); + $s = wfMsgNoTrans( $longContext, "* $s\n" ); } } else { $s = ''; foreach ( $this->errors as $error ) { $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ); - $s .= '* ' . wfMsgReal( $error['message'], $params ) . "\n"; + $s .= '* ' . wfMsgReal( $error['message'], $params, true, false, false ) . "\n"; } if ( $longContext ) { - $s = wfMsg( $longContext, $s ); + $s = wfMsgNoTrans( $longContext, $s ); } elseif ( $shortContext ) { - $s = wfMsg( $shortContext, "\n* $s\n" ); + $s = wfMsgNoTrans( $shortContext, "\n* $s\n" ); } } return $s; diff --git a/includes/filerepo/ICRepo.php b/includes/filerepo/ICRepo.php index 124fe2b6..ab686f9b 100644 --- a/includes/filerepo/ICRepo.php +++ b/includes/filerepo/ICRepo.php @@ -1,24 +1,24 @@ <?php /** - * A repository for files accessible via InstantCommons. + * A repository for files accessible via InstantCommons. */ class ICRepo extends LocalRepo { - var $directory, $url, $hashLevels, $cache; + var $directory, $url, $hashLevels, $cache; var $fileFactory = array( 'ICFile', 'newFromTitle' ); var $oldFileFactory = false; function __construct( $info ) { - parent::__construct( $info ); + parent::__construct( $info ); // Required settings $this->directory = $info['directory']; $this->url = $info['url']; $this->hashLevels = $info['hashLevels']; if(isset($info['cache'])){ $this->cache = getcwd().'/images/'.$info['cache']; - } - } + } + } } /** @@ -26,30 +26,30 @@ class ICRepo extends LocalRepo { */ class ICFile extends LocalFile{ static function newFromTitle($title,$repo){ - return new self($title, $repo); + return new self($title, $repo); } - + /** * Returns true if the file comes from the local file repository. * * @return bool */ - function isLocal() { - return true; + function isLocal() { + return true; } - + function load(){ if (!$this->dataLoaded ) { if ( !$this->loadFromCache() ) { if(!$this->loadFromDB()){ $this->loadFromIC(); - } - $this->saveToCache(); + } + $this->saveToCache(); } $this->dataLoaded = true; - } + } } - + /** * Load file metadata from the DB */ @@ -62,15 +62,15 @@ class ICFile extends LocalFile{ $dbr = $this->repo->getSlaveDB(); $row = $dbr->selectRow( 'ic_image', $this->getCacheFields( 'img_' ), - array( 'img_name' => $this->getName() ), __METHOD__ ); + array( 'img_name' => $this->getName() ), __METHOD__ ); if ( $row ) { if (trim($row->img_media_type)==NULL) { $this->upgradeRow(); $this->upgraded = true; - } + } $this->loadFromRow( $row ); //This means that these files are local so the repository locations are local - $this->setUrlPathLocal(); + $this->setUrlPathLocal(); $this->fileExists = true; //var_dump($this); exit; } else { @@ -78,10 +78,10 @@ class ICFile extends LocalFile{ } wfProfileOut( __METHOD__ ); - + return $this->fileExists; } - + /** * Fix assorted version-related problems with the image row by reloading it from the file */ @@ -110,106 +110,102 @@ class ICFile extends LocalFile{ $this->saveToCache(); wfProfileOut( __METHOD__ ); } - + function exists(){ $this->load(); return $this->fileExists; - } - + /** * Fetch the file from the repository. Check local ic_images table first. If not available, check remote server - */ - function loadFromIC(){ - # Unconditionally set loaded=true, we don't want the accessors constantly rechecking + */ + function loadFromIC(){ + # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->dataLoaded = true; - $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform; - if($h = @fopen($icUrl, 'rb')){ - $contents = fread($h, 3000); - $image = $this->api_xml_to_array($contents); - if($image['fileExists']){ - foreach($image as $property=>$value){ - if($property=="url"){$value=$this->repo->url.$value; } - $this->$property = $value; - } - if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){ - //Record the image - $this->recordDownload("Downloaded with InstantCommons"); - - //Then cache it - }else{//set fileExists back to false - $this->fileExists = false; - } - } + $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform; + if($h = @fopen($icUrl, 'rb')){ + $contents = fread($h, 3000); + $image = $this->api_xml_to_array($contents); + if($image['fileExists']){ + foreach($image as $property=>$value){ + if($property=="url"){$value=$this->repo->url.$value; } + $this->$property = $value; + } + if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){ + //Record the image + $this->recordDownload("Downloaded with InstantCommons"); + + //Then cache it + }else{//set fileExists back to false + $this->fileExists = false; + } + } } - } - - - function setUrlPathLocal(){ - global $wgScriptPath; - $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath)); - $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform); + } + + function setUrlPathLocal(){ + global $wgScriptPath; + $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath)); + $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform); $this->repo->directory = $this->repo->cache;//.'/'.rawurlencode($this->title->mDbkeyform); - - } - - function getThumbPath( $suffix=false ){ - $path = $this->repo->cache; - if ( $suffix !== false ) { + + } + + function getThumbPath( $suffix=false ){ + $path = $this->repo->cache; + if ( $suffix !== false ) { $path .= '/thumb/' . rawurlencode( $suffix ); } return $path; - } - function getThumbUrl( $suffix=false ){ - global $wgScriptPath; + } + function getThumbUrl( $suffix=false ){ + global $wgScriptPath; $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath)); - if ( $suffix !== false ) { + if ( $suffix !== false ) { $path .= '/thumb/' . rawurlencode( $suffix ); } return $path; - } - - /** - * Convert the InstantCommons Server API XML Response to an associative array - */ - function api_xml_to_array($xml){ - preg_match("/<instantcommons><image(.*?)<\/instantcommons>/",$xml,$match); - preg_match_all("/(.*?=\".*?\")/",$match[1], $matches); - foreach($matches[1] as $match){ - list($key,$value) = split("=",$match); - $image[trim($key,'<" ')]=trim($value,' "'); - } - return $image; - } - + } + + /** + * Convert the InstantCommons Server API XML Response to an associative array + */ + function api_xml_to_array($xml){ + preg_match("/<instantcommons><image(.*?)<\/instantcommons>/",$xml,$match); + preg_match_all("/(.*?=\".*?\")/",$match[1], $matches); + foreach($matches[1] as $match){ + list($key,$value) = split("=",$match); + $image[trim($key,'<" ')]=trim($value,' "'); + } + return $image; + } + /** - * Use cURL to read the content of a URL into a string - * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot - * @param string $url - the URL to fetch - * @param resource $fp - filename to write file contents to - * @param boolean $bg - call cURL in the background (don't hang page until complete) - * @param int $timeout - cURL connect timeout - */ - function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) { - { - # Call curl in the background to download the file - $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &'; - wfDebug('Curl download initiated='.$cmd ); - $success = false; - $file_contents = array(); - $file_contents['err'] = wfShellExec($cmd, $file_contents['return']); - if($file_contents['err']==0){//Success - $success = true; - } - } - return $success; - } - + * Use cURL to read the content of a URL into a string + * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot + * @param string $url - the URL to fetch + * @param resource $fp - filename to write file contents to + * @param boolean $bg - call cURL in the background (don't hang page until complete) + * @param int $timeout - cURL connect timeout + */ + function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) { + # Call curl in the background to download the file + $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &'; + wfDebug('Curl download initiated='.$cmd ); + $success = false; + $file_contents = array(); + $file_contents['err'] = wfShellExec($cmd, $file_contents['return']); + if($file_contents['err']==0){//Success + $success = true; + } + return $success; + } + function getMasterDB() { if ( !isset( $this->dbConn ) ) { $class = 'Database' . ucfirst( $this->dbType ); - $this->dbConn = new $class( $this->dbServer, $this->dbUser, - $this->dbPassword, $this->dbName, false, $this->dbFlags, + $this->dbConn = new $class( $this->dbServer, $this->dbUser, + $this->dbPassword, $this->dbName, false, $this->dbFlags, $this->tablePrefix ); } return $this->dbConn; @@ -219,10 +215,10 @@ class ICFile extends LocalFile{ * Record a file upload in the upload log and the image table */ private function recordDownload($comment='', $timestamp = false ){ - global $wgUser; + global $wgUser; $dbw = $this->repo->getMasterDB(); - + if ( $timestamp === false ) { $timestamp = $dbw->timestamp(); } @@ -252,7 +248,7 @@ class ICFile extends LocalFile{ ); if( $dbw->affectedRows() == 0 ) { - # Collision, this is an update of a file + # Collision, this is an update of a file # Update the current image row $dbw->update( 'ic_image', array( /* SET */ @@ -297,7 +293,7 @@ class ICFile extends LocalFile{ $descTitle->purgeSquid(); } - + # Commit the transaction now, in case something goes wrong later # The most important thing is that files don't get lost, especially archives $dbw->immediateCommit(); @@ -308,6 +304,6 @@ class ICFile extends LocalFile{ return true; } - + } diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 1e5fc449..9b06fe2d 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -5,7 +5,7 @@ /** * Bump this number when serialized cache records may be incompatible. */ -define( 'MW_FILE_VERSION', 4 ); +define( 'MW_FILE_VERSION', 7 ); /** * Class to represent a local file in the wiki's own database @@ -29,24 +29,26 @@ class LocalFile extends File /**#@+ * @private */ - var $fileExists, # does the file file exist on disk? (loadFromXxx) - $historyLine, # Number of line to return by nextHistoryLine() (constructor) - $historyRes, # result of the query for the file's history (nextHistoryLine) - $width, # \ - $height, # | - $bits, # --- returned by getimagesize (loadFromXxx) - $attr, # / - $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...) - $mime, # MIME type, determined by MimeMagic::guessMimeType - $major_mime, # Major mime type - $minor_mime, # Minor mime type - $size, # Size in bytes (loadFromXxx) - $metadata, # Handler-specific metadata - $timestamp, # Upload timestamp - $sha1, # SHA-1 base 36 content hash - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) - $upgraded, # Whether the row was upgraded on load - $locked; # True if the image row is locked + var $fileExists, # does the file file exist on disk? (loadFromXxx) + $historyLine, # Number of line to return by nextHistoryLine() (constructor) + $historyRes, # result of the query for the file's history (nextHistoryLine) + $width, # \ + $height, # | + $bits, # --- returned by getimagesize (loadFromXxx) + $attr, # / + $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...) + $mime, # MIME type, determined by MimeMagic::guessMimeType + $major_mime, # Major mime type + $minor_mime, # Minor mime type + $size, # Size in bytes (loadFromXxx) + $metadata, # Handler-specific metadata + $timestamp, # Upload timestamp + $sha1, # SHA-1 base 36 content hash + $user, $user_text, # User, who uploaded the file + $description, # Description of current revision of the file + $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) + $upgraded, # Whether the row was upgraded on load + $locked; # True if the image row is locked /**#@-*/ @@ -110,12 +112,9 @@ class LocalFile extends File wfDebug( "Pulling file metadata from cache key $key\n" ); $this->fileExists = $cachedValues['fileExists']; if ( $this->fileExists ) { - unset( $cachedValues['version'] ); - unset( $cachedValues['fileExists'] ); - foreach ( $cachedValues as $name => $value ) { - $this->$name = $value; - } + $this->setProps( $cachedValues ); } + $this->dataLoaded = true; } if ( $this->dataLoaded ) { wfIncrStats( 'image_cache_hit' ); @@ -158,7 +157,7 @@ class LocalFile extends File function getCacheFields( $prefix = 'img_' ) { static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', - 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' ); + 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' ); static $results = array(); if ( $prefix == '' ) { return $fields; @@ -184,7 +183,7 @@ class LocalFile extends File # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->dataLoaded = true; - $dbr = $this->repo->getSlaveDB(); + $dbr = $this->repo->getMasterDB(); $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), array( 'img_name' => $this->getName() ), $fname ); @@ -294,6 +293,9 @@ class LocalFile extends File $dbw = $this->repo->getMasterDB(); list( $major, $minor ) = self::splitMime( $this->mime ); + if ( wfReadOnly() ) { + return; + } wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n"); $dbw->update( 'image', @@ -313,6 +315,13 @@ class LocalFile extends File wfProfileOut( __METHOD__ ); } + /** + * Set properties in this object to be equal to those given in the + * associative array $info. Only cacheable fields can be set. + * + * If 'mime' is given, it will be split into major_mime/minor_mime. + * If major_mime/minor_mime are given, $this->mime will also be set. + */ function setProps( $info ) { $this->dataLoaded = true; $fields = $this->getCacheFields( '' ); @@ -378,6 +387,20 @@ class LocalFile extends File } /** + * Returns ID or name of user who uploaded the file + * + * @param $type string 'text' or 'id' + */ + function getUser($type='text') { + $this->load(); + if( $type == 'text' ) { + return $this->user_text; + } elseif( $type == 'id' ) { + return $this->user; + } + } + + /** * Get handler-specific metadata */ function getMetadata() { @@ -555,6 +578,28 @@ class LocalFile extends File /** purgeDescription inherited */ /** purgeEverything inherited */ + function getHistory($limit = null, $start = null, $end = null) { + $dbr = $this->repo->getSlaveDB(); + $conds = $opts = array(); + $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() ); + if( $start !== null ) { + $conds[] = "oi_timestamp <= " . $dbr->addQuotes( $dbr->timestamp( $start ) ); + } + if( $end !== null ) { + $conds[] = "oi_timestamp >= " . $dbr->addQuotes( $dbr->timestamp( $end ) ); + } + if( $limit ) { + $opts['LIMIT'] = $limit; + } + $opts['ORDER BY'] = 'oi_timestamp DESC'; + $res = $dbr->select('oldimage', '*', $conds, __METHOD__, $opts); + $r = array(); + while( $row = $dbr->fetchObject($res) ) { + $r[] = OldLocalFile::newFromRow($row, $this->repo); + } + return $r; + } + /** * Return the history of this file, line by line. * starts with current version, then old versions. @@ -566,6 +611,9 @@ class LocalFile extends File * @public */ function nextHistoryLine() { + # Polymorphic function name to distinguish foreign and local fetches + $fname = get_class( $this ) . '::' . __FUNCTION__; + $dbr = $this->repo->getSlaveDB(); if ( $this->historyLine == 0 ) {// called for the first time, return line from cur @@ -575,7 +623,7 @@ class LocalFile extends File "'' AS oi_archive_name" ), array( 'img_name' => $this->title->getDBkey() ), - __METHOD__ + $fname ); if ( 0 == $dbr->numRows( $this->historyRes ) ) { $dbr->freeResult($this->historyRes); @@ -586,7 +634,7 @@ class LocalFile extends File $dbr->freeResult($this->historyRes); $this->historyRes = $dbr->select( 'oldimage', '*', array( 'oi_name' => $this->title->getDBkey() ), - __METHOD__, + $fname, array( 'ORDER BY' => 'oi_timestamp DESC' ) ); } @@ -678,6 +726,10 @@ class LocalFile extends File if ( !$props ) { $props = $this->repo->getFileProps( $this->getVirtualUrl() ); } + $props['description'] = $comment; + $props['user'] = $wgUser->getID(); + $props['user_text'] = $wgUser->getName(); + $props['timestamp'] = wfTimestamp( TS_MW ); $this->setProps( $props ); // Delete thumbnails and refresh the metadata cache @@ -964,6 +1016,11 @@ class LocalFile extends File return $html; } + function getDescription() { + $this->load(); + return $this->description; + } + function getTimestamp() { $this->load(); return $this->timestamp; @@ -1188,12 +1245,12 @@ class LocalFileDeleteBatch { list( $oldRels, $deleteCurrent ) = $this->getOldRels(); if ( $deleteCurrent ) { + $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) ); $where = array( 'img_name' => $this->file->getName() ); $dbw->insertSelect( 'filearchive', 'image', array( 'fa_storage_group' => $encGroup, - 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))", - + 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END", 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, 'fa_deleted_reason' => $encReason, @@ -1217,15 +1274,14 @@ class LocalFileDeleteBatch { } if ( count( $oldRels ) ) { + $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) ); $where = array( 'oi_name' => $this->file->getName(), 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ); - $dbw->insertSelect( 'filearchive', 'oldimage', array( 'fa_storage_group' => $encGroup, - 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))", - + 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END", 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, 'fa_deleted_reason' => $encReason, @@ -1252,9 +1308,6 @@ class LocalFileDeleteBatch { function doDBDeletes() { $dbw = $this->file->repo->getMasterDB(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); - if ( $deleteCurrent ) { - $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ ); - } if ( count( $oldRels ) ) { $dbw->delete( 'oldimage', array( @@ -1262,6 +1315,9 @@ class LocalFileDeleteBatch { 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ), __METHOD__ ); } + if ( $deleteCurrent ) { + $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ ); + } } /** diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 72f9e9a6..a259bd48 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -62,4 +62,52 @@ class LocalRepo extends FSRepo { } return $status; } + + /** + * Function link Title::getArticleID(). + * We can't say Title object, what database it should use, so we duplicate that function here. + */ + private function getArticleID( $title ) { + if( !$title instanceof Title ) { + return 0; + } + $dbr = $this->getSlaveDB(); + $id = $dbr->selectField( + 'page', // Table + 'page_id', //Field + array( //Conditions + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbKey(), + ), + __METHOD__ //Function name + ); + return $id; + } + + function checkRedirect( $title ) { + global $wgFileRedirects; + if( !$wgFileRedirects ) { + return false; + } + + if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) { + $title = Title::makeTitle( NS_IMAGE, $title->getText() ); + } + + $id = $this->getArticleID( $title ); + if( !$id ) { + return false; + } + $dbr = $this->getSlaveDB(); + $row = $dbr->selectRow( + 'redirect', + array( 'rd_title', 'rd_namespace' ), + array( 'rd_from' => $id ), + __METHOD__ + ); + if( !$row ) { + return false; + } + return Title::makeTitle( $row->rd_namespace, $row->rd_title ); + } } diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php new file mode 100644 index 00000000..87bfd3ab --- /dev/null +++ b/includes/filerepo/NullRepo.php @@ -0,0 +1,34 @@ +<?php + +/** + * File repository with no files, for performance testing + */ + +class NullRepo extends FileRepo { + function __construct( $info ) {} + + function storeBatch( $triplets, $flags = 0 ) { + return false; + } + + function storeTemp( $originalName, $srcPath ) { + return false; + } + function publishBatch( $triplets, $flags = 0 ) { + return false; + } + function deleteBatch( $sourceDestPairs ) { + return false; + } + function getFileProps( $virtualUrl ) { + return false; + } + function newFile( $title, $time = false ) { + return false; + } + function findFile( $title, $time = false ) { + return false; + } +} + +?> diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 23d222af..b0e1d782 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -32,6 +32,13 @@ class RepoGroup { } /** + * Set the singleton instance to a given object + */ + static function setSingleton( $instance ) { + self::$instance = $instance; + } + + /** * Construct a group of file repositories. * @param array $data Array of repository info arrays. * Each info array is an associative array with the 'class' member @@ -47,8 +54,8 @@ class RepoGroup { * Search repositories for an image. * You can also use wfGetFile() to do this. * @param mixed $title Title object or string - * @param mixed $time The 14-char timestamp before which the file should - * have been uploaded, or false for the current version + * @param mixed $time The 14-char timestamp the file should have + * been uploaded, or false for the current version * @return File object or false if it is not found */ function findFile( $title, $time = false ) { @@ -70,13 +77,34 @@ class RepoGroup { } /** + * Interface for FileRepo::checkRedirect() + */ + function checkRedirect( $title ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $redir = $this->localRepo->checkRedirect( $title ); + if( $redir ) { + return $redir; + } + foreach ( $this->foreignRepos as $repo ) { + $redir = $repo->checkRedirect( $title ); + if ( $redir ) { + return $redir; + } + } + return false; + } + + /** * Get the repo instance with a given key. */ function getRepo( $index ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } - if ( $index == 'local' ) { + if ( $index === 'local' ) { return $this->localRepo; } elseif ( isset( $this->foreignRepos[$index] ) ) { return $this->foreignRepos[$index]; @@ -84,6 +112,19 @@ class RepoGroup { return false; } } + /** + * Get the repo instance by its name + */ + function getRepoByName( $name ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + foreach ( $this->foreignRepos as $key => $repo ) { + if ( $repo->name == $name) + return $repo; + } + return false; + } /** * Get the local repository, i.e. the one corresponding to the local image diff --git a/includes/media/Generic.php b/includes/media/Generic.php index c7ab7d81..19914929 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -96,6 +96,19 @@ abstract class MediaHandler { */ function isMetadataValid( $image, $metadata ) { return true; } + + /** + * Get a MediaTransformOutput object representing an alternate of the transformed + * output which will call an intermediary thumbnail assist script. + * + * Used when the repository has a thumbnailScriptUrl option configured. + * + * Return false to fall back to the regular getTransform(). + */ + function getScriptedTransform( $image, $script, $params ) { + return false; + } + /** * Get a MediaTransformOutput object representing the transformed output. Does not * actually do the transform. @@ -191,7 +204,7 @@ abstract class MediaHandler { * to do things like visual indication of grouped and chained streams * in ogg container files. */ - function formatMetadata( $image, $metadata ) { + function formatMetadata( $image ) { return false; } @@ -224,7 +237,7 @@ abstract class MediaHandler { return wfMsg( 'file-info', $sk->formatSize( $file->getSize() ), $file->getMimeType() ); } - function getDimensionsString() { + function getDimensionsString( $file ) { return ''; } @@ -372,7 +385,10 @@ abstract class ImageHandler extends MediaHandler { } $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) ); $page = isset( $params['page'] ) ? $params['page'] : false; - return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page ); + + if( $image->mustRender() || $params['width'] < $image->getWidth() ) { + return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page ); + } } function getImageSize( $image, $path ) { @@ -386,26 +402,24 @@ abstract class ImageHandler extends MediaHandler { global $wgLang; $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( $file->getSize() ) ) . ')'; - $widthheight = wfMsgHtml( 'widthheight', $file->getWidth(), $file->getHeight() ); - + $widthheight = wfMsgHtml( 'widthheight', $wgLang->formatNum( $file->getWidth() ) ,$wgLang->formatNum( $file->getHeight() ) ); + return "$widthheight ($nbytes)"; } function getLongDesc( $file ) { global $wgLang; - return wfMsgHtml('file-info-size', $file->getWidth(), $file->getHeight(), + return wfMsgHtml('file-info-size', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ), $wgLang->formatSize( $file->getSize() ), $file->getMimeType() ); } function getDimensionsString( $file ) { + global $wgLang; $pages = $file->pageCount(); if ( $pages > 1 ) { - return wfMsg( 'widthheightpage', $file->getWidth(), $file->getHeight(), $pages ); + return wfMsg( 'widthheightpage', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ), $wgLang->formatNum( $pages ) ); } else { - return wfMsg( 'widthheight', $file->getWidth(), $file->getHeight() ); + return wfMsg( 'widthheight', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ) ); } } } - - - diff --git a/includes/mime.info b/includes/mime.info index a960f023..dd3af7d0 100644 --- a/includes/mime.info +++ b/includes/mime.info @@ -18,6 +18,7 @@ image/x-portable-pixmap [BITMAP] image/x-portable-graymap image/x-portable-greymap [BITMAP] image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP] image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP] +image/vnd.djvu image/x.djvu image/x-djvu [BITMAP] image/svg+xml application/svg+xml application/svg image/svg [DRAWING] application/postscript [DRAWING] diff --git a/includes/mime.types b/includes/mime.types index 19a61517..64f77c12 100644 --- a/includes/mime.types +++ b/includes/mime.types @@ -55,7 +55,7 @@ application/x-wais-source src application/x-xpinstall xpi application/xhtml+xml xhtml xht application/xslt+xml xslt -application/xml xml xsl +application/xml xml xsl xsd application/xml-dtd dtd application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw audio/basic au snd diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index 127c30a0..ac24800a 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -24,6 +24,7 @@ class UserloginTemplate extends QuickTemplate { <div class="visualClear"></div> <?php } ?> +<div id="loginstart"><?php $this->msgWiki( 'loginstart' ); ?></div> <div id="userloginForm"> <form name="userlogin" method="post" action="<?php $this->text('action') ?>"> <h2><?php $this->msg('login') ?></h2> @@ -33,16 +34,16 @@ class UserloginTemplate extends QuickTemplate { <?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?> <table> <tr> - <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpName1'><?php $this->msg('yourname') ?></label></td> + <td class="mw-input"> <input type='text' class='loginText' name="wpName" id="wpName1" tabindex="1" value="<?php $this->text('name') ?>" size='20' /> </td> </tr> <tr> - <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td> + <td class="mw-input"> <input type='password' class='loginPassword' name="wpPassword" id="wpPassword1" tabindex="2" value="" size='20' /> @@ -55,8 +56,8 @@ class UserloginTemplate extends QuickTemplate { } ?> <tr> - <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td> - <td align='left'> + <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td> + <td class="mw-input"> <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>" tabindex="3"> <?php echo $doms ?> @@ -66,7 +67,7 @@ class UserloginTemplate extends QuickTemplate { <?php } ?> <tr> <td></td> - <td align='left'> + <td class="mw-input"> <input type='checkbox' name="wpRemember" tabindex="4" value="1" id="wpRemember" @@ -76,7 +77,7 @@ class UserloginTemplate extends QuickTemplate { </tr> <tr> <td></td> - <td align='left' style="white-space:nowrap"> + <td class="mw-submit"> <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] && $this->data['canreset']) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword" tabindex="6" value="<?php $this->msg('mailmypassword') ?>" /> @@ -117,16 +118,16 @@ class UsercreateTemplate extends QuickTemplate { <?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?> <table> <tr> - <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpName2'><?php $this->msg('yourname') ?></label></td> + <td class="mw-input"> <input type='text' class='loginText' name="wpName" id="wpName2" tabindex="1" value="<?php $this->text('name') ?>" size='20' /> </td> </tr> <tr> - <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td> + <td class="mw-input"> <input type='password' class='loginPassword' name="wpPassword" id="wpPassword2" tabindex="2" value="" size='20' /> @@ -139,8 +140,8 @@ class UsercreateTemplate extends QuickTemplate { } ?> <tr> - <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td> - <td align='left'> + <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td> + <td class="mw-input"> <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>" tabindex="3"> <?php echo $doms ?> @@ -149,8 +150,8 @@ class UsercreateTemplate extends QuickTemplate { </tr> <?php } ?> <tr> - <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td> + <td class="mw-input"> <input type='password' class='loginPassword' name="wpRetype" id="wpRetype" tabindex="4" value="" @@ -159,21 +160,25 @@ class UsercreateTemplate extends QuickTemplate { </tr> <tr> <?php if( $this->data['useemail'] ) { ?> - <td align='right' style='vertical-align: top'><label for='wpEmail'><?php $this->msg('youremail') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpEmail'><?php $this->msg('youremail') ?></label></td> + <td class="mw-input"> <input type='text' class='loginText' name="wpEmail" id="wpEmail" tabindex="5" value="<?php $this->text('email') ?>" size='20' /> <div class="prefsectiontip"> - <?php $this->msgWiki('prefs-help-email'); ?> + <?php if( $this->data['emailrequired'] ) { + $this->msgWiki('prefs-help-email-required'); + } else { + $this->msgWiki('prefs-help-email'); + } ?> </div> </td> <?php } ?> <?php if( $this->data['userealname'] ) { ?> </tr> <tr> - <td align='right' style='vertical-align: top'><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td> - <td align='left'> + <td class="mw-label"><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td> + <td class="mw-input"> <input type='text' class='loginText' name="wpRealName" id="wpRealName" tabindex="6" value="<?php $this->text('realname') ?>" size='20' /> @@ -185,7 +190,7 @@ class UsercreateTemplate extends QuickTemplate { </tr> <tr> <td></td> - <td align='left'> + <td class="mw-input"> <input type='checkbox' name="wpRemember" tabindex="7" value="1" id="wpRemember" @@ -195,7 +200,7 @@ class UsercreateTemplate extends QuickTemplate { </tr> <tr> <td></td> - <td align='left'> + <td class="mw-submit"> <input type='submit' name="wpCreateaccount" id="wpCreateaccount" tabindex="8" value="<?php $this->msg('createaccount') ?>" /> diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile index 30679fbb..c63e4db7 100644 --- a/includes/zhtable/Makefile +++ b/includes/zhtable/Makefile @@ -11,30 +11,35 @@ SED = LANG=zh_CN.UTF8 sed DIFF = LANG=zh_CN.UTF8 diff CC ?= gcc -#installation directory +SF_MIRROR = easynews +SCIM_TABLES_VER = 0.5.7 +SCIM_PINYIN_VER = 0.5.91 +LIBTABE_VER = 0.2.3 + +# Installation directory INSTDIR = /usr/local/share/zhdaemons/ -all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toCN.dict toTW.dict toHK.dict toSG.dict +all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toHans.dict toHant.dict toCN.dict toTW.dict toHK.dict toSG.dict Unihan.txt: wget -nc ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip unzip -q Unihan.zip EZ.txt.in: - wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-tables-0.5.1.tar.gz - tar -xzf scim-tables-0.5.1.tar.gz -O scim-tables-0.5.1/zh/EZ.txt.in > EZ.txt.in + wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-tables-$(SCIM_TABLES_VER).tar.gz + tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/EZ-Big.txt.in > EZ.txt.in phrase_lib.txt: - wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-pinyin-0.5.0.tar.gz - tar -xzf scim-pinyin-0.5.0.tar.gz -O scim-pinyin-0.5.0/data/phrase_lib.txt > phrase_lib.txt + wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-pinyin-$(SCIM_PINYIN_VER).tar.gz + tar -xzf scim-pinyin-$(SCIM_PINYIN_VER).tar.gz -O scim-pinyin-$(SCIM_PINYIN_VER)/data/phrase_lib.txt > phrase_lib.txt tsi.src: - wget -nc http://unc.dl.sourceforge.net/sourceforge/libtabe/libtabe-0.2.3.tgz - tar -xzf libtabe-0.2.3.tgz -O libtabe/tsi-src/tsi.src > tsi.src + wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/libtabe/libtabe-$(LIBTABE_VER).tgz + tar -xzf libtabe-$(LIBTABE_VER).tgz -O libtabe/tsi-src/tsi.src > tsi.src wordlist: phrase_lib.txt EZ.txt.in tsi.src iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/# //g' | $(SED) 's/[ ][0-9].*//' > wordlist - $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >>wordlist + $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >> wordlist $(SED) '1,/BEGIN_TABLE/d' EZ.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" >> wordlist sort wordlist | uniq | $(SED) 's/ //g' > t mv t wordlist @@ -184,67 +189,68 @@ simp2trad.php: simp2trad1to1.t simpphrases.t cat simpphrases.t >> simp2trad.php printf '";\n$$t=strtr($$str, $$simp2trad);\necho $$t;\n?>' >> simp2trad.php -simp2trad.phrases.t: trad2simp.php tradphrases.t toTW.manual +simp2trad.phrases.t: trad2simp.php tradphrases.t php -f trad2simp.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1 cat tradphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2 paste tmp1 tmp2 > simp2trad.phrases.t - $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toTW.manual >> simp2trad.phrases.t -trad2simp.phrases.t: simp2trad.php simpphrases.t toCN.manual +trad2simp.phrases.t: simp2trad.php simpphrases.t php -f simp2trad.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1 cat simpphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2 paste tmp1 tmp2 > trad2simp.phrases.t - $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toCN.manual >> trad2simp.phrases.t -toCN.dict: trad2simp1to1.t trad2simp.phrases.t - cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toCN.dict - cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toCN.dict +toHans.dict: trad2simp1to1.t trad2simp.phrases.t + cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHans.dict + cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHans.dict + +toHant.dict: simp2trad1to1.t simp2trad.phrases.t + cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHant.dict + cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHant.dict -toTW.dict: simp2trad1to1.t simp2trad.phrases.t - cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toTW.dict - cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toTW.dict +toTW.dict: toTW.manual + cat toTW.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toTW.dict toHK.dict: toHK.manual cat toHK.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toHK.dict +toCN.dict: toCN.manual + cat toCN.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toCN.dict + toSG.dict: toSG.manual cat toSG.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toSG.dict - - -ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toHK.manual toSG.manual - printf '<?php\n/**\n * Simplified/Traditional Chinese conversion tables\n' > ZhConversion.php +ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toCN.manual toHK.manual toSG.manual toTW.manual + printf '<?php\n/**\n * Simplified / Traditional Chinese conversion tables\n' > ZhConversion.php printf ' *\n * Automatically generated using code and data in includes/zhtable/\n' >> ZhConversion.php - printf ' * Do not modify directly! \n *\n * @package MediaWiki\n*/\n\n' >> ZhConversion.php - printf '$$zh2TW=array(\n' >> ZhConversion.php + printf ' * Do not modify directly!\n */\n\n' >> ZhConversion.php + printf '$$zh2Hant = array(\n' >> ZhConversion.php cat simp2trad1to1.t >> ZhConversion.php echo >> ZhConversion.php cat simp2trad.phrases.t >> ZhConversion.php - echo >> ZhConversion.php echo ');' >> ZhConversion.php echo >> ZhConversion.php - echo >> ZhConversion.php - printf '$$zh2CN=array(\n' >> ZhConversion.php + printf '$$zh2Hans = array(\n' >> ZhConversion.php cat trad2simp1to1.t >> ZhConversion.php echo >> ZhConversion.php cat trad2simp.phrases.t >> ZhConversion.php + echo ');' >> ZhConversion.php echo >> ZhConversion.php - printf ');' >> ZhConversion.php - echo >> ZhConversion.php + printf '$$zh2TW = array(\n' >> ZhConversion.php + $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toTW.manual >> ZhConversion.php + echo ');' >> ZhConversion.php echo >> ZhConversion.php - printf '$$zh2HK=array(\n' >> ZhConversion.php + printf '$$zh2HK = array(\n' >> ZhConversion.php $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toHK.manual >> ZhConversion.php + echo ');' >> ZhConversion.php echo >> ZhConversion.php - printf ');' >> ZhConversion.php - echo >> ZhConversion.php + printf '$$zh2CN = array(\n' >> ZhConversion.php + $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toCN.manual >> ZhConversion.php + echo ');' >> ZhConversion.php echo >> ZhConversion.php - printf '$$zh2SG=array(\n' >> ZhConversion.php + printf '$$zh2SG = array(\n' >> ZhConversion.php $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSG.manual >> ZhConversion.php echo >> ZhConversion.php printf ');' >> ZhConversion.php - echo >> ZhConversion.php - printf '?>' >> ZhConversion.php - clean: cleantmp cleandl @@ -262,7 +268,7 @@ cleantmp: cleandl: rm -f \ Unihan.zip \ - scim-tables-0.5.1.tar.gz \ - scim-pinyin-0.5.0.tar.gz \ - libtabe-0.2.3.tgz + scim-tables-$(SCIM_TABLES_VER).tar.gz \ + scim-pinyin-$(SCIM_PINYIN_VER).tar.gz \ + libtabe-$(LIBTABE_VER).tgz diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual index caff9c14..427afad2 100644 --- a/includes/zhtable/toCN.manual +++ b/includes/zhtable/toCN.manual @@ -1,26 +1,19 @@ 記憶體 内存 預設 默认 -預設 缺省 串列 串行 乙太網 以太网 點陣圖 位图 常式 例程 -通道 信道 游標 光标 光碟 光盘 光碟機 光驱 全形 全角 共用 共享 -相容 兼容 -首碼 前缀 -尾碼 后缀 載入 加载 半形 半角 變數 变量 雜訊 噪声 因數 因子 -線上 在线 -離線 脱机 功能變數名稱 域名 音效卡 声卡 字型大小 字号 @@ -43,19 +36,16 @@ 磁碟 磁盘 磁軌 磁道 程式控制 程控 -埠 端口 運算元 算子 演算法 算法 晶片 芯片 晶元 芯片 片語 词组 -解碼 译码 軟碟機 软驱 -快閃記憶體 闪存 +快閃記憶體 快闪存储器 滑鼠 鼠标 進位 进制 互動式 交互式 -模擬 仿真 優先順序 优先级 感測 传感 攜帶型 便携式 @@ -64,26 +54,19 @@ 防寫 写保护 分散式 分布式 解析度 分辨率 -程式 程序 伺服器 服务器 等於 等于 區域網 局域网 -上傳 上载 -電腦 计算机 巨集 宏 掃瞄器 扫瞄仪 寬頻 宽带 -視窗 窗口 資料庫 数据库 -西曆 公历 乳酪 奶酪 鉅賈 巨商 手電筒 手电 萬曆 万历 永曆 永历 辭彙 词汇 -保全 保安 -慣用 习用 母音 元音 自由球 任意球 頭槌 头球 @@ -177,11 +160,11 @@ 坦桑尼亞 坦桑尼亚 坦尚尼亞 坦桑尼亚 埃塞俄比亞 埃塞俄比亚 +衣索匹亞 埃塞俄比亚 衣索比亞 埃塞俄比亚 吉里巴斯 基里巴斯 基里巴斯 基里巴斯 塔吉克 塔吉克斯坦 -獅子山 塞拉利昂 塞拉利昂 塞拉利昂 塞普勒斯 塞浦路斯 塞浦路斯 塞浦路斯 @@ -218,13 +201,11 @@ 斯洛維尼亞 斯洛文尼亚 新西蘭 新西兰 紐西蘭 新西兰 -北韓 朝鲜 格林納達 格林纳达 格瑞那達 格林纳达 -格魯吉亞 格鲁吉亚 -喬治亞 格鲁吉亚 +格魯吉亞 乔治亚 +喬治亞 乔治亚 梵蒂岡 梵蒂冈 -教廷 梵蒂冈 毛里塔尼亞 毛里塔尼亚 茅利塔尼亞 毛里塔尼亚 毛里裘斯 毛里求斯 @@ -271,8 +252,7 @@ 馬爾代夫 马尔代夫 馬爾地夫 马尔代夫 馬爾他 马耳他 -馬里 马里 -馬利 马里 +馬利共和國 马里共和国 即食麵 方便面 快速面 方便面 速食麵 方便面 @@ -286,11 +266,9 @@ 夜学 夜校 华乐 民乐 中樂 民乐 -住屋 住房 屋价 房价 的士 出租车 計程車 出租车 -巴士 公共汽车 公車 公共汽车 單車 自行车 節慶 节日 @@ -305,14 +283,14 @@ 衛生 卫生 賓士 奔驰 平治 奔驰 -捷豹 美洲虎 -積架 美洲虎 +積架 捷豹 福斯 大众 福士 大众 雪鐵龍 雪铁龙 萬事得 马自达 馬自達 马自达 寶獅 标志 +拿破崙 拿破仑 布殊 布什 布希 布什 柯林頓 克林顿 @@ -326,6 +304,5 @@ 沙芬 马拉特·萨芬 舒麥加 迈克尔·舒马赫 希特拉 希特勒 -戴安娜 狄安娜 -黛安娜 狄安娜 -希拉 赫拉
\ No newline at end of file +黛安娜 戴安娜 +希拉 赫拉 diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual index ab623455..6a872fa6 100644 --- a/includes/zhtable/toHK.manual +++ b/includes/zhtable/toHK.manual @@ -1,7 +1,7 @@ 打印机 打印機 印表機 打印機 -字节 字節 -位元組 字節 +字节 位元組 +字節 位元組 打印 打印 列印 打印 硬件 硬件 @@ -83,10 +83,11 @@ 坦桑尼亚 坦桑尼亞 坦尚尼亞 坦桑尼亞 埃塞俄比亚 埃塞俄比亞 +衣索匹亞 埃塞俄比亞 衣索比亞 埃塞俄比亞 基里巴斯 基里巴斯 吉里巴斯 基里巴斯 -獅子山 塞拉利昂 +狮子山 獅子山 塞普勒斯 塞浦路斯 塞舌尔 塞舌爾 塞席爾 塞舌爾 @@ -119,16 +120,14 @@ 紐西蘭 新西蘭 格林纳达 格林納達 格瑞那達 格林納達 -格鲁吉亚 格魯吉亞 -喬治亞 格魯吉亞 +格鲁吉亚 喬治亞 +格魯吉亞 喬治亞 梵蒂冈 梵蒂岡 -教廷 梵蒂岡 毛里塔尼亚 毛里塔尼亞 茅利塔尼亞 毛里塔尼亞 毛里求斯 毛里裘斯 模里西斯 毛里裘斯 -沙特阿拉伯 沙地阿拉伯 -沙烏地阿拉伯 沙地阿拉伯 +沙烏地阿拉伯 沙特阿拉伯 波斯尼亚和黑塞哥维那 波斯尼亞黑塞哥維那 波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那 津巴布韦 津巴布韋 @@ -163,29 +162,22 @@ 阿拉伯聯合大公國 阿拉伯聯合酋長國 马尔代夫 馬爾代夫 馬爾地夫 馬爾代夫 -马里 馬里 -馬利 馬里 +馬利共和國 馬里共和國 方便面 即食麵 快速面 即食麵 速食麵 即食麵 泡麵 即食麵 -土豆 薯仔 +土豆 馬鈴薯 华乐 中樂 民乐 中樂 -計程車 的士 +計程車 的士 出租车 的士 公車 巴士 -公共汽车 巴士 自行车 單車 -节日 節慶 犬只 狗隻 台球 桌球 撞球 桌球 冰淇淋 雪糕 -冰淇淋 雪糕 -卫生 衞生 -衛生 衞生 -老人 長者 賓士 平治 捷豹 積架 福斯 福士 @@ -195,12 +187,14 @@ 马自达 萬事得 馬自達 萬事得 寶獅 標致 +拿破崙 拿破侖 布什 布殊 布希 布殊 克林顿 克林頓 柯林頓 克林頓 萨达姆 薩達姆 -海珊 薩達姆 +海珊 侯賽因 +侯赛因 侯賽因 大卫·贝克汉姆 大衛碧咸 迈克尔·欧文 米高奧雲 珍妮弗·卡普里亚蒂 卡佩雅蒂 @@ -208,4 +202,4 @@ 迈克尔·舒马赫 舒麥加 希特勒 希特拉 狄安娜 戴安娜 -黛安娜 戴安娜
\ No newline at end of file +黛安娜 戴安娜 diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual index 5c90dbe3..a1639f7f 100644 --- a/includes/zhtable/toTW.manual +++ b/includes/zhtable/toTW.manual @@ -1,5 +1,3 @@ -内存 記憶體 -默认 預設 缺省 預設 串行 串列 以太网 乙太網 @@ -10,19 +8,13 @@ 光盘 光碟 光驱 光碟機 全角 全形 -共享 共用 -兼容 相容 -前缀 首碼 -后缀 尾碼 加载 載入 半角 半形 变量 變數 噪声 雜訊 -因子 因數 -在线 線上 脱机 離線 -域名 功能變數名稱 声卡 音效卡 +老字号 老字號 字号 字型大小 字库 字型檔 字段 欄位 @@ -51,6 +43,7 @@ 词组 片語 译码 解碼 软驱 軟碟機 +快闪存储器 快閃記憶體 闪存 快閃記憶體 鼠标 滑鼠 进制 進位 @@ -60,29 +53,22 @@ 传感 感測 便携式 攜帶型 信息论 資訊理論 -循环 迴圈 写保护 防寫 分布式 分散式 分辨率 解析度 -程序 程式 服务器 伺服器 等于 等於 局域网 區域網 -上载 上傳 计算机 電腦 -宏 巨集 扫瞄仪 掃瞄器 宽带 寬頻 -窗口 視窗 数据库 資料庫 -公历 西曆 奶酪 乳酪 巨商 鉅賈 手电 手電筒 万历 萬曆 永历 永曆 词汇 辭彙 -保安 保全 习用 慣用 元音 母音 任意球 自由球 @@ -103,8 +89,6 @@ 二極管 二極體 三极管 三極體 三極管 三極體 -数码 數位 -數碼 數位 软件 軟體 軟件 軟體 网络 網路 @@ -219,13 +203,12 @@ 斯洛文尼亞 斯洛維尼亞 新西兰 紐西蘭 新西蘭 紐西蘭 -朝鲜 北韓 格林纳达 格瑞那達 格林納達 格瑞那達 格鲁吉亚 喬治亞 格魯吉亞 喬治亞 -梵蒂冈 教廷 -梵蒂岡 教廷 +佐治亚 喬治亞 +佐治亞 喬治亞 毛里塔尼亚 茅利塔尼亞 毛里塔尼亞 茅利塔尼亞 毛里求斯 模里西斯 @@ -267,12 +250,11 @@ 阿塞拜疆 亞塞拜然 阿拉伯联合酋长国 阿拉伯聯合大公國 阿拉伯聯合酋長國 阿拉伯聯合大公國 -韩国 南韓 马尔代夫 馬爾地夫 馬爾代夫 馬爾地夫 马耳他 馬爾他 -马里 馬利 -馬里 馬利 +马里共和国 馬利共和國 +馬里共和國 馬利共和國 方便面 速食麵 快速面 速食麵 即食麵 速食麵 @@ -281,10 +263,7 @@ 绑紧跳 笨豬跳 冷菜 冷盤 凉菜 冷盤 -的士 計程車 出租车 計程車 -巴士 公車 -公共汽车 公車 台球 撞球 桌球 撞球 雪糕 冰淇淋 @@ -297,13 +276,15 @@ 雪铁龙 雪鐵龍 马自达 馬自達 萬事得 馬自達 +拿破仑 拿破崙 +拿破侖 拿破崙 布什 布希 布殊 布希 克林顿 柯林頓 克林頓 柯林頓 -萨达姆 海珊 -薩達姆 海珊 +侯赛因 海珊 +侯賽因 海珊 凡高 梵谷 狄安娜 黛安娜 戴安娜 黛安娜 -赫拉 希拉
\ No newline at end of file +赫拉 希拉 |