summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2008-03-21 11:49:34 +0100
committerPierre Schmitz <pierre@archlinux.de>2008-03-21 11:49:34 +0100
commit086ae52d12011746a75f5588e877347bc0457352 (patch)
treee73263c7a29d0f94fafb874562610e16eb292ba8 /includes
parent749e7fb2bae7bbda855de3c9e319435b9f698ff7 (diff)
Update auf MediaWiki 1.12.0
Diffstat (limited to 'includes')
-rw-r--r--includes/AjaxFunctions.php121
-rw-r--r--includes/Article.php718
-rw-r--r--includes/AuthPlugin.php16
-rw-r--r--includes/AutoLoader.php55
-rw-r--r--includes/Autopromote.php113
-rw-r--r--includes/BagOStuff.php35
-rw-r--r--includes/CategoryPage.php17
-rw-r--r--includes/ChangesList.php14
-rw-r--r--includes/CoreParserFunctions.php77
-rw-r--r--includes/Database.php52
-rw-r--r--includes/DatabasePostgres.php563
-rw-r--r--includes/DefaultSettings.php243
-rw-r--r--includes/Defines.php27
-rw-r--r--includes/DifferenceEngine.php256
-rw-r--r--includes/EditPage.php594
-rw-r--r--includes/Exception.php42
-rw-r--r--includes/Export.php2
-rw-r--r--includes/ExternalEdit.php8
-rw-r--r--includes/ExternalStore.php9
-rw-r--r--includes/ExternalStoreHttp.php6
-rw-r--r--includes/Feed.php6
-rw-r--r--includes/FileDeleteForm.php88
-rw-r--r--includes/FileRevertForm.php9
-rw-r--r--includes/FileStore.php2
-rw-r--r--includes/GlobalFunctions.php276
-rw-r--r--includes/HTMLCacheUpdate.php22
-rw-r--r--includes/ImageGallery.php2
-rw-r--r--includes/ImagePage.php72
-rw-r--r--includes/JobQueue.php3
-rw-r--r--includes/LinkBatch.php2
-rw-r--r--includes/LinkFilter.php9
-rw-r--r--includes/Linker.php348
-rw-r--r--includes/LinksUpdate.php11
-rw-r--r--includes/LoadBalancer.php28
-rw-r--r--includes/LogPage.php16
-rw-r--r--includes/MagicWord.php52
-rw-r--r--includes/Math.php15
-rw-r--r--includes/MessageCache.php149
-rw-r--r--includes/MimeMagic.php318
-rw-r--r--includes/Namespace.php5
-rw-r--r--includes/OutputHandler.php84
-rw-r--r--includes/OutputPage.php371
-rw-r--r--includes/PageHistory.php97
-rw-r--r--includes/Pager.php24
-rw-r--r--includes/Parser.php2143
-rw-r--r--includes/ParserOptions.php26
-rw-r--r--includes/ParserOutput.php23
-rw-r--r--includes/Parser_DiffTest.php85
-rw-r--r--includes/Parser_OldPP.php4942
-rw-r--r--includes/PrefixSearch.php135
-rw-r--r--includes/Preprocessor.php154
-rw-r--r--includes/Preprocessor_DOM.php1356
-rw-r--r--includes/Preprocessor_Hash.php1471
-rw-r--r--includes/ProfilerSimple.php4
-rw-r--r--includes/ProtectionForm.php59
-rw-r--r--includes/QueryPage.php11
-rw-r--r--includes/RawPage.php37
-rw-r--r--includes/RecentChange.php24
-rw-r--r--includes/Revision.php19
-rw-r--r--includes/Sanitizer.php2
-rw-r--r--includes/SearchEngine.php19
-rw-r--r--includes/SearchMySQL4.php10
-rw-r--r--includes/SearchPostgres.php27
-rw-r--r--includes/Setup.php5
-rw-r--r--includes/SiteConfiguration.php71
-rw-r--r--includes/Skin.php103
-rw-r--r--includes/SkinTemplate.php87
-rw-r--r--includes/SpecialAllmessages.php33
-rw-r--r--includes/SpecialAllpages.php38
-rw-r--r--includes/SpecialBlockip.php149
-rw-r--r--includes/SpecialBlockme.php5
-rw-r--r--includes/SpecialBooksources.php4
-rw-r--r--includes/SpecialBrokenRedirects.php2
-rw-r--r--includes/SpecialCategories.php4
-rw-r--r--includes/SpecialConfirmemail.php18
-rw-r--r--includes/SpecialContributions.php48
-rw-r--r--includes/SpecialDoubleRedirects.php4
-rw-r--r--includes/SpecialEmailuser.php47
-rw-r--r--includes/SpecialExport.php96
-rw-r--r--includes/SpecialFilepath.php69
-rw-r--r--includes/SpecialImport.php156
-rw-r--r--includes/SpecialIpblocklist.php100
-rw-r--r--includes/SpecialListredirects.php15
-rw-r--r--includes/SpecialLockdb.php4
-rw-r--r--includes/SpecialLog.php37
-rw-r--r--includes/SpecialMIMEsearch.php2
-rw-r--r--includes/SpecialMergeHistory.php423
-rw-r--r--includes/SpecialMostlinked.php2
-rw-r--r--includes/SpecialMostlinkedcategories.php2
-rw-r--r--includes/SpecialMostlinkedtemplates.php16
-rw-r--r--includes/SpecialMovepage.php42
-rw-r--r--includes/SpecialNewimages.php2
-rw-r--r--includes/SpecialNewpages.php309
-rw-r--r--includes/SpecialPage.php80
-rw-r--r--includes/SpecialPreferences.php82
-rw-r--r--includes/SpecialPrefixindex.php10
-rw-r--r--includes/SpecialProtectedpages.php3
-rwxr-xr-xincludes/SpecialProtectedtitles.php219
-rw-r--r--includes/SpecialRandompage.php66
-rw-r--r--includes/SpecialRandomredirect.php27
-rw-r--r--includes/SpecialRecentchanges.php69
-rw-r--r--includes/SpecialRecentchangeslinked.php23
-rw-r--r--includes/SpecialResetpass.php4
-rw-r--r--includes/SpecialRevisiondelete.php4
-rw-r--r--includes/SpecialSearch.php41
-rw-r--r--includes/SpecialShortpages.php2
-rw-r--r--includes/SpecialSpecialpages.php2
-rw-r--r--includes/SpecialStatistics.php10
-rw-r--r--includes/SpecialUndelete.php248
-rw-r--r--includes/SpecialUnlockdb.php6
-rw-r--r--includes/SpecialUnusedimages.php22
-rw-r--r--includes/SpecialUpload.php339
-rw-r--r--includes/SpecialUserlogin.php91
-rw-r--r--includes/SpecialUserlogout.php15
-rw-r--r--includes/SpecialUserrights.php431
-rw-r--r--includes/SpecialVersion.php165
-rw-r--r--includes/SpecialWantedcategories.php2
-rw-r--r--includes/SpecialWantedpages.php2
-rw-r--r--includes/SpecialWatchlist.php50
-rw-r--r--includes/SpecialWhatlinkshere.php66
-rw-r--r--includes/SpecialWithoutinterwiki.php41
-rw-r--r--includes/SquidUpdate.php2
-rw-r--r--includes/StreamFile.php6
-rw-r--r--includes/StubObject.php3
-rw-r--r--includes/Title.php542
-rw-r--r--includes/User.php321
-rw-r--r--includes/UserMailer.php461
-rw-r--r--includes/UserRightsProxy.php161
-rw-r--r--includes/WatchlistEditor.php40
-rw-r--r--includes/WebRequest.php44
-rw-r--r--includes/Wiki.php35
-rw-r--r--includes/WikiError.php8
-rw-r--r--includes/Xml.php67
-rw-r--r--includes/XmlTypeCheck.php93
-rw-r--r--includes/ZhConversion.php1723
-rw-r--r--includes/api/ApiBase.php186
-rw-r--r--includes/api/ApiBlock.php164
-rw-r--r--includes/api/ApiChangeRights.php155
-rw-r--r--includes/api/ApiDelete.php155
-rw-r--r--includes/api/ApiExpandTemplates.php97
-rw-r--r--includes/api/ApiFeedWatchlist.php29
-rw-r--r--includes/api/ApiFormatBase.php52
-rw-r--r--includes/api/ApiFormatDbg.php59
-rw-r--r--includes/api/ApiFormatJson.php8
-rw-r--r--includes/api/ApiFormatPhp.php4
-rw-r--r--includes/api/ApiFormatTxt.php59
-rw-r--r--includes/api/ApiFormatWddx.php4
-rw-r--r--includes/api/ApiFormatXml.php4
-rw-r--r--includes/api/ApiFormatYaml.php4
-rw-r--r--includes/api/ApiFormatYaml_spyc.php14
-rw-r--r--includes/api/ApiHelp.php8
-rw-r--r--includes/api/ApiLogin.php22
-rw-r--r--includes/api/ApiLogout.php71
-rw-r--r--includes/api/ApiMain.php112
-rw-r--r--includes/api/ApiMove.php152
-rw-r--r--includes/api/ApiOpenSearch.php51
-rw-r--r--includes/api/ApiParamInfo.php167
-rw-r--r--includes/api/ApiParse.php202
-rw-r--r--includes/api/ApiProtect.php154
-rw-r--r--includes/api/ApiQuery.php26
-rw-r--r--includes/api/ApiQueryAllCategories.php142
-rw-r--r--includes/api/ApiQueryAllLinks.php8
-rw-r--r--includes/api/ApiQueryAllUsers.php17
-rw-r--r--includes/api/ApiQueryAllmessages.php129
-rw-r--r--includes/api/ApiQueryAllpages.php48
-rw-r--r--includes/api/ApiQueryBacklinks.php10
-rw-r--r--includes/api/ApiQueryBase.php7
-rw-r--r--includes/api/ApiQueryBlocks.php239
-rw-r--r--includes/api/ApiQueryCategories.php8
-rw-r--r--includes/api/ApiQueryCategoryMembers.php61
-rw-r--r--includes/api/ApiQueryDeletedrevs.php235
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php8
-rw-r--r--includes/api/ApiQueryExternalLinks.php4
-rw-r--r--includes/api/ApiQueryImageInfo.php170
-rw-r--r--includes/api/ApiQueryImages.php4
-rw-r--r--includes/api/ApiQueryInfo.php113
-rw-r--r--includes/api/ApiQueryLangLinks.php4
-rw-r--r--includes/api/ApiQueryLinks.php8
-rw-r--r--includes/api/ApiQueryLogEvents.php9
-rw-r--r--includes/api/ApiQueryRandom.php157
-rw-r--r--includes/api/ApiQueryRecentChanges.php110
-rw-r--r--includes/api/ApiQueryRevisions.php77
-rw-r--r--includes/api/ApiQuerySearch.php8
-rw-r--r--includes/api/ApiQuerySiteinfo.php39
-rw-r--r--includes/api/ApiQueryUserContributions.php36
-rw-r--r--includes/api/ApiQueryUserInfo.php138
-rw-r--r--includes/api/ApiQueryUsers.php162
-rw-r--r--includes/api/ApiQueryWatchlist.php50
-rw-r--r--includes/api/ApiResult.php45
-rw-r--r--includes/api/ApiRollback.php128
-rw-r--r--includes/api/ApiUnblock.php124
-rw-r--r--includes/api/ApiUndelete.php123
-rw-r--r--includes/filerepo/ArchivedFile.php306
-rw-r--r--includes/filerepo/FSRepo.php2
-rw-r--r--includes/filerepo/File.php75
-rw-r--r--includes/filerepo/FileRepo.php25
-rw-r--r--includes/filerepo/FileRepoStatus.php12
-rw-r--r--includes/filerepo/ICRepo.php204
-rw-r--r--includes/filerepo/LocalFile.php128
-rw-r--r--includes/filerepo/LocalRepo.php48
-rw-r--r--includes/filerepo/NullRepo.php34
-rw-r--r--includes/filerepo/RepoGroup.php47
-rw-r--r--includes/media/Generic.php36
-rw-r--r--includes/mime.info1
-rw-r--r--includes/mime.types2
-rw-r--r--includes/templates/Userlogin.php51
-rw-r--r--includes/zhtable/Makefile86
-rw-r--r--includes/zhtable/toCN.manual41
-rw-r--r--includes/zhtable/toHK.manual34
-rw-r--r--includes/zhtable/toTW.manual41
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>&nbsp;</td>
- <td>$watch</td>
- </tr>
- <tr>
- <td>&nbsp;</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 = "&nbsp;&nbsp;&nbsp;<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 = "&nbsp;&nbsp;&nbsp;<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 &nbsp; 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( '&amp;', '&', $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( '&#91;', '&#124;', '&#39;', 'ISBN&#32;', 'RFC&#32;', '&#58;//', "\n&#61;", '&#123;&#123;' ),
+ array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ),
+ array( '&#91;', '&#124;', '&#93;', '&#39;', 'ISBN&#32;', 'RFC&#32;', '&#58;//', "\n&#61;", '&#123;&#123;' ),
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 '&lt;' . htmlspecialchars($key) . '&gt;';
}
-
- # 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( '-->', '--&gt;', $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&nbsp;\\2',
+ # french spaces, Guillemet-right
+ '/(\\302\\253) /' => '\\1&nbsp;',
+ );
+ $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&lt;div\\5&gt;\\6&lt;/div&gt;\\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 &ouml; 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('&lt;', '&gt;'), 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