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 |