summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
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
+ # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+ $text = $this->replaceVariables( $text );
+
+ # Strip out <nowiki> etc. added via replaceVariables
+ $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
+
+ # Signatures
+ $sigText = $this->getUserSig( $user );
+ $text = strtr( $text, array(
+ '~~~~~' => $d,
+ '~~~~' => "$sigText $d",
+ '~~~' => $sigText
+ ) );
+
+ # Context links: [[|name]] and [[name (context)|]]
+ #
+ global $wgLegalTitleChars;
+ $tc = "[$wgLegalTitleChars]";
+ $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
+
+ $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
+ $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
+ $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
+
+ # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
+ $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
+ $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
+
+ $t = $this->mTitle->getText();
+ $m = array();
+ if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
+ $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+ } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
+ $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+ } else {
+ # if there's no context, don't bother duplicating the title
+ $text = preg_replace( $p2, '[[\\1]]', $text );
+ }
+
+ # Trim trailing whitespace
+ $text = rtrim( $text );
+
+ return $text;
+ }
+
+ /**
+ * Fetch the user's signature text, if any, and normalize to
+ * validated, ready-to-insert wikitext.
+ *
+ * @param User $user
+ * @return string
+ * @private
+ */
+ function getUserSig( &$user ) {
+ global $wgMaxSigChars;
+
+ $username = $user->getName();
+ $nickname = $user->getOption( 'nickname' );
+ $nickname = $nickname === '' ? $username : $nickname;
+
+ if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+ $nickname = $username;
+ wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
+ } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
+ # Sig. might contain markup; validate this
+ if( $this->validateSig( $nickname ) !== false ) {
+ # Validated; clean up (if needed) and return it
+ return $this->cleanSig( $nickname, true );
+ } else {
+ # Failed to validate; fall back to the default
+ $nickname = $username;
+ wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+ }
+ }
+
+ // Make sure nickname doesnt get a sig in a sig
+ $nickname = $this->cleanSigInSig( $nickname );
+
+ # If we're still here, make it a link to the user page
+ $userText = wfEscapeWikiText( $username );
+ $nickText = wfEscapeWikiText( $nickname );
+ if ( $user->isAnon() ) {
+ return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
+ } else {
+ return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
+ }
+ }
+
+ /**
+ * Check that the user's signature contains no bad XML
+ *
+ * @param string $text
+ * @return mixed An expanded string, or false if invalid.
+ */
+ function validateSig( $text ) {
+ return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+ }
+
+ /**
+ * Clean up signature text
+ *
+ * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
+ * 2) Substitute all transclusions
+ *
+ * @param string $text
+ * @param $parsing Whether we're cleaning (preferences save) or parsing
+ * @return string Signature text
+ */
+ function cleanSig( $text, $parsing = false ) {
+ global $wgTitle;
+ $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG );
+
+ $substWord = MagicWord::get( 'subst' );
+ $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
+ $substText = '{{' . $substWord->getSynonym( 0 );
+
+ $text = preg_replace( $substRegex, $substText, $text );
+ $text = $this->cleanSigInSig( $text );
+ $text = $this->replaceVariables( $text );
+
+ $this->clearState();
+ return $text;
+ }
+
+ /**
+ * Strip ~~~, ~~~~ and ~~~~~ out of signatures
+ * @param string $text
+ * @return string Signature text with /~{3,5}/ removed
+ */
+ function cleanSigInSig( $text ) {
+ $text = preg_replace( '/~{3,5}/', '', $text );
+ return $text;
+ }
+
+ /**
+ * Set up some variables which are usually set up in parse()
+ * so that an external function can call some class members with confidence
+ * @public
+ */
+ function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+ $this->mTitle =& $title;
+ $this->mOptions = $options;
+ $this->setOutputType( $outputType );
+ if ( $clearState ) {
+ $this->clearState();
+ }
+ }
+
+ /**
+ * Transform a MediaWiki message by replacing magic variables.
+ *
+ * @param string $text the text to transform
+ * @param ParserOptions $options options
+ * @return string the text with variables substituted
+ * @public
+ */
+ function transformMsg( $text, $options ) {
+ global $wgTitle;
+ static $executing = false;
+
+ $fname = "Parser::transformMsg";
+
+ # Guard against infinite recursion
+ if ( $executing ) {
+ return $text;
+ }
+ $executing = true;
+
+ wfProfileIn($fname);
+
+ if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) {
+ $this->mTitle = $wgTitle;
+ } else {
+ $this->mTitle = Title::newFromText('msg');
+ }
+ $this->mOptions = $options;
+ $this->setOutputType( self::OT_MSG );
+ $this->clearState();
+ $text = $this->replaceVariables( $text );
+
+ $executing = false;
+ wfProfileOut($fname);
+ return $text;
+ }
+
+ /**
+ * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+ * The callback should have the following form:
+ * function myParserHook( $text, $params, &$parser ) { ... }
+ *
+ * Transform and return $text. Use $parser for any required context, e.g. use
+ * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
+ *
+ * @public
+ *
+ * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
+ * @param mixed $callback The callback function (and object) to use for the tag
+ *
+ * @return The old value of the mTagHooks array associated with the hook
+ */
+ function setHook( $tag, $callback ) {
+ $tag = strtolower( $tag );
+ $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
+ $this->mTagHooks[$tag] = $callback;
+
+ return $oldVal;
+ }
+
+ function setTransparentTagHook( $tag, $callback ) {
+ $tag = strtolower( $tag );
+ $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
+ $this->mTransparentTagHooks[$tag] = $callback;
+
+ return $oldVal;
+ }
+
+ /**
+ * Create a function, e.g. {{sum:1|2|3}}
+ * The callback function should have the form:
+ * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
+ *
+ * The callback may either return the text result of the function, or an array with the text
+ * in element 0, and a number of flags in the other elements. The names of the flags are
+ * specified in the keys. Valid flags are:
+ * found The text returned is valid, stop processing the template. This
+ * is on by default.
+ * nowiki Wiki markup in the return value should be escaped
+ * noparse Unsafe HTML tags should not be stripped, etc.
+ * noargs Don't replace triple-brace arguments in the return value
+ * isHTML The returned text is HTML, armour it against wikitext transformation
+ *
+ * @public
+ *
+ * @param string $id The magic word ID
+ * @param mixed $callback The callback function (and object) to use
+ * @param integer $flags a combination of the following flags:
+ * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
+ *
+ * @return The old callback function for this name, if any
+ */
+ function setFunctionHook( $id, $callback, $flags = 0 ) {
+ $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null;
+ $this->mFunctionHooks[$id] = $callback;
+
+ # Add to function cache
+ $mw = MagicWord::get( $id );
+ if( !$mw )
+ throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+
+ $synonyms = $mw->getSynonyms();
+ $sensitive = intval( $mw->isCaseSensitive() );
+
+ foreach ( $synonyms as $syn ) {
+ # Case
+ if ( !$sensitive ) {
+ $syn = strtolower( $syn );
+ }
+ # Add leading hash
+ if ( !( $flags & SFH_NO_HASH ) ) {
+ $syn = '#' . $syn;
+ }
+ # Remove trailing colon
+ if ( substr( $syn, -1, 1 ) == ':' ) {
+ $syn = substr( $syn, 0, -1 );
+ }
+ $this->mFunctionSynonyms[$sensitive][$syn] = $id;
+ }
+ return $oldVal;
+ }
+
+ /**
+ * Get all registered function hook identifiers
+ *
+ * @return array
+ */
+ function getFunctionHooks() {
+ return array_keys( $this->mFunctionHooks );
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with actual links, in the buffer
+ * Placeholders created in Skin::makeLinkObj()
+ * Returns an array of links found, indexed by PDBK:
+ * 0 - broken
+ * 1 - normal link
+ * 2 - stub
+ * $options is a bit field, RLH_FOR_UPDATE to select for update
+ */
+ function replaceLinkHolders( &$text, $options = 0 ) {
+ global $wgUser;
+ global $wgContLang;
+
+ $fname = 'Parser::replaceLinkHolders';
+ wfProfileIn( $fname );
+
+ $pdbks = array();
+ $colours = array();
+ $sk = $this->mOptions->getSkin();
+ $linkCache =& LinkCache::singleton();
+
+ if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
+ wfProfileIn( $fname.'-check' );
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $threshold = $wgUser->getOption('stubthreshold');
+
+ # Sort by namespace
+ asort( $this->mLinkHolders['namespaces'] );
+
+ # Generate query
+ $query = false;
+ $current = null;
+ foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+ # Make title object
+ $title = $this->mLinkHolders['titles'][$key];
+
+ # Skip invalid entries.
+ # Result will be ugly, but prevents crash.
+ if ( is_null( $title ) ) {
+ continue;
+ }
+ $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
+
+ # Check if it's a static known link, e.g. interwiki
+ if ( $title->isAlwaysKnown() ) {
+ $colours[$pdbk] = 1;
+ } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+ $colours[$pdbk] = 1;
+ $this->mOutput->addLink( $title, $id );
+ } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+ $colours[$pdbk] = 0;
+ } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
+ $colours[$pdbk] = 0;
+ } else {
+ # Not in the link cache, add it to the query
+ if ( !isset( $current ) ) {
+ $current = $ns;
+ $query = "SELECT page_id, page_namespace, page_title";
+ if ( $threshold > 0 ) {
+ $query .= ', page_len, page_is_redirect';
+ }
+ $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
+ } elseif ( $current != $ns ) {
+ $current = $ns;
+ $query .= ")) OR (page_namespace=$ns AND page_title IN(";
+ } else {
+ $query .= ', ';
+ }
+
+ $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
+ }
+ }
+ if ( $query ) {
+ $query .= '))';
+ if ( $options & RLH_FOR_UPDATE ) {
+ $query .= ' FOR UPDATE';
+ }
+
+ $res = $dbr->query( $query, $fname );
+
+ # Fetch data and form into an associative array
+ # non-existent = broken
+ # 1 = known
+ # 2 = stub
+ while ( $s = $dbr->fetchObject($res) ) {
+ $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $pdbk = $title->getPrefixedDBkey();
+ $linkCache->addGoodLinkObj( $s->page_id, $title );
+ $this->mOutput->addLink( $title, $s->page_id );
+
+ $colours[$pdbk] = ( $threshold == 0 || (
+ $s->page_len >= $threshold || # always true if $threshold <= 0
+ $s->page_is_redirect ||
+ !Namespace::isContent( $s->page_namespace ) )
+ ? 1 : 2 );
+ }
+ }
+ wfProfileOut( $fname.'-check' );
+
+ # Do a second query for different language variants of links and categories
+ if($wgContLang->hasVariants()){
+ $linkBatch = new LinkBatch();
+ $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
+ $categoryMap = array(); // maps $category_variant => $category (dbkeys)
+ $varCategories = array(); // category replacements oldDBkey => newDBkey
+
+ $categories = $this->mOutput->getCategoryLinks();
+
+ // Add variants of links to link batch
+ foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+ $title = $this->mLinkHolders['titles'][$key];
+ if ( is_null( $title ) )
+ continue;
+
+ $pdbk = $title->getPrefixedDBkey();
+ $titleText = $title->getText();
+
+ // generate all variants of the link title text
+ $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
+
+ // if link was not found (in first query), add all variants to query
+ if ( !isset($colours[$pdbk]) ){
+ foreach($allTextVariants as $textVariant){
+ if($textVariant != $titleText){
+ $variantTitle = Title::makeTitle( $ns, $textVariant );
+ if(is_null($variantTitle)) continue;
+ $linkBatch->addObj( $variantTitle );
+ $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
+ }
+ }
+ }
+ }
+
+ // process categories, check if a category exists in some variant
+ foreach( $categories as $category ){
+ $variants = $wgContLang->convertLinkToAllVariants($category);
+ foreach($variants as $variant){
+ if($variant != $category){
+ $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
+ if(is_null($variantTitle)) continue;
+ $linkBatch->addObj( $variantTitle );
+ $categoryMap[$variant] = $category;
+ }
+ }
+ }
+
+
+ if(!$linkBatch->isEmpty()){
+ // construct query
+ $titleClause = $linkBatch->constructSet('page', $dbr);
+
+ $variantQuery = "SELECT page_id, page_namespace, page_title";
+ if ( $threshold > 0 ) {
+ $variantQuery .= ', page_len, page_is_redirect';
+ }
+
+ $variantQuery .= " FROM $page WHERE $titleClause";
+ if ( $options & RLH_FOR_UPDATE ) {
+ $variantQuery .= ' FOR UPDATE';
+ }
+
+ $varRes = $dbr->query( $variantQuery, $fname );
+
+ // for each found variants, figure out link holders and replace
+ while ( $s = $dbr->fetchObject($varRes) ) {
+
+ $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $varPdbk = $variantTitle->getPrefixedDBkey();
+ $vardbk = $variantTitle->getDBkey();
+
+ $holderKeys = array();
+ if(isset($variantMap[$varPdbk])){
+ $holderKeys = $variantMap[$varPdbk];
+ $linkCache->addGoodLinkObj( $s->page_id, $variantTitle );
+ $this->mOutput->addLink( $variantTitle, $s->page_id );
+ }
+
+ // loop over link holders
+ foreach($holderKeys as $key){
+ $title = $this->mLinkHolders['titles'][$key];
+ if ( is_null( $title ) ) continue;
+
+ $pdbk = $title->getPrefixedDBkey();
+
+ if(!isset($colours[$pdbk])){
+ // found link in some of the variants, replace the link holder data
+ $this->mLinkHolders['titles'][$key] = $variantTitle;
+ $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
+
+ // set pdbk and colour
+ $pdbks[$key] = $varPdbk;
+ if ( $threshold > 0 ) {
+ $size = $s->page_len;
+ if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
+ $colours[$varPdbk] = 1;
+ } else {
+ $colours[$varPdbk] = 2;
+ }
+ }
+ else {
+ $colours[$varPdbk] = 1;
+ }
+ }
+ }
+
+ // check if the object is a variant of a category
+ if(isset($categoryMap[$vardbk])){
+ $oldkey = $categoryMap[$vardbk];
+ if($oldkey != $vardbk)
+ $varCategories[$oldkey]=$vardbk;
+ }
+ }
+
+ // rebuild the categories in original order (if there are replacements)
+ if(count($varCategories)>0){
+ $newCats = array();
+ $originalCats = $this->mOutput->getCategories();
+ foreach($originalCats as $cat => $sortkey){
+ // make the replacement
+ if( array_key_exists($cat,$varCategories) )
+ $newCats[$varCategories[$cat]] = $sortkey;
+ else $newCats[$cat] = $sortkey;
+ }
+ $this->mOutput->setCategoryLinks($newCats);
+ }
+ }
+ }
+
+ # Construct search and replace arrays
+ wfProfileIn( $fname.'-construct' );
+ $replacePairs = array();
+ foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+ $pdbk = $pdbks[$key];
+ $searchkey = "<!--LINK $key-->";
+ $title = $this->mLinkHolders['titles'][$key];
+ if ( empty( $colours[$pdbk] ) ) {
+ $linkCache->addBadLinkObj( $title );
+ $colours[$pdbk] = 0;
+ $this->mOutput->addLink( $title, 0 );
+ $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ } elseif ( $colours[$pdbk] == 1 ) {
+ $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ } elseif ( $colours[$pdbk] == 2 ) {
+ $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ }
+ }
+ $replacer = new HashtableReplacer( $replacePairs, 1 );
+ wfProfileOut( $fname.'-construct' );
+
+ # Do the thing
+ wfProfileIn( $fname.'-replace' );
+ $text = preg_replace_callback(
+ '/(<!--LINK .*?-->)/',
+ $replacer->cb(),
+ $text);
+
+ wfProfileOut( $fname.'-replace' );
+ }
+
+ # Now process interwiki link holders
+ # This is quite a bit simpler than internal links
+ if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
+ wfProfileIn( $fname.'-interwiki' );
+ # Make interwiki link HTML
+ $replacePairs = array();
+ foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
+ $title = $this->mInterwikiLinkHolders['titles'][$key];
+ $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
+ }
+ $replacer = new HashtableReplacer( $replacePairs, 1 );
+
+ $text = preg_replace_callback(
+ '/<!--IWLINK (.*?)-->/',
+ $replacer->cb(),
+ $text );
+ wfProfileOut( $fname.'-interwiki' );
+ }
+
+ wfProfileOut( $fname );
+ return $colours;
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with plain text of links
+ * (not HTML-formatted).
+ * @param string $text
+ * @return string
+ */
+ function replaceLinkHoldersText( $text ) {
+ $fname = 'Parser::replaceLinkHoldersText';
+ wfProfileIn( $fname );
+
+ $text = preg_replace_callback(
+ '/<!--(LINK|IWLINK) (.*?)-->/',
+ array( &$this, 'replaceLinkHoldersTextCallback' ),
+ $text );
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function replaceLinkHoldersTextCallback( $matches ) {
+ $type = $matches[1];
+ $key = $matches[2];
+ if( $type == 'LINK' ) {
+ if( isset( $this->mLinkHolders['texts'][$key] ) ) {
+ return $this->mLinkHolders['texts'][$key];
+ }
+ } elseif( $type == 'IWLINK' ) {
+ if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
+ return $this->mInterwikiLinkHolders['texts'][$key];
+ }
+ }
+ return $matches[0];
+ }
+
+ /**
+ * Tag hook handler for 'pre'.
+ */
+ function renderPreTag( $text, $attribs ) {
+ // Backwards-compatibility hack
+ $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
+
+ $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+ return wfOpenElement( 'pre', $attribs ) .
+ Xml::escapeTagsOnly( $content ) .
+ '</pre>';
+ }
+
+ /**
+ * Renders an image gallery from a text with one line per image.
+ * text labels may be given by using |-style alternative text. E.g.
+ * Image:one.jpg|The number "1"
+ * Image:tree.jpg|A tree
+ * given as text will return the HTML of a gallery with two images,
+ * labeled 'The number "1"' and
+ * 'A tree'.
+ */
+ function renderImageGallery( $text, $params ) {
+ $ig = new ImageGallery();
+ $ig->setContextTitle( $this->mTitle );
+ $ig->setShowBytes( false );
+ $ig->setShowFilename( false );
+ $ig->setParser( $this );
+ $ig->setHideBadImages();
+ $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
+ $ig->useSkin( $this->mOptions->getSkin() );
+ $ig->mRevisionId = $this->mRevisionId;
+
+ if( isset( $params['caption'] ) ) {
+ $caption = $params['caption'];
+ $caption = htmlspecialchars( $caption );
+ $caption = $this->replaceInternalLinks( $caption );
+ $ig->setCaptionHtml( $caption );
+ }
+ if( isset( $params['perrow'] ) ) {
+ $ig->setPerRow( $params['perrow'] );
+ }
+ if( isset( $params['widths'] ) ) {
+ $ig->setWidths( $params['widths'] );
+ }
+ if( isset( $params['heights'] ) ) {
+ $ig->setHeights( $params['heights'] );
+ }
+
+ wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
+
+ $lines = explode( "\n", $text );
+ foreach ( $lines as $line ) {
+ # match lines like these:
+ # Image:someimage.jpg|This is some image
+ $matches = array();
+ preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
+ # Skip empty lines
+ if ( count( $matches ) == 0 ) {
+ continue;
+ }
+ $tp = Title::newFromText( $matches[1] );
+ $nt =& $tp;
+ if( is_null( $nt ) ) {
+ # Bogus title. Ignore these so we don't bomb out later.
+ continue;
+ }
+ if ( isset( $matches[3] ) ) {
+ $label = $matches[3];
+ } else {
+ $label = '';
+ }
+
+ $pout = $this->parse( $label,
+ $this->mTitle,
+ $this->mOptions,
+ false, // Strip whitespace...?
+ false // Don't clear state!
+ );
+ $html = $pout->getText();
+
+ $ig->add( $nt, $html );
+
+ # Only add real images (bug #5586)
+ if ( $nt->getNamespace() == NS_IMAGE ) {
+ $this->mOutput->addImage( $nt->getDBkey() );
+ }
+ }
+ return $ig->toHTML();
+ }
+
+ function getImageParams( $handler ) {
+ if ( $handler ) {
+ $handlerClass = get_class( $handler );
+ } else {
+ $handlerClass = '';
+ }
+ if ( !isset( $this->mImageParams[$handlerClass] ) ) {
+ // Initialise static lists
+ static $internalParamNames = array(
+ 'horizAlign' => array( 'left', 'right', 'center', 'none' ),
+ 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
+ 'bottom', 'text-bottom' ),
+ 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
+ 'upright', 'border' ),
+ );
+ static $internalParamMap;
+ if ( !$internalParamMap ) {
+ $internalParamMap = array();
+ foreach ( $internalParamNames as $type => $names ) {
+ foreach ( $names as $name ) {
+ $magicName = str_replace( '-', '_', "img_$name" );
+ $internalParamMap[$magicName] = array( $type, $name );
+ }
+ }
+ }
+
+ // Add handler params
+ $paramMap = $internalParamMap;
+ if ( $handler ) {
+ $handlerParamMap = $handler->getParamMap();
+ foreach ( $handlerParamMap as $magic => $paramName ) {
+ $paramMap[$magic] = array( 'handler', $paramName );
+ }
+ }
+ $this->mImageParams[$handlerClass] = $paramMap;
+ $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
+ }
+ return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
+ }
+
+ /**
+ * Parse image options text and use it to make an image
+ */
+ function makeImage( $title, $options ) {
+ # @TODO: let the MediaHandler specify its transform parameters
+ #
+ # Check if the options text is of the form "options|alt text"
+ # Options are:
+ # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
+ # * left no resizing, just left align. label is used for alt= only
+ # * right same, but right aligned
+ # * none same, but not aligned
+ # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
+ # * center center the image
+ # * framed Keep original image size, no magnify-button.
+ # * frameless like 'thumb' but without a frame. Keeps user preferences for width
+ # * upright reduce width for upright images, rounded to full __0 px
+ # * border draw a 1px border around the image
+ # vertical-align values (no % or length right now):
+ # * baseline
+ # * sub
+ # * super
+ # * top
+ # * text-top
+ # * middle
+ # * bottom
+ # * text-bottom
+
+ $parts = array_map( 'trim', explode( '|', $options) );
+ $sk = $this->mOptions->getSkin();
+
+ # Give extensions a chance to select the file revision for us
+ $skip = $time = false;
+ wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) );
+
+ if ( $skip ) {
+ return $sk->makeLinkObj( $title );
+ }
+
+ # Get parameter map
+ $file = wfFindFile( $title, $time );
+ $handler = $file ? $file->getHandler() : false;
+
+ list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
+
+ # Process the input parameters
+ $caption = '';
+ $params = array( 'frame' => array(), 'handler' => array(),
+ 'horizAlign' => array(), 'vertAlign' => array() );
+ foreach( $parts as $part ) {
+ list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
+ if ( isset( $paramMap[$magicName] ) ) {
+ list( $type, $paramName ) = $paramMap[$magicName];
+ $params[$type][$paramName] = $value;
+
+ // Special case; width and height come in one variable together
+ if( $type == 'handler' && $paramName == 'width' ) {
+ $m = array();
+ if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) {
+ $params[$type]['width'] = intval( $m[1] );
+ $params[$type]['height'] = intval( $m[2] );
+ } else {
+ $params[$type]['width'] = intval( $value );
+ }
+ }
+ } else {
+ $caption = $part;
+ }
+ }
+
+ # Process alignment parameters
+ if ( $params['horizAlign'] ) {
+ $params['frame']['align'] = key( $params['horizAlign'] );
+ }
+ if ( $params['vertAlign'] ) {
+ $params['frame']['valign'] = key( $params['vertAlign'] );
+ }
+
+ # Validate the handler parameters
+ if ( $handler ) {
+ foreach ( $params['handler'] as $name => $value ) {
+ if ( !$handler->validateParam( $name, $value ) ) {
+ unset( $params['handler'][$name] );
+ }
+ }
+ }
+
+ # Strip bad stuff out of the alt text
+ $alt = $this->replaceLinkHoldersText( $caption );
+
+ # make sure there are no placeholders in thumbnail attributes
+ # that are later expanded to html- so expand them now and
+ # remove the tags
+ $alt = $this->mStripState->unstripBoth( $alt );
+ $alt = Sanitizer::stripAllTags( $alt );
+
+ $params['frame']['alt'] = $alt;
+ $params['frame']['caption'] = $caption;
+
+ # Linker does the rest
+ $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] );
+
+ # Give the handler a chance to modify the parser object
+ if ( $handler ) {
+ $handler->parserTransformHook( $this, $file );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Set a flag in the output object indicating that the content is dynamic and
+ * shouldn't be cached.
+ */
+ function disableCache() {
+ wfDebug( "Parser output marked as uncacheable.\n" );
+ $this->mOutput->mCacheTime = -1;
+ }
+
+ /**#@+
+ * Callback from the Sanitizer for expanding items found in HTML attribute
+ * values, so they can be safely tested and escaped.
+ * @param string $text
+ * @param array $args
+ * @return string
+ * @private
+ */
+ function attributeStripCallback( &$text, $args ) {
+ $text = $this->replaceVariables( $text, $args );
+ $text = $this->mStripState->unstripBoth( $text );
+ return $text;
+ }
+
+ /**#@-*/
+
+ /**#@+
+ * Accessor/mutator
+ */
+ function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
+ function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
+ function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
+ /**#@-*/
+
+ /**#@+
+ * Accessor
+ */
+ function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
+ /**#@-*/
+
+
+ /**
+ * Break wikitext input into sections, and either pull or replace
+ * some particular section's text.
+ *
+ * External callers should use the getSection and replaceSection methods.
+ *
+ * @param $text Page wikitext
+ * @param $section Numbered section. 0 pulls the text before the first
+ * heading; other numbers will pull the given section
+ * along with its lower-level subsections.
+ * @param $mode One of "get" or "replace"
+ * @param $newtext Replacement text for section data.
+ * @return string for "get", the extracted section text.
+ * for "replace", the whole page with the section replaced.
+ */
+ private function extractSections( $text, $section, $mode, $newtext='' ) {
+ # I.... _hope_ this is right.
+ # Otherwise, sometimes we don't have things initialized properly.
+ $this->clearState();
+
+ # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
+ # comments to be stripped as well)
+ $stripState = new StripState;
+
+ $oldOutputType = $this->mOutputType;
+ $oldOptions = $this->mOptions;
+ $this->mOptions = new ParserOptions();
+ $this->setOutputType( self::OT_WIKI );
+
+ $striptext = $this->strip( $text, $stripState, true );
+
+ $this->setOutputType( $oldOutputType );
+ $this->mOptions = $oldOptions;
+
+ # now that we can be sure that no pseudo-sections are in the source,
+ # split it up by section
+ $uniq = preg_quote( $this->uniqPrefix(), '/' );
+ $comment = "(?:$uniq-!--.*?QINU\x07)";
+ $secs = preg_split(
+ "/
+ (
+ ^
+ (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+ (=+) # Should this be limited to 6?
+ .+? # Section title...
+ \\2 # Ending = count must match start
+ (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
+ $
+ |
+ <h([1-6])\b.*?>
+ .*?
+ <\/h\\3\s*>
+ )
+ /mix",
+ $striptext, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ if( $mode == "get" ) {
+ if( $section == 0 ) {
+ // "Section 0" returns the content before any other section.
+ $rv = $secs[0];
+ } else {
+ //track missing section, will replace if found.
+ $rv = $newtext;
+ }
+ } elseif( $mode == "replace" ) {
+ if( $section == 0 ) {
+ $rv = $newtext . "\n\n";
+ $remainder = true;
+ } else {
+ $rv = $secs[0];
+ $remainder = false;
+ }
+ }
+ $count = 0;
+ $sectionLevel = 0;
+ for( $index = 1; $index < count( $secs ); ) {
+ $headerLine = $secs[$index++];
+ if( $secs[$index] ) {
+ // A wiki header
+ $headerLevel = strlen( $secs[$index++] );
+ } else {
+ // An HTML header
+ $index++;
+ $headerLevel = intval( $secs[$index++] );
+ }
+ $content = $secs[$index++];
+
+ $count++;
+ if( $mode == "get" ) {
+ if( $count == $section ) {
+ $rv = $headerLine . $content;
+ $sectionLevel = $headerLevel;
+ } elseif( $count > $section ) {
+ if( $sectionLevel && $headerLevel > $sectionLevel ) {
+ $rv .= $headerLine . $content;
+ } else {
+ // Broke out to a higher-level section
+ break;
+ }
+ }
+ } elseif( $mode == "replace" ) {
+ if( $count < $section ) {
+ $rv .= $headerLine . $content;
+ } elseif( $count == $section ) {
+ $rv .= $newtext . "\n\n";
+ $sectionLevel = $headerLevel;
+ } elseif( $count > $section ) {
+ if( $headerLevel <= $sectionLevel ) {
+ // Passed the section's sub-parts.
+ $remainder = true;
+ }
+ if( $remainder ) {
+ $rv .= $headerLine . $content;
+ }
+ }
+ }
+ }
+ if (is_string($rv))
+ # reinsert stripped tags
+ $rv = trim( $stripState->unstripBoth( $rv ) );
+
+ return $rv;
+ }
+
+ /**
+ * This function returns the text of a section, specified by a number ($section).
+ * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
+ * the first section before any such heading (section 0).
+ *
+ * If a section contains subsections, these are also returned.
+ *
+ * @param $text String: text to look in
+ * @param $section Integer: section number
+ * @param $deftext: default to return if section is not found
+ * @return string text of the requested section
+ */
+ public function getSection( $text, $section, $deftext='' ) {
+ return $this->extractSections( $text, $section, "get", $deftext );
+ }
+
+ public function replaceSection( $oldtext, $section, $text ) {
+ return $this->extractSections( $oldtext, $section, "replace", $text );
+ }
+
+ /**
+ * Get the timestamp associated with the current revision, adjusted for
+ * the default server-local timestamp
+ */
+ function getRevisionTimestamp() {
+ if ( is_null( $this->mRevisionTimestamp ) ) {
+ wfProfileIn( __METHOD__ );
+ global $wgContLang;
+ $dbr = wfGetDB( DB_SLAVE );
+ $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
+
+ // Normalize timestamp to internal MW format for timezone processing.
+ // This has the added side-effect of replacing a null value with
+ // the current time, which gives us more sensible behavior for
+ // previews.
+ $timestamp = wfTimestamp( TS_MW, $timestamp );
+
+ // The cryptic '' timezone parameter tells to use the site-default
+ // timezone offset instead of the user settings.
+ //
+ // Since this value will be saved into the parser cache, served
+ // to other users, and potentially even used inside links and such,
+ // it needs to be consistent for all visitors.
+ $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
+
+ wfProfileOut( __METHOD__ );
+ }
+ return $this->mRevisionTimestamp;
+ }
+
+ /**
+ * Mutator for $mDefaultSort
+ *
+ * @param $sort New value
+ */
+ public function setDefaultSort( $sort ) {
+ $this->mDefaultSort = $sort;
+ }
+
+ /**
+ * Accessor for $mDefaultSort
+ * Will use the title/prefixed title if none is set
+ *
+ * @return string
+ */
+ public function getDefaultSort() {
+ if( $this->mDefaultSort !== false ) {
+ return $this->mDefaultSort;
+ } else {
+ return $this->mTitle->getNamespace() == NS_CATEGORY
+ ? $this->mTitle->getText()
+ : $this->mTitle->getPrefixedText();
+ }
+ }
+
+ /**
+ * Try to guess the section anchor name based on a wikitext fragment
+ * presumably extracted from a heading, for example "Header" from
+ * "== Header ==".
+ */
+ public function guessSectionNameFromWikiText( $text ) {
+ # Strip out wikitext links(they break the anchor)
+ $text = $this->stripSectionName( $text );
+ $headline = Sanitizer::decodeCharReferences( $text );
+ # strip out HTML
+ $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
+ $headline = trim( $headline );
+ $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
+ $replacearray = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+ return str_replace(
+ array_keys( $replacearray ),
+ array_values( $replacearray ),
+ $sectionanchor );
+ }
+
+ /**
+ * Strips a text string of wikitext for use in a section anchor
+ *
+ * Accepts a text string and then removes all wikitext from the
+ * string and leaves only the resultant text (i.e. the result of
+ * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
+ * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
+ * to create valid section anchors by mimicing the output of the
+ * parser when headings are parsed.
+ *
+ * @param $text string Text string to be stripped of wikitext
+ * for use in a Section anchor
+ * @return Filtered text string
+ */
+ public function stripSectionName( $text ) {
+ # Strip internal link markup
+ $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
+ $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
+
+ # Strip external link markup (FIXME: Not Tolerant to blank link text
+ # I.E. [http://www.mediawiki.org] will render as [1] or something depending
+ # on how many empty links there are on the page - need to figure that out.
+ $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
+
+ # Parse wikitext quotes (italics & bold)
+ $text = $this->doQuotes($text);
+
+ # Strip HTML tags
+ $text = StringUtils::delimiterReplace( '<', '>', '', $text );
+ return $text;
+ }
+
+ /**
+ * strip/replaceVariables/unstrip for preprocessor regression testing
+ */
+ function srvus( $text ) {
+ $text = $this->strip( $text, $this->mStripState );
+ $text = Sanitizer::removeHTMLtags( $text );
+ $text = $this->replaceVariables( $text );
+ $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text );
+ $text = $this->mStripState->unstripBoth( $text );
+ return $text;
+ }
+}
+
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
new file mode 100644
index 00000000..bddfb9f1
--- /dev/null
+++ b/includes/PrefixSearch.php
@@ -0,0 +1,135 @@
+<?php
+
+class PrefixSearch {
+ /**
+ * Do a prefix search of titles and return a list of matching page names.
+ * @param string $search
+ * @param int $limit
+ * @return array of strings
+ */
+ public static function titleSearch( $search, $limit ) {
+ $search = trim( $search );
+ if( $search == '' ) {
+ return array(); // Return empty result
+ }
+
+ $title = Title::newFromText( $search );
+ if( $title && $title->getInterwiki() == '' ) {
+ $ns = $title->getNamespace();
+ return self::searchBackend(
+ $title->getNamespace(), $title->getText(), $limit );
+ }
+
+ // Is this a namespace prefix?
+ $title = Title::newFromText( $search . 'Dummy' );
+ if( $title && $title->getText() == 'Dummy'
+ && $title->getNamespace() != NS_MAIN
+ && $title->getInterwiki() == '' ) {
+ return self::searchBackend(
+ $title->getNamespace(), '', $limit );
+ }
+
+ return self::searchBackend( 0, $search, $limit );
+ }
+
+
+ /**
+ * Do a prefix search of titles and return a list of matching page names.
+ * @param string $search
+ * @param int $limit
+ * @return array of strings
+ */
+ protected static function searchBackend( $ns, $search, $limit ) {
+ if( $ns == NS_MEDIA ) {
+ $ns = NS_IMAGE;
+ } elseif( $ns == NS_SPECIAL ) {
+ return self::specialSearch( $search, $limit );
+ }
+
+ $srchres = array();
+ if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) {
+ return self::defaultSearchBackend( $ns, $search, $limit );
+ }
+ return $srchres;
+ }
+
+ /**
+ * Prefix search special-case for Special: namespace.
+ */
+ protected static function specialSearch( $search, $limit ) {
+ global $wgContLang;
+ $searchKey = $wgContLang->caseFold( $search );
+
+ // Unlike SpecialPage itself, we want the canonical forms of both
+ // canonical and alias title forms...
+ SpecialPage::initList();
+ SpecialPage::initAliasList();
+ $keys = array();
+ foreach( array_keys( SpecialPage::$mList ) as $page ) {
+ $keys[$wgContLang->caseFold( $page )] = $page;
+ }
+ foreach( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
+ foreach( $aliases as $alias ) {
+ $keys[$wgContLang->caseFold( $alias )] = $alias;
+ }
+ }
+ ksort( $keys );
+
+ $srchres = array();
+ foreach( $keys as $pageKey => $page ) {
+ if( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
+ $srchres[] = Title::makeTitle( NS_SPECIAL, $page )->getPrefixedText();
+ }
+ if( count( $srchres ) >= $limit ) {
+ break;
+ }
+ }
+ return $srchres;
+ }
+
+ /**
+ * Unless overridden by PrefixSearchBackend hook...
+ * This is case-sensitive except the first letter (per $wgCapitalLinks)
+ *
+ * @param int $ns Namespace to search in
+ * @param string $search term
+ * @param int $limit max number of items to return
+ * @return array of title strings
+ */
+ protected static function defaultSearchBackend( $ns, $search, $limit ) {
+ global $wgCapitalLinks, $wgContLang;
+
+ if( $wgCapitalLinks ) {
+ $search = $wgContLang->ucfirst( $search );
+ }
+
+ // Prepare nested request
+ $req = new FauxRequest(array (
+ 'action' => 'query',
+ 'list' => 'allpages',
+ 'apnamespace' => $ns,
+ 'aplimit' => $limit,
+ 'apprefix' => $search
+ ));
+
+ // Execute
+ $module = new ApiMain($req);
+ $module->execute();
+
+ // Get resulting data
+ $data = $module->getResultData();
+
+ // Reformat useful data for future printing by JSON engine
+ $srchres = array ();
+ foreach ($data['query']['allpages'] as & $pageinfo) {
+ // Note: this data will no be printable by the xml engine
+ // because it does not support lists of unnamed items
+ $srchres[] = $pageinfo['title'];
+ }
+
+ return $srchres;
+ }
+
+}
+
+?> \ No newline at end of file
diff --git a/includes/Preprocessor.php b/includes/Preprocessor.php
new file mode 100644
index 00000000..34bc1e5b
--- /dev/null
+++ b/includes/Preprocessor.php
@@ -0,0 +1,154 @@
+<?php
+
+interface Preprocessor {
+ /** Create a new preprocessor object based on an initialised Parser object */
+ function __construct( $parser );
+
+ /** Create a new top-level frame for expansion of a page */
+ function newFrame();
+
+ /** Preprocess text to a PPNode */
+ function preprocessToObj( $text, $flags = 0 );
+}
+
+interface PPFrame {
+ const NO_ARGS = 1;
+ const NO_TEMPLATES = 2;
+ const STRIP_COMMENTS = 4;
+ const NO_IGNORE = 8;
+ const RECOVER_COMMENTS = 16;
+
+ const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet
+
+ /**
+ * Create a child frame
+ */
+ function newChild( $args = false, $title = false );
+
+ /**
+ * Expand a document tree node
+ */
+ function expand( $root, $flags = 0 );
+
+ /**
+ * Implode with flags for expand()
+ */
+ function implodeWithFlags( $sep, $flags /*, ... */ );
+
+ /**
+ * Implode with no flags specified
+ */
+ function implode( $sep /*, ... */ );
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ );
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ );
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty();
+
+ /**
+ * Get an argument to this frame by name
+ */
+ function getArgument( $name );
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title );
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate();
+}
+
+/**
+ * There are three types of nodes:
+ * * Tree nodes, which have a name and contain other nodes as children
+ * * Array nodes, which also contain other nodes but aren't considered part of a tree
+ * * Leaf nodes, which contain the actual data
+ *
+ * This interface provides access to the tree structure and to the contents of array nodes,
+ * but it does not provide access to the internal structure of leaf nodes. Access to leaf
+ * data is provided via two means:
+ * * PPFrame::expand(), which provides expanded text
+ * * The PPNode::split*() functions, which provide metadata about certain types of tree node
+ */
+interface PPNode {
+ /**
+ * Get an array-type node containing the children of this node.
+ * Returns false if this is not a tree node.
+ */
+ function getChildren();
+
+ /**
+ * Get the first child of a tree node. False if there isn't one.
+ */
+ function getFirstChild();
+
+ /**
+ * Get the next sibling of any node. False if there isn't one
+ */
+ function getNextSibling();
+
+ /**
+ * Get all children of this tree node which have a given name.
+ * Returns an array-type node, or false if this is not a tree node.
+ */
+ function getChildrenOfType( $type );
+
+
+ /**
+ * Returns the length of the array, or false if this is not an array-type node
+ */
+ function getLength();
+
+ /**
+ * Returns an item of an array-type node
+ */
+ function item( $i );
+
+ /**
+ * Get the name of this node. The following names are defined here:
+ *
+ * h A heading node.
+ * template A double-brace node.
+ * tplarg A triple-brace node.
+ * title The first argument to a template or tplarg node.
+ * part Subsequent arguments to a template or tplarg node.
+ * #nodelist An array-type node
+ *
+ * The subclass may define various other names for tree and leaf nodes.
+ */
+ function getName();
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg();
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt();
+
+ /**
+ * Split an <h> node
+ */
+ function splitHeading();
+}
+
diff --git a/includes/Preprocessor_DOM.php b/includes/Preprocessor_DOM.php
new file mode 100644
index 00000000..0e2e9a16
--- /dev/null
+++ b/includes/Preprocessor_DOM.php
@@ -0,0 +1,1356 @@
+<?php
+
+class Preprocessor_DOM implements Preprocessor {
+ var $parser, $memoryLimit;
+
+ function __construct( $parser ) {
+ $this->parser = $parser;
+ $mem = ini_get( 'memory_limit' );
+ $this->memoryLimit = false;
+ if ( strval( $mem ) !== '' && $mem != -1 ) {
+ if ( preg_match( '/^\d+$/', $mem ) ) {
+ $this->memoryLimit = $mem;
+ } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
+ $this->memoryLimit = $m[1] * 1048576;
+ }
+ }
+ }
+
+ function newFrame() {
+ return new PPFrame_DOM( $this );
+ }
+
+ function memCheck() {
+ if ( $this->memoryLimit === false ) {
+ return;
+ }
+ $usage = memory_get_usage();
+ if ( $usage > $this->memoryLimit * 0.9 ) {
+ $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
+ throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
+ }
+ return $usage <= $this->memoryLimit * 0.8;
+ }
+
+ /**
+ * Preprocess some wikitext and return the document tree.
+ * This is the ghost of Parser::replace_variables().
+ *
+ * @param string $text The text to parse
+ * @param integer flags Bitwise combination of:
+ * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * included. Default is to assume a direct page view.
+ *
+ * The generated DOM tree must depend only on the input text and the flags.
+ * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+ *
+ * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+ * change in the DOM tree for a given text, must be passed through the section identifier
+ * in the section edit link and thus back to extractSections().
+ *
+ * The output of this function is currently only cached in process memory, but a persistent
+ * cache may be implemented at a later date which takes further advantage of these strict
+ * dependency requirements.
+ *
+ * @private
+ */
+ function preprocessToObj( $text, $flags = 0 ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__.'-makexml' );
+
+ $rules = array(
+ '{' => array(
+ 'end' => '}',
+ 'names' => array(
+ 2 => 'template',
+ 3 => 'tplarg',
+ ),
+ 'min' => 2,
+ 'max' => 3,
+ ),
+ '[' => array(
+ 'end' => ']',
+ 'names' => array( 2 => null ),
+ 'min' => 2,
+ 'max' => 2,
+ )
+ );
+
+ $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+ $xmlishElements = $this->parser->getStripList();
+ $enableOnlyinclude = false;
+ if ( $forInclusion ) {
+ $ignoredTags = array( 'includeonly', '/includeonly' );
+ $ignoredElements = array( 'noinclude' );
+ $xmlishElements[] = 'noinclude';
+ if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+ $enableOnlyinclude = true;
+ }
+ } else {
+ $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+ $ignoredElements = array( 'includeonly' );
+ $xmlishElements[] = 'includeonly';
+ }
+ $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+ // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+ $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+ $stack = new PPDStack;
+
+ $searchBase = "[{<\n"; #}
+ $revText = strrev( $text ); // For fast reverse searches
+
+ $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
+ $accum =& $stack->getAccum(); # Current accumulator
+ $accum = '<root>';
+ $findEquals = false; # True to find equals signs in arguments
+ $findPipe = false; # True to take notice of pipe characters
+ $headingIndex = 1;
+ $inHeading = false; # True if $i is inside a possible heading
+ $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
+ $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+ $fakeLineStart = true; # Do a line-start run without outputting an LF character
+
+ while ( true ) {
+ //$this->memCheck();
+
+ if ( $findOnlyinclude ) {
+ // Ignore all input up to the next <onlyinclude>
+ $startPos = strpos( $text, '<onlyinclude>', $i );
+ if ( $startPos === false ) {
+ // Ignored section runs to the end
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
+ break;
+ }
+ $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
+ $i = $tagEndPos;
+ $findOnlyinclude = false;
+ }
+
+ if ( $fakeLineStart ) {
+ $found = 'line-start';
+ $curChar = '';
+ } else {
+ # Find next opening brace, closing brace or pipe
+ $search = $searchBase;
+ if ( $stack->top === false ) {
+ $currentClosing = '';
+ } else {
+ $currentClosing = $stack->top->close;
+ $search .= $currentClosing;
+ }
+ if ( $findPipe ) {
+ $search .= '|';
+ }
+ if ( $findEquals ) {
+ // First equals will be for the template
+ $search .= '=';
+ }
+ $rule = null;
+ # Output literal section, advance input counter
+ $literalLength = strcspn( $text, $search, $i );
+ if ( $literalLength > 0 ) {
+ $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
+ $i += $literalLength;
+ }
+ if ( $i >= strlen( $text ) ) {
+ if ( $currentClosing == "\n" ) {
+ // Do a past-the-end run to finish off the heading
+ $curChar = '';
+ $found = 'line-end';
+ } else {
+ # All done
+ break;
+ }
+ } else {
+ $curChar = $text[$i];
+ if ( $curChar == '|' ) {
+ $found = 'pipe';
+ } elseif ( $curChar == '=' ) {
+ $found = 'equals';
+ } elseif ( $curChar == '<' ) {
+ $found = 'angle';
+ } elseif ( $curChar == "\n" ) {
+ if ( $inHeading ) {
+ $found = 'line-end';
+ } else {
+ $found = 'line-start';
+ }
+ } elseif ( $curChar == $currentClosing ) {
+ $found = 'close';
+ } elseif ( isset( $rules[$curChar] ) ) {
+ $found = 'open';
+ $rule = $rules[$curChar];
+ } else {
+ # Some versions of PHP have a strcspn which stops on null characters
+ # Ignore and continue
+ ++$i;
+ continue;
+ }
+ }
+ }
+
+ if ( $found == 'angle' ) {
+ $matches = false;
+ // Handle </onlyinclude>
+ if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+ $findOnlyinclude = true;
+ continue;
+ }
+
+ // Determine element name
+ if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+ // Element name missing or not listed
+ $accum .= '&lt;';
+ ++$i;
+ continue;
+ }
+ // Handle comments
+ if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+ // To avoid leaving blank lines, when a comment is both preceded
+ // and followed by a newline (ignoring spaces), trim leading and
+ // trailing spaces and one of the newlines.
+
+ // Find the end
+ $endPos = strpos( $text, '-->', $i + 4 );
+ if ( $endPos === false ) {
+ // Unclosed comment in input, runs to end
+ $inner = substr( $text, $i );
+ $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+ $i = strlen( $text );
+ } else {
+ // Search backwards for leading whitespace
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ // Search forwards for trailing whitespace
+ // $wsEnd will be the position of the last space
+ $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ // Eat the line if possible
+ // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+ // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+ // it's a possible beneficial b/c break.
+ if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+ && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+ {
+ $startPos = $wsStart;
+ $endPos = $wsEnd + 1;
+ // Remove leading whitespace from the end of the accumulator
+ // Sanity check first though
+ $wsLength = $i - $wsStart;
+ if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) {
+ $accum = substr( $accum, 0, -$wsLength );
+ }
+ // Do a line-start run next time to look for headings after the comment
+ $fakeLineStart = true;
+ } else {
+ // No line to eat, just take the comment itself
+ $startPos = $i;
+ $endPos += 2;
+ }
+
+ if ( $stack->top ) {
+ $part = $stack->top->getCurrentPart();
+ if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+ // Comments abutting, no change in visual end
+ $part->commentEnd = $wsEnd;
+ } else {
+ $part->visualEnd = $wsStart;
+ $part->commentEnd = $endPos;
+ }
+ }
+ $i = $endPos + 1;
+ $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+ $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+ }
+ continue;
+ }
+ $name = $matches[1];
+ $attrStart = $i + strlen( $name ) + 1;
+
+ // Find end of tag
+ $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+ if ( $tagEndPos === false ) {
+ // Infinite backtrack
+ // Disable tag search to prevent worst-case O(N^2) performance
+ $noMoreGT = true;
+ $accum .= '&lt;';
+ ++$i;
+ continue;
+ }
+
+ // Handle ignored tags
+ if ( in_array( $name, $ignoredTags ) ) {
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>';
+ $i = $tagEndPos + 1;
+ continue;
+ }
+
+ $tagStartPos = $i;
+ if ( $text[$tagEndPos-1] == '/' ) {
+ $attrEnd = $tagEndPos - 1;
+ $inner = null;
+ $i = $tagEndPos + 1;
+ $close = '';
+ } else {
+ $attrEnd = $tagEndPos;
+ // Find closing tag
+ if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+ $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+ $i = $matches[0][1] + strlen( $matches[0][0] );
+ $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
+ } else {
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = strlen( $text );
+ $close = '';
+ }
+ }
+ // <includeonly> and <noinclude> just become <ignore> tags
+ if ( in_array( $name, $ignoredElements ) ) {
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
+ . '</ignore>';
+ continue;
+ }
+
+ $accum .= '<ext>';
+ if ( $attrEnd <= $attrStart ) {
+ $attr = '';
+ } else {
+ $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+ }
+ $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
+ // Note that the attr element contains the whitespace between name and attribute,
+ // this is necessary for precise reconstruction during pre-save transform.
+ '<attr>' . htmlspecialchars( $attr ) . '</attr>';
+ if ( $inner !== null ) {
+ $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
+ }
+ $accum .= $close . '</ext>';
+ }
+
+ elseif ( $found == 'line-start' ) {
+ // Is this the start of a heading?
+ // Line break belongs before the heading element in any case
+ if ( $fakeLineStart ) {
+ $fakeLineStart = false;
+ } else {
+ $accum .= $curChar;
+ $i++;
+ }
+
+ $count = strspn( $text, '=', $i, 6 );
+ if ( $count == 1 && $findEquals ) {
+ // DWIM: This looks kind of like a name/value separator
+ // Let's let the equals handler have it and break the potential heading
+ // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+ } elseif ( $count > 0 ) {
+ $piece = array(
+ 'open' => "\n",
+ 'close' => "\n",
+ 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ),
+ 'startPos' => $i,
+ 'count' => $count );
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ $i += $count;
+ }
+ }
+
+ elseif ( $found == 'line-end' ) {
+ $piece = $stack->top;
+ // A heading must be open, otherwise \n wouldn't have been in the search list
+ assert( $piece->open == "\n" );
+ $part = $piece->getCurrentPart();
+ // Search back through the input to see if it has a proper close
+ // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+ $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $searchStart = $i - $wsLength;
+ if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+ // Comment found at line end
+ // Search for equals signs before the comment
+ $searchStart = $part->visualEnd;
+ $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ }
+ $count = $piece->count;
+ $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ if ( $equalsLength > 0 ) {
+ if ( $i - $equalsLength == $piece->startPos ) {
+ // This is just a single string of equals signs on its own line
+ // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // First find out how many equals signs there really are (don't stop at 6)
+ $count = $equalsLength;
+ if ( $count < 3 ) {
+ $count = 0;
+ } else {
+ $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+ }
+ } else {
+ $count = min( $equalsLength, $count );
+ }
+ if ( $count > 0 ) {
+ // Normal match, output <h>
+ $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
+ $headingIndex++;
+ } else {
+ // Single equals sign on its own line, count=0
+ $element = $accum;
+ }
+ } else {
+ // No match, no <h>, just pass down the inner text
+ $element = $accum;
+ }
+ // Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+
+ // Append the result to the enclosing accumulator
+ $accum .= $element;
+ // Note that we do NOT increment the input pointer.
+ // This is because the closing linebreak could be the opening linebreak of
+ // another heading. Infinite loops are avoided because the next iteration MUST
+ // hit the heading open case above, which unconditionally increments the
+ // input pointer.
+ }
+
+ elseif ( $found == 'open' ) {
+ # count opening brace characters
+ $count = strspn( $text, $curChar, $i );
+
+ # we need to add to stack only if opening brace count is enough for one of the rules
+ if ( $count >= $rule['min'] ) {
+ # Add it to the stack
+ $piece = array(
+ 'open' => $curChar,
+ 'close' => $rule['end'],
+ 'count' => $count,
+ 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ );
+
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ } else {
+ # Add literal brace(s)
+ $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+ }
+ $i += $count;
+ }
+
+ elseif ( $found == 'close' ) {
+ $piece = $stack->top;
+ # lets check if there are enough characters for closing brace
+ $maxCount = $piece->count;
+ $count = strspn( $text, $curChar, $i, $maxCount );
+
+ # check for maximum matching characters (if there are 5 closing
+ # characters, we will probably need only 3 - depending on the rules)
+ $matchingCount = 0;
+ $rule = $rules[$piece->open];
+ if ( $count > $rule['max'] ) {
+ # The specified maximum exists in the callback array, unless the caller
+ # has made an error
+ $matchingCount = $rule['max'];
+ } else {
+ # Count is less than the maximum
+ # Skip any gaps in the callback array to find the true largest match
+ # Need to use array_key_exists not isset because the callback can be null
+ $matchingCount = $count;
+ while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
+ --$matchingCount;
+ }
+ }
+
+ if ($matchingCount <= 0) {
+ # No matching element found in callback array
+ # Output a literal closing brace and continue
+ $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+ $i += $count;
+ continue;
+ }
+ $name = $rule['names'][$matchingCount];
+ if ( $name === null ) {
+ // No element, just literal text
+ $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
+ } else {
+ # Create XML element
+ # Note: $parts is already XML, does not need to be encoded further
+ $parts = $piece->parts;
+ $title = $parts[0]->out;
+ unset( $parts[0] );
+
+ # The invocation is at the start of the line if lineStart is set in
+ # the stack, and all opening brackets are used up.
+ if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ $attr = ' lineStart="1"';
+ } else {
+ $attr = '';
+ }
+
+ $element = "<$name$attr>";
+ $element .= "<title>$title</title>";
+ $argIndex = 1;
+ foreach ( $parts as $partIndex => $part ) {
+ if ( isset( $part->eqpos ) ) {
+ $argName = substr( $part->out, 0, $part->eqpos );
+ $argValue = substr( $part->out, $part->eqpos + 1 );
+ $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
+ } else {
+ $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
+ $argIndex++;
+ }
+ }
+ $element .= "</$name>";
+ }
+
+ # Advance input pointer
+ $i += $matchingCount;
+
+ # Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+
+ # Re-add the old stack element if it still has unmatched opening characters remaining
+ if ($matchingCount < $piece->count) {
+ $piece->parts = array( new PPDPart );
+ $piece->count -= $matchingCount;
+ # do we still qualify for any callback with remaining count?
+ $names = $rules[$piece->open]['names'];
+ $skippedBraces = 0;
+ $enclosingAccum =& $accum;
+ while ( $piece->count ) {
+ if ( array_key_exists( $piece->count, $names ) ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ break;
+ }
+ --$piece->count;
+ $skippedBraces ++;
+ }
+ $enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
+ }
+
+ extract( $stack->getFlags() );
+
+ # Add XML element to the enclosing accumulator
+ $accum .= $element;
+ }
+
+ elseif ( $found == 'pipe' ) {
+ $findEquals = true; // shortcut for getFlags()
+ $stack->addPart();
+ $accum =& $stack->getAccum();
+ ++$i;
+ }
+
+ elseif ( $found == 'equals' ) {
+ $findEquals = false; // shortcut for getFlags()
+ $stack->getCurrentPart()->eqpos = strlen( $accum );
+ $accum .= '=';
+ ++$i;
+ }
+ }
+
+ # Output any remaining unclosed brackets
+ foreach ( $stack->stack as $piece ) {
+ $stack->rootAccum .= $piece->breakSyntax();
+ }
+ $stack->rootAccum .= '</root>';
+ $xml = $stack->rootAccum;
+
+ wfProfileOut( __METHOD__.'-makexml' );
+ wfProfileIn( __METHOD__.'-loadXML' );
+ $dom = new DOMDocument;
+ wfSuppressWarnings();
+ $result = $dom->loadXML( $xml );
+ wfRestoreWarnings();
+ if ( !$result ) {
+ // Try running the XML through UtfNormal to get rid of invalid characters
+ $xml = UtfNormal::cleanUp( $xml );
+ $result = $dom->loadXML( $xml );
+ if ( !$result ) {
+ throw new MWException( __METHOD__.' generated invalid XML' );
+ }
+ }
+ $obj = new PPNode_DOM( $dom->documentElement );
+ wfProfileOut( __METHOD__.'-loadXML' );
+ wfProfileOut( __METHOD__ );
+ return $obj;
+ }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ */
+class PPDStack {
+ var $stack, $rootAccum, $top;
+ var $out;
+ var $elementClass = 'PPDStackElement';
+
+ static $false = false;
+
+ function __construct() {
+ $this->stack = array();
+ $this->top = false;
+ $this->rootAccum = '';
+ $this->accum =& $this->rootAccum;
+ }
+
+ function count() {
+ return count( $this->stack );
+ }
+
+ function &getAccum() {
+ return $this->accum;
+ }
+
+ function getCurrentPart() {
+ if ( $this->top === false ) {
+ return false;
+ } else {
+ return $this->top->getCurrentPart();
+ }
+ }
+
+ function push( $data ) {
+ if ( $data instanceof $this->elementClass ) {
+ $this->stack[] = $data;
+ } else {
+ $class = $this->elementClass;
+ $this->stack[] = new $class( $data );
+ }
+ $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->accum =& $this->top->getAccum();
+ }
+
+ function pop() {
+ if ( !count( $this->stack ) ) {
+ throw new MWException( __METHOD__.': no elements remaining' );
+ }
+ $temp = array_pop( $this->stack );
+
+ if ( count( $this->stack ) ) {
+ $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->accum =& $this->top->getAccum();
+ } else {
+ $this->top = self::$false;
+ $this->accum =& $this->rootAccum;
+ }
+ return $temp;
+ }
+
+ function addPart( $s = '' ) {
+ $this->top->addPart( $s );
+ $this->accum =& $this->top->getAccum();
+ }
+
+ function getFlags() {
+ if ( !count( $this->stack ) ) {
+ return array(
+ 'findEquals' => false,
+ 'findPipe' => false,
+ 'inHeading' => false,
+ );
+ } else {
+ return $this->top->getFlags();
+ }
+ }
+}
+
+class PPDStackElement {
+ var $open, // Opening character (\n for heading)
+ $close, // Matching closing character
+ $count, // Number of opening characters found (number of "=" for heading)
+ $parts, // Array of PPDPart objects describing pipe-separated parts.
+ $lineStart; // True if the open char appeared at the start of the input line. Not set for headings.
+
+ var $partClass = 'PPDPart';
+
+ function __construct( $data = array() ) {
+ $class = $this->partClass;
+ $this->parts = array( new $class );
+
+ foreach ( $data as $name => $value ) {
+ $this->$name = $value;
+ }
+ }
+
+ function &getAccum() {
+ return $this->parts[count($this->parts) - 1]->out;
+ }
+
+ function addPart( $s = '' ) {
+ $class = $this->partClass;
+ $this->parts[] = new $class( $s );
+ }
+
+ function getCurrentPart() {
+ return $this->parts[count($this->parts) - 1];
+ }
+
+ function getFlags() {
+ $partCount = count( $this->parts );
+ $findPipe = $this->open != "\n" && $this->open != '[';
+ return array(
+ 'findPipe' => $findPipe,
+ 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
+ 'inHeading' => $this->open == "\n",
+ );
+ }
+
+ /**
+ * Get the output string that would result if the close is not found.
+ */
+ function breakSyntax( $openingCount = false ) {
+ if ( $this->open == "\n" ) {
+ $s = $this->parts[0]->out;
+ } else {
+ if ( $openingCount === false ) {
+ $openingCount = $this->count;
+ }
+ $s = str_repeat( $this->open, $openingCount );
+ $first = true;
+ foreach ( $this->parts as $part ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= '|';
+ }
+ $s .= $part->out;
+ }
+ }
+ return $s;
+ }
+}
+
+class PPDPart {
+ var $out; // Output accumulator string
+
+ // Optional member variables:
+ // eqpos Position of equals sign in output accumulator
+ // commentEnd Past-the-end input pointer for the last comment encountered
+ // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
+
+ function __construct( $out = '' ) {
+ $this->out = $out;
+ }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ */
+class PPFrame_DOM implements PPFrame {
+ var $preprocessor, $parser, $title;
+ var $titleCache;
+
+ /**
+ * Hashtable listing templates which are disallowed for expansion in this frame,
+ * having been encountered previously in parent frames.
+ */
+ var $loopCheckHash;
+
+ /**
+ * Recursion depth of this frame, top = 0
+ */
+ var $depth;
+
+
+ /**
+ * Construct a new preprocessor frame.
+ * @param Preprocessor $preprocessor The parent preprocessor
+ */
+ function __construct( $preprocessor ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->title = $this->parser->mTitle;
+ $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+ $this->loopCheckHash = array();
+ $this->depth = 0;
+ }
+
+ /**
+ * Create a new child frame
+ * $args is optionally a multi-root PPNode or array containing the template arguments
+ */
+ function newChild( $args = false, $title = false ) {
+ $namedArgs = array();
+ $numberedArgs = array();
+ if ( $title === false ) {
+ $title = $this->title;
+ }
+ if ( $args !== false ) {
+ $xpath = false;
+ if ( $args instanceof PPNode ) {
+ $args = $args->node;
+ }
+ foreach ( $args as $arg ) {
+ if ( !$xpath ) {
+ $xpath = new DOMXPath( $arg->ownerDocument );
+ }
+
+ $nameNodes = $xpath->query( 'name', $arg );
+ $value = $xpath->query( 'value', $arg );
+ if ( $nameNodes->item( 0 )->hasAttributes() ) {
+ // Numbered parameter
+ $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
+ $numberedArgs[$index] = $value->item( 0 );
+ unset( $namedArgs[$index] );
+ } else {
+ // Named parameter
+ $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
+ $namedArgs[$name] = $value->item( 0 );
+ unset( $numberedArgs[$name] );
+ }
+ }
+ }
+ return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+ }
+
+ function expand( $root, $flags = 0 ) {
+ if ( is_string( $root ) ) {
+ return $root;
+ }
+
+ if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+ {
+ return '<span class="error">Node-count limit exceeded</span>';
+ }
+
+ if ( $root instanceof PPNode_DOM ) {
+ $root = $root->node;
+ }
+ if ( $root instanceof DOMDocument ) {
+ $root = $root->documentElement;
+ }
+
+ $outStack = array( '', '' );
+ $iteratorStack = array( false, $root );
+ $indexStack = array( 0, 0 );
+
+ while ( count( $iteratorStack ) > 1 ) {
+ $level = count( $outStack ) - 1;
+ $iteratorNode =& $iteratorStack[ $level ];
+ $out =& $outStack[$level];
+ $index =& $indexStack[$level];
+
+ if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node;
+
+ if ( is_array( $iteratorNode ) ) {
+ if ( $index >= count( $iteratorNode ) ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode[$index];
+ $index++;
+ }
+ } elseif ( $iteratorNode instanceof DOMNodeList ) {
+ if ( $index >= $iteratorNode->length ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode->item( $index );
+ $index++;
+ }
+ } else {
+ // Copy to $contextNode and then delete from iterator stack,
+ // because this is not an iterator but we do have to execute it once
+ $contextNode = $iteratorStack[$level];
+ $iteratorStack[$level] = false;
+ }
+
+ if ( $contextNode instanceof PPNode_DOM ) $contextNode = $contextNode->node;
+
+ $newIterator = false;
+
+ if ( $contextNode === false ) {
+ // nothing to do
+ } elseif ( is_string( $contextNode ) ) {
+ $out .= $contextNode;
+ } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
+ $newIterator = $contextNode;
+ } elseif ( $contextNode instanceof DOMNode ) {
+ if ( $contextNode->nodeType == XML_TEXT_NODE ) {
+ $out .= $contextNode->nodeValue;
+ } elseif ( $contextNode->nodeName == 'template' ) {
+ # Double-brace expansion
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $titles = $xpath->query( 'title', $contextNode );
+ $title = $titles->item( 0 );
+ $parts = $xpath->query( 'part', $contextNode );
+ if ( $flags & self::NO_TEMPLATES ) {
+ $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
+ } else {
+ $lineStart = $contextNode->getAttribute( 'lineStart' );
+ $params = array(
+ 'title' => new PPNode_DOM( $title ),
+ 'parts' => new PPNode_DOM( $parts ),
+ 'lineStart' => $lineStart );
+ $ret = $this->parser->braceSubstitution( $params, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->nodeName == 'tplarg' ) {
+ # Triple-brace expansion
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $titles = $xpath->query( 'title', $contextNode );
+ $title = $titles->item( 0 );
+ $parts = $xpath->query( 'part', $contextNode );
+ if ( $flags & self::NO_ARGS ) {
+ $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
+ } else {
+ $params = array(
+ 'title' => new PPNode_DOM( $title ),
+ 'parts' => new PPNode_DOM( $parts ) );
+ $ret = $this->parser->argSubstitution( $params, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->nodeName == 'comment' ) {
+ # HTML-style comment
+ # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+ if ( $this->parser->ot['html']
+ || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+ || ( $flags & self::STRIP_COMMENTS ) )
+ {
+ $out .= '';
+ }
+ # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+ # Not in RECOVER_COMMENTS mode (extractSections) though
+ elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+ $out .= $this->parser->insertStripItem( $contextNode->textContent );
+ }
+ # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+ else {
+ $out .= $contextNode->textContent;
+ }
+ } elseif ( $contextNode->nodeName == 'ignore' ) {
+ # Output suppression used by <includeonly> etc.
+ # OT_WIKI will only respect <ignore> in substed templates.
+ # The other output types respect it unless NO_IGNORE is set.
+ # extractSections() sets NO_IGNORE and so never respects it.
+ if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+ $out .= $contextNode->textContent;
+ } else {
+ $out .= '';
+ }
+ } elseif ( $contextNode->nodeName == 'ext' ) {
+ # Extension tag
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $names = $xpath->query( 'name', $contextNode );
+ $attrs = $xpath->query( 'attr', $contextNode );
+ $inners = $xpath->query( 'inner', $contextNode );
+ $closes = $xpath->query( 'close', $contextNode );
+ $params = array(
+ 'name' => new PPNode_DOM( $names->item( 0 ) ),
+ 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
+ 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
+ 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
+ );
+ $out .= $this->parser->extensionSubstitution( $params, $this );
+ } elseif ( $contextNode->nodeName == 'h' ) {
+ # Heading
+ $s = $this->expand( $contextNode->childNodes, $flags );
+
+ # Insert a heading marker only for <h> children of <root>
+ # This is to stop extractSections from going over multiple tree levels
+ if ( $contextNode->parentNode->nodeName == 'root'
+ && $this->parser->ot['html'] )
+ {
+ # Insert heading index marker
+ $headingIndex = $contextNode->getAttribute( 'i' );
+ $titleText = $this->title->getPrefixedDBkey();
+ $this->parser->mHeadings[] = array( $titleText, $headingIndex );
+ $serial = count( $this->parser->mHeadings ) - 1;
+ $marker = "{$this->parser->mUniqPrefix}-h-$serial-{$this->parser->mMarkerSuffix}";
+ $count = $contextNode->getAttribute( 'level' );
+ $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
+ $this->parser->mStripState->general->setPair( $marker, '' );
+ }
+ $out .= $s;
+ } else {
+ # Generic recursive expansion
+ $newIterator = $contextNode->childNodes;
+ }
+ } else {
+ throw new MWException( __METHOD__.': Invalid parameter type' );
+ }
+
+ if ( $newIterator !== false ) {
+ if ( $newIterator instanceof PPNode_DOM ) {
+ $newIterator = $newIterator->node;
+ }
+ $outStack[] = '';
+ $iteratorStack[] = $newIterator;
+ $indexStack[] = 0;
+ } elseif ( $iteratorStack[$level] === false ) {
+ // Return accumulated value to parent
+ // With tail recursion
+ while ( $iteratorStack[$level] === false && $level > 0 ) {
+ $outStack[$level - 1] .= $out;
+ array_pop( $outStack );
+ array_pop( $iteratorStack );
+ array_pop( $indexStack );
+ $level--;
+ }
+ }
+ }
+ return $outStack[0];
+ }
+
+ function implodeWithFlags( $sep, $flags /*, ... */ ) {
+ $args = array_slice( func_get_args(), 2 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node, $flags );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Implode with no flags specified
+ * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+ */
+ function implode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+ $out = array();
+ $first = true;
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+
+ foreach ( $args as $root ) {
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+ $args = array_slice( func_get_args(), 3 );
+ $out = array( $start );
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ $out[] = $end;
+ return $out;
+ }
+
+ function __toString() {
+ return 'frame{}';
+ }
+
+ function getPDBK( $level = false ) {
+ if ( $level === false ) {
+ return $this->title->getPrefixedDBkey();
+ } else {
+ return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+ }
+ }
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return true;
+ }
+
+ function getArgument( $name ) {
+ return false;
+ }
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title ) {
+ return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return false;
+ }
+}
+
+/**
+ * Expansion frame with template arguments
+ */
+class PPTemplateFrame_DOM extends PPFrame_DOM {
+ var $numberedArgs, $namedArgs, $parent;
+ var $numberedExpansionCache, $namedExpansionCache;
+
+ function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->parent = $parent;
+ $this->numberedArgs = $numberedArgs;
+ $this->namedArgs = $namedArgs;
+ $this->title = $title;
+ $pdbk = $title ? $title->getPrefixedDBkey() : false;
+ $this->titleCache = $parent->titleCache;
+ $this->titleCache[] = $pdbk;
+ $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+ if ( $pdbk !== false ) {
+ $this->loopCheckHash[$pdbk] = true;
+ }
+ $this->depth = $parent->depth + 1;
+ $this->numberedExpansionCache = $this->namedExpansionCache = array();
+ }
+
+ function __toString() {
+ $s = 'tplframe{';
+ $first = true;
+ $args = $this->numberedArgs + $this->namedArgs;
+ foreach ( $args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+ }
+
+ function getNumberedArgument( $index ) {
+ if ( !isset( $this->numberedArgs[$index] ) ) {
+ return false;
+ }
+ if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+ # No trimming for unnamed arguments
+ $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+ }
+ return $this->numberedExpansionCache[$index];
+ }
+
+ function getNamedArgument( $name ) {
+ if ( !isset( $this->namedArgs[$name] ) ) {
+ return false;
+ }
+ if ( !isset( $this->namedExpansionCache[$name] ) ) {
+ # Trim named arguments post-expand, for backwards compatibility
+ $this->namedExpansionCache[$name] = trim(
+ $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+ }
+ return $this->namedExpansionCache[$name];
+ }
+
+ function getArgument( $name ) {
+ $text = $this->getNumberedArgument( $name );
+ if ( $text === false ) {
+ $text = $this->getNamedArgument( $name );
+ }
+ return $text;
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return true;
+ }
+}
+
+class PPNode_DOM implements PPNode {
+ var $node;
+
+ function __construct( $node, $xpath = false ) {
+ $this->node = $node;
+ }
+
+ function __get( $name ) {
+ if ( $name == 'xpath' ) {
+ $this->xpath = new DOMXPath( $this->node->ownerDocument );
+ }
+ return $this->xpath;
+ }
+
+ function __toString() {
+ if ( $this->node instanceof DOMNodeList ) {
+ $s = '';
+ foreach ( $this->node as $node ) {
+ $s .= $node->ownerDocument->saveXML( $node );
+ }
+ } else {
+ $s = $this->node->ownerDocument->saveXML( $this->node );
+ }
+ return $s;
+ }
+
+ function getChildren() {
+ return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
+ }
+
+ function getFirstChild() {
+ return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
+ }
+
+ function getNextSibling() {
+ return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
+ }
+
+ function getChildrenOfType( $type ) {
+ return new self( $this->xpath->query( $type, $this->node ) );
+ }
+
+ function getLength() {
+ if ( $this->node instanceof DOMNodeList ) {
+ return $this->node->length;
+ } else {
+ return false;
+ }
+ }
+
+ function item( $i ) {
+ $item = $this->node->item( $i );
+ return $item ? new self( $item ) : false;
+ }
+
+ function getName() {
+ if ( $this->node instanceof DOMNodeList ) {
+ return '#nodelist';
+ } else {
+ return $this->node->nodeName;
+ }
+ }
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg() {
+ $names = $this->xpath->query( 'name', $this->node );
+ $values = $this->xpath->query( 'value', $this->node );
+ if ( !$names->length || !$values->length ) {
+ throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+ }
+ $name = $names->item( 0 );
+ $index = $name->getAttribute( 'index' );
+ return array(
+ 'name' => new self( $name ),
+ 'index' => $index,
+ 'value' => new self( $values->item( 0 ) ) );
+ }
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt() {
+ $names = $this->xpath->query( 'name', $this->node );
+ $attrs = $this->xpath->query( 'attr', $this->node );
+ $inners = $this->xpath->query( 'inner', $this->node );
+ $closes = $this->xpath->query( 'close', $this->node );
+ if ( !$names->length || !$attrs->length ) {
+ throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+ }
+ $parts = array(
+ 'name' => new self( $names->item( 0 ) ),
+ 'attr' => new self( $attrs->item( 0 ) ) );
+ if ( $inners->length ) {
+ $parts['inner'] = new self( $inners->item( 0 ) );
+ }
+ if ( $closes->length ) {
+ $parts['close'] = new self( $closes->item( 0 ) );
+ }
+ return $parts;
+ }
+
+ /**
+ * Split a <h> node
+ */
+ function splitHeading() {
+ if ( !$this->nodeName == 'h' ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ return array(
+ 'i' => $this->node->getAttribute( 'i' ),
+ 'level' => $this->node->getAttribute( 'level' ),
+ 'contents' => $this->getChildren()
+ );
+ }
+}
diff --git a/includes/Preprocessor_Hash.php b/includes/Preprocessor_Hash.php
new file mode 100644
index 00000000..2034278d
--- /dev/null
+++ b/includes/Preprocessor_Hash.php
@@ -0,0 +1,1471 @@
+<?php
+
+/**
+ * Differences from DOM schema:
+ * * attribute nodes are children
+ * * <h> nodes that aren't at the top are replaced with <possible-h>
+ */
+
+class Preprocessor_Hash implements Preprocessor {
+ var $parser;
+
+ function __construct( $parser ) {
+ $this->parser = $parser;
+ }
+
+ function newFrame() {
+ return new PPFrame_Hash( $this );
+ }
+
+ /**
+ * Preprocess some wikitext and return the document tree.
+ * This is the ghost of Parser::replace_variables().
+ *
+ * @param string $text The text to parse
+ * @param integer flags Bitwise combination of:
+ * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * included. Default is to assume a direct page view.
+ *
+ * The generated DOM tree must depend only on the input text and the flags.
+ * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+ *
+ * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+ * change in the DOM tree for a given text, must be passed through the section identifier
+ * in the section edit link and thus back to extractSections().
+ *
+ * The output of this function is currently only cached in process memory, but a persistent
+ * cache may be implemented at a later date which takes further advantage of these strict
+ * dependency requirements.
+ *
+ * @private
+ */
+ function preprocessToObj( $text, $flags = 0 ) {
+ wfDebug( __METHOD__."\n" . $text . "\n" );
+ wfProfileIn( __METHOD__ );
+
+ $rules = array(
+ '{' => array(
+ 'end' => '}',
+ 'names' => array(
+ 2 => 'template',
+ 3 => 'tplarg',
+ ),
+ 'min' => 2,
+ 'max' => 3,
+ ),
+ '[' => array(
+ 'end' => ']',
+ 'names' => array( 2 => null ),
+ 'min' => 2,
+ 'max' => 2,
+ )
+ );
+
+ $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+ $xmlishElements = $this->parser->getStripList();
+ $enableOnlyinclude = false;
+ if ( $forInclusion ) {
+ $ignoredTags = array( 'includeonly', '/includeonly' );
+ $ignoredElements = array( 'noinclude' );
+ $xmlishElements[] = 'noinclude';
+ if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+ $enableOnlyinclude = true;
+ }
+ } else {
+ $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+ $ignoredElements = array( 'includeonly' );
+ $xmlishElements[] = 'includeonly';
+ }
+ $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+ // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+ $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+ $stack = new PPDStack_Hash;
+
+ $searchBase = "[{<\n";
+ $revText = strrev( $text ); // For fast reverse searches
+
+ $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
+ $accum =& $stack->getAccum(); # Current accumulator
+ $findEquals = false; # True to find equals signs in arguments
+ $findPipe = false; # True to take notice of pipe characters
+ $headingIndex = 1;
+ $inHeading = false; # True if $i is inside a possible heading
+ $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
+ $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+ $fakeLineStart = true; # Do a line-start run without outputting an LF character
+
+ while ( true ) {
+ //$this->memCheck();
+
+ if ( $findOnlyinclude ) {
+ // Ignore all input up to the next <onlyinclude>
+ $startPos = strpos( $text, '<onlyinclude>', $i );
+ if ( $startPos === false ) {
+ // Ignored section runs to the end
+ $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
+ break;
+ }
+ $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+ $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
+ $i = $tagEndPos;
+ $findOnlyinclude = false;
+ }
+
+ if ( $fakeLineStart ) {
+ $found = 'line-start';
+ $curChar = '';
+ } else {
+ # Find next opening brace, closing brace or pipe
+ $search = $searchBase;
+ if ( $stack->top === false ) {
+ $currentClosing = '';
+ } else {
+ $currentClosing = $stack->top->close;
+ $search .= $currentClosing;
+ }
+ if ( $findPipe ) {
+ $search .= '|';
+ }
+ if ( $findEquals ) {
+ // First equals will be for the template
+ $search .= '=';
+ }
+ $rule = null;
+ # Output literal section, advance input counter
+ $literalLength = strcspn( $text, $search, $i );
+ if ( $literalLength > 0 ) {
+ $accum->addLiteral( substr( $text, $i, $literalLength ) );
+ $i += $literalLength;
+ }
+ if ( $i >= strlen( $text ) ) {
+ if ( $currentClosing == "\n" ) {
+ // Do a past-the-end run to finish off the heading
+ $curChar = '';
+ $found = 'line-end';
+ } else {
+ # All done
+ break;
+ }
+ } else {
+ $curChar = $text[$i];
+ if ( $curChar == '|' ) {
+ $found = 'pipe';
+ } elseif ( $curChar == '=' ) {
+ $found = 'equals';
+ } elseif ( $curChar == '<' ) {
+ $found = 'angle';
+ } elseif ( $curChar == "\n" ) {
+ if ( $inHeading ) {
+ $found = 'line-end';
+ } else {
+ $found = 'line-start';
+ }
+ } elseif ( $curChar == $currentClosing ) {
+ $found = 'close';
+ } elseif ( isset( $rules[$curChar] ) ) {
+ $found = 'open';
+ $rule = $rules[$curChar];
+ } else {
+ # Some versions of PHP have a strcspn which stops on null characters
+ # Ignore and continue
+ ++$i;
+ continue;
+ }
+ }
+ }
+
+ if ( $found == 'angle' ) {
+ $matches = false;
+ // Handle </onlyinclude>
+ if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+ $findOnlyinclude = true;
+ continue;
+ }
+
+ // Determine element name
+ if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+ // Element name missing or not listed
+ $accum->addLiteral( '<' );
+ ++$i;
+ continue;
+ }
+ // Handle comments
+ if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+ // To avoid leaving blank lines, when a comment is both preceded
+ // and followed by a newline (ignoring spaces), trim leading and
+ // trailing spaces and one of the newlines.
+
+ // Find the end
+ $endPos = strpos( $text, '-->', $i + 4 );
+ if ( $endPos === false ) {
+ // Unclosed comment in input, runs to end
+ $inner = substr( $text, $i );
+ $accum->addNodeWithText( 'comment', $inner );
+ $i = strlen( $text );
+ } else {
+ // Search backwards for leading whitespace
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ // Search forwards for trailing whitespace
+ // $wsEnd will be the position of the last space
+ $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ // Eat the line if possible
+ // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+ // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+ // it's a possible beneficial b/c break.
+ if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+ && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+ {
+ $startPos = $wsStart;
+ $endPos = $wsEnd + 1;
+ // Remove leading whitespace from the end of the accumulator
+ // Sanity check first though
+ $wsLength = $i - $wsStart;
+ if ( $wsLength > 0
+ && $accum->lastNode instanceof PPNode_Hash_Text
+ && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
+ {
+ $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
+ }
+ // Do a line-start run next time to look for headings after the comment
+ $fakeLineStart = true;
+ } else {
+ // No line to eat, just take the comment itself
+ $startPos = $i;
+ $endPos += 2;
+ }
+
+ if ( $stack->top ) {
+ $part = $stack->top->getCurrentPart();
+ if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+ // Comments abutting, no change in visual end
+ $part->commentEnd = $wsEnd;
+ } else {
+ $part->visualEnd = $wsStart;
+ $part->commentEnd = $endPos;
+ }
+ }
+ $i = $endPos + 1;
+ $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+ $accum->addNodeWithText( 'comment', $inner );
+ }
+ continue;
+ }
+ $name = $matches[1];
+ $attrStart = $i + strlen( $name ) + 1;
+
+ // Find end of tag
+ $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+ if ( $tagEndPos === false ) {
+ // Infinite backtrack
+ // Disable tag search to prevent worst-case O(N^2) performance
+ $noMoreGT = true;
+ $accum->addLiteral( '<' );
+ ++$i;
+ continue;
+ }
+
+ // Handle ignored tags
+ if ( in_array( $name, $ignoredTags ) ) {
+ $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
+ $i = $tagEndPos + 1;
+ continue;
+ }
+
+ $tagStartPos = $i;
+ if ( $text[$tagEndPos-1] == '/' ) {
+ // Short end tag
+ $attrEnd = $tagEndPos - 1;
+ $inner = null;
+ $i = $tagEndPos + 1;
+ $close = null;
+ } else {
+ $attrEnd = $tagEndPos;
+ // Find closing tag
+ if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+ $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+ $i = $matches[0][1] + strlen( $matches[0][0] );
+ $close = $matches[0][0];
+ } else {
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = strlen( $text );
+ $close = null;
+ }
+ }
+ // <includeonly> and <noinclude> just become <ignore> tags
+ if ( in_array( $name, $ignoredElements ) ) {
+ $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
+ continue;
+ }
+
+ if ( $attrEnd <= $attrStart ) {
+ $attr = '';
+ } else {
+ // Note that the attr element contains the whitespace between name and attribute,
+ // this is necessary for precise reconstruction during pre-save transform.
+ $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+ }
+
+ $extNode = new PPNode_Hash_Tree( 'ext' );
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
+ if ( $inner !== null ) {
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
+ }
+ if ( $close !== null ) {
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
+ }
+ $accum->addNode( $extNode );
+ }
+
+ elseif ( $found == 'line-start' ) {
+ // Is this the start of a heading?
+ // Line break belongs before the heading element in any case
+ if ( $fakeLineStart ) {
+ $fakeLineStart = false;
+ } else {
+ $accum->addLiteral( $curChar );
+ $i++;
+ }
+
+ $count = strspn( $text, '=', $i, 6 );
+ if ( $count == 1 && $findEquals ) {
+ // DWIM: This looks kind of like a name/value separator
+ // Let's let the equals handler have it and break the potential heading
+ // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+ } elseif ( $count > 0 ) {
+ $piece = array(
+ 'open' => "\n",
+ 'close' => "\n",
+ 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
+ 'startPos' => $i,
+ 'count' => $count );
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ $i += $count;
+ }
+ }
+
+ elseif ( $found == 'line-end' ) {
+ $piece = $stack->top;
+ // A heading must be open, otherwise \n wouldn't have been in the search list
+ assert( $piece->open == "\n" );
+ $part = $piece->getCurrentPart();
+ // Search back through the input to see if it has a proper close
+ // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+ $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $searchStart = $i - $wsLength;
+ if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+ // Comment found at line end
+ // Search for equals signs before the comment
+ $searchStart = $part->visualEnd;
+ $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ }
+ $count = $piece->count;
+ $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ if ( $equalsLength > 0 ) {
+ if ( $i - $equalsLength == $piece->startPos ) {
+ // This is just a single string of equals signs on its own line
+ // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // First find out how many equals signs there really are (don't stop at 6)
+ $count = $equalsLength;
+ if ( $count < 3 ) {
+ $count = 0;
+ } else {
+ $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+ }
+ } else {
+ $count = min( $equalsLength, $count );
+ }
+ if ( $count > 0 ) {
+ // Normal match, output <h>
+ $element = new PPNode_Hash_Tree( 'possible-h' );
+ $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
+ $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
+ $element->lastChild->nextSibling = $accum->firstNode;
+ $element->lastChild = $accum->lastNode;
+ } else {
+ // Single equals sign on its own line, count=0
+ $element = $accum;
+ }
+ } else {
+ // No match, no <h>, just pass down the inner text
+ $element = $accum;
+ }
+ // Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+
+ // Append the result to the enclosing accumulator
+ if ( $element instanceof PPNode ) {
+ $accum->addNode( $element );
+ } else {
+ $accum->addAccum( $element );
+ }
+ // Note that we do NOT increment the input pointer.
+ // This is because the closing linebreak could be the opening linebreak of
+ // another heading. Infinite loops are avoided because the next iteration MUST
+ // hit the heading open case above, which unconditionally increments the
+ // input pointer.
+ }
+
+ elseif ( $found == 'open' ) {
+ # count opening brace characters
+ $count = strspn( $text, $curChar, $i );
+
+ # we need to add to stack only if opening brace count is enough for one of the rules
+ if ( $count >= $rule['min'] ) {
+ # Add it to the stack
+ $piece = array(
+ 'open' => $curChar,
+ 'close' => $rule['end'],
+ 'count' => $count,
+ 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ );
+
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ } else {
+ # Add literal brace(s)
+ $accum->addLiteral( str_repeat( $curChar, $count ) );
+ }
+ $i += $count;
+ }
+
+ elseif ( $found == 'close' ) {
+ $piece = $stack->top;
+ # lets check if there are enough characters for closing brace
+ $maxCount = $piece->count;
+ $count = strspn( $text, $curChar, $i, $maxCount );
+
+ # check for maximum matching characters (if there are 5 closing
+ # characters, we will probably need only 3 - depending on the rules)
+ $matchingCount = 0;
+ $rule = $rules[$piece->open];
+ if ( $count > $rule['max'] ) {
+ # The specified maximum exists in the callback array, unless the caller
+ # has made an error
+ $matchingCount = $rule['max'];
+ } else {
+ # Count is less than the maximum
+ # Skip any gaps in the callback array to find the true largest match
+ # Need to use array_key_exists not isset because the callback can be null
+ $matchingCount = $count;
+ while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
+ --$matchingCount;
+ }
+ }
+
+ if ($matchingCount <= 0) {
+ # No matching element found in callback array
+ # Output a literal closing brace and continue
+ $accum->addLiteral( str_repeat( $curChar, $count ) );
+ $i += $count;
+ continue;
+ }
+ $name = $rule['names'][$matchingCount];
+ if ( $name === null ) {
+ // No element, just literal text
+ $element = $piece->breakSyntax( $matchingCount );
+ $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
+ } else {
+ # Create XML element
+ # Note: $parts is already XML, does not need to be encoded further
+ $parts = $piece->parts;
+ $titleAccum = $parts[0]->out;
+ unset( $parts[0] );
+
+ $element = new PPNode_Hash_Tree( $name );
+
+ # The invocation is at the start of the line if lineStart is set in
+ # the stack, and all opening brackets are used up.
+ if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
+ }
+ $titleNode = new PPNode_Hash_Tree( 'title' );
+ $titleNode->firstChild = $titleAccum->firstNode;
+ $titleNode->lastChild = $titleAccum->lastNode;
+ $element->addChild( $titleNode );
+ $argIndex = 1;
+ foreach ( $parts as $partIndex => $part ) {
+ if ( isset( $part->eqpos ) ) {
+ // Find equals
+ $lastNode = false;
+ for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
+ if ( $node === $part->eqpos ) {
+ break;
+ }
+ $lastNode = $node;
+ }
+ if ( !$node ) {
+ throw new MWException( __METHOD__. ': eqpos not found' );
+ }
+ if ( $node->name !== 'equals' ) {
+ throw new MWException( __METHOD__ .': eqpos is not equals' );
+ }
+ $equalsNode = $node;
+
+ // Construct name node
+ $nameNode = new PPNode_Hash_Tree( 'name' );
+ if ( $lastNode !== false ) {
+ $lastNode->nextSibling = false;
+ $nameNode->firstChild = $part->out->firstNode;
+ $nameNode->lastChild = $lastNode;
+ }
+
+ // Construct value node
+ $valueNode = new PPNode_Hash_Tree( 'value' );
+ if ( $equalsNode->nextSibling !== false ) {
+ $valueNode->firstChild = $equalsNode->nextSibling;
+ $valueNode->lastChild = $part->out->lastNode;
+ }
+ $partNode = new PPNode_Hash_Tree( 'part' );
+ $partNode->addChild( $nameNode );
+ $partNode->addChild( $equalsNode->firstChild );
+ $partNode->addChild( $valueNode );
+ $element->addChild( $partNode );
+ } else {
+ $partNode = new PPNode_Hash_Tree( 'part' );
+ $nameNode = new PPNode_Hash_Tree( 'name' );
+ $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
+ $valueNode = new PPNode_Hash_Tree( 'value' );
+ $valueNode->firstChild = $part->out->firstNode;
+ $valueNode->lastChild = $part->out->lastNode;
+ $partNode->addChild( $nameNode );
+ $partNode->addChild( $valueNode );
+ $element->addChild( $partNode );
+ }
+ }
+ }
+
+ # Advance input pointer
+ $i += $matchingCount;
+
+ # Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+
+ # Re-add the old stack element if it still has unmatched opening characters remaining
+ if ($matchingCount < $piece->count) {
+ $piece->parts = array( new PPDPart_Hash );
+ $piece->count -= $matchingCount;
+ # do we still qualify for any callback with remaining count?
+ $names = $rules[$piece->open]['names'];
+ $skippedBraces = 0;
+ $enclosingAccum =& $accum;
+ while ( $piece->count ) {
+ if ( array_key_exists( $piece->count, $names ) ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ break;
+ }
+ --$piece->count;
+ $skippedBraces ++;
+ }
+ $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
+ }
+
+ extract( $stack->getFlags() );
+
+ # Add XML element to the enclosing accumulator
+ if ( $element instanceof PPNode ) {
+ $accum->addNode( $element );
+ } else {
+ $accum->addAccum( $element );
+ }
+ }
+
+ elseif ( $found == 'pipe' ) {
+ $findEquals = true; // shortcut for getFlags()
+ $stack->addPart();
+ $accum =& $stack->getAccum();
+ ++$i;
+ }
+
+ elseif ( $found == 'equals' ) {
+ $findEquals = false; // shortcut for getFlags()
+ $accum->addNodeWithText( 'equals', '=' );
+ $stack->getCurrentPart()->eqpos = $accum->lastNode;
+ ++$i;
+ }
+ }
+
+ # Output any remaining unclosed brackets
+ foreach ( $stack->stack as $piece ) {
+ $stack->rootAccum->addAccum( $piece->breakSyntax() );
+ }
+
+ # Enable top-level headings
+ for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
+ if ( isset( $node->name ) && $node->name === 'possible-h' ) {
+ $node->name = 'h';
+ }
+ }
+
+ $rootNode = new PPNode_Hash_Tree( 'root' );
+ $rootNode->firstChild = $stack->rootAccum->firstNode;
+ $rootNode->lastChild = $stack->rootAccum->lastNode;
+ wfProfileOut( __METHOD__ );
+ return $rootNode;
+ }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ */
+class PPDStack_Hash extends PPDStack {
+ function __construct() {
+ $this->elementClass = 'PPDStackElement_Hash';
+ parent::__construct();
+ $this->rootAccum = new PPDAccum_Hash;
+ }
+}
+
+class PPDStackElement_Hash extends PPDStackElement {
+ function __construct( $data = array() ) {
+ $this->partClass = 'PPDPart_Hash';
+ parent::__construct( $data );
+ }
+
+ /**
+ * Get the accumulator that would result if the close is not found.
+ */
+ function breakSyntax( $openingCount = false ) {
+ if ( $this->open == "\n" ) {
+ $accum = $this->parts[0]->out;
+ } else {
+ if ( $openingCount === false ) {
+ $openingCount = $this->count;
+ }
+ $accum = new PPDAccum_Hash;
+ $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
+ $first = true;
+ foreach ( $this->parts as $part ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $accum->addLiteral( '|' );
+ }
+ $accum->addAccum( $part->out );
+ }
+ }
+ return $accum;
+ }
+}
+
+class PPDPart_Hash extends PPDPart {
+ function __construct( $out = '' ) {
+ $accum = new PPDAccum_Hash;
+ if ( $out !== '' ) {
+ $accum->addLiteral( $out );
+ }
+ parent::__construct( $accum );
+ }
+}
+
+class PPDAccum_Hash {
+ var $firstNode, $lastNode;
+
+ function __construct() {
+ $this->firstNode = $this->lastNode = false;
+ }
+
+ /**
+ * Append a string literal
+ */
+ function addLiteral( $s ) {
+ if ( $this->lastNode === false ) {
+ $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
+ } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
+ $this->lastNode->value .= $s;
+ } else {
+ $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
+ $this->lastNode = $this->lastNode->nextSibling;
+ }
+ }
+
+ /**
+ * Append a PPNode
+ */
+ function addNode( PPNode $node ) {
+ if ( $this->lastNode === false ) {
+ $this->firstNode = $this->lastNode = $node;
+ } else {
+ $this->lastNode->nextSibling = $node;
+ $this->lastNode = $node;
+ }
+ }
+
+ /**
+ * Append a tree node with text contents
+ */
+ function addNodeWithText( $name, $value ) {
+ $node = PPNode_Hash_Tree::newWithText( $name, $value );
+ $this->addNode( $node );
+ }
+
+ /**
+ * Append a PPAccum_Hash
+ * Takes over ownership of the nodes in the source argument. These nodes may
+ * subsequently be modified, especially nextSibling.
+ */
+ function addAccum( $accum ) {
+ if ( $accum->lastNode === false ) {
+ // nothing to add
+ } elseif ( $this->lastNode === false ) {
+ $this->firstNode = $accum->firstNode;
+ $this->lastNode = $accum->lastNode;
+ } else {
+ $this->lastNode->nextSibling = $accum->firstNode;
+ $this->lastNode = $accum->lastNode;
+ }
+ }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ */
+class PPFrame_Hash implements PPFrame {
+ var $preprocessor, $parser, $title;
+ var $titleCache;
+
+ /**
+ * Hashtable listing templates which are disallowed for expansion in this frame,
+ * having been encountered previously in parent frames.
+ */
+ var $loopCheckHash;
+
+ /**
+ * Recursion depth of this frame, top = 0
+ */
+ var $depth;
+
+
+ /**
+ * Construct a new preprocessor frame.
+ * @param Preprocessor $preprocessor The parent preprocessor
+ */
+ function __construct( $preprocessor ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->title = $this->parser->mTitle;
+ $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+ $this->loopCheckHash = array();
+ $this->depth = 0;
+ }
+
+ /**
+ * Create a new child frame
+ * $args is optionally a multi-root PPNode or array containing the template arguments
+ */
+ function newChild( $args = false, $title = false ) {
+ $namedArgs = array();
+ $numberedArgs = array();
+ if ( $title === false ) {
+ $title = $this->title;
+ }
+ if ( $args !== false ) {
+ $xpath = false;
+ if ( $args instanceof PPNode_Hash_Array ) {
+ $args = $args->value;
+ } elseif ( !is_array( $args ) ) {
+ throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
+ }
+ foreach ( $args as $arg ) {
+ $bits = $arg->splitArg();
+ if ( $bits['index'] !== '' ) {
+ // Numbered parameter
+ $numberedArgs[$bits['index']] = $bits['value'];
+ unset( $namedArgs[$bits['index']] );
+ } else {
+ // Named parameter
+ $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
+ $namedArgs[$name] = $bits['value'];
+ unset( $numberedArgs[$name] );
+ }
+ }
+ }
+ return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+ }
+
+ function expand( $root, $flags = 0 ) {
+ if ( is_string( $root ) ) {
+ return $root;
+ }
+
+ if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+ {
+ return '<span class="error">Node-count limit exceeded</span>';
+ }
+
+ $outStack = array( '', '' );
+ $iteratorStack = array( false, $root );
+ $indexStack = array( 0, 0 );
+
+ while ( count( $iteratorStack ) > 1 ) {
+ $level = count( $outStack ) - 1;
+ $iteratorNode =& $iteratorStack[ $level ];
+ $out =& $outStack[$level];
+ $index =& $indexStack[$level];
+
+ if ( is_array( $iteratorNode ) ) {
+ if ( $index >= count( $iteratorNode ) ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode[$index];
+ $index++;
+ }
+ } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
+ if ( $index >= $iteratorNode->getLength() ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode->item( $index );
+ $index++;
+ }
+ } else {
+ // Copy to $contextNode and then delete from iterator stack,
+ // because this is not an iterator but we do have to execute it once
+ $contextNode = $iteratorStack[$level];
+ $iteratorStack[$level] = false;
+ }
+
+ $newIterator = false;
+
+ if ( $contextNode === false ) {
+ // nothing to do
+ } elseif ( is_string( $contextNode ) ) {
+ $out .= $contextNode;
+ } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
+ $newIterator = $contextNode;
+ } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
+ // No output
+ } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
+ $out .= $contextNode->value;
+ } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
+ if ( $contextNode->name == 'template' ) {
+ # Double-brace expansion
+ $bits = $contextNode->splitTemplate();
+ if ( $flags & self::NO_TEMPLATES ) {
+ $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
+ } else {
+ $ret = $this->parser->braceSubstitution( $bits, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->name == 'tplarg' ) {
+ # Triple-brace expansion
+ $bits = $contextNode->splitTemplate();
+ if ( $flags & self::NO_ARGS ) {
+ $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
+ } else {
+ $ret = $this->parser->argSubstitution( $bits, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->name == 'comment' ) {
+ # HTML-style comment
+ # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+ if ( $this->parser->ot['html']
+ || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+ || ( $flags & self::STRIP_COMMENTS ) )
+ {
+ $out .= '';
+ }
+ # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+ # Not in RECOVER_COMMENTS mode (extractSections) though
+ elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+ $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
+ }
+ # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+ else {
+ $out .= $contextNode->firstChild->value;
+ }
+ } elseif ( $contextNode->name == 'ignore' ) {
+ # Output suppression used by <includeonly> etc.
+ # OT_WIKI will only respect <ignore> in substed templates.
+ # The other output types respect it unless NO_IGNORE is set.
+ # extractSections() sets NO_IGNORE and so never respects it.
+ if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+ $out .= $contextNode->firstChild->value;
+ } else {
+ //$out .= '';
+ }
+ } elseif ( $contextNode->name == 'ext' ) {
+ # Extension tag
+ $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
+ $out .= $this->parser->extensionSubstitution( $bits, $this );
+ } elseif ( $contextNode->name == 'h' ) {
+ # Heading
+ if ( $this->parser->ot['html'] ) {
+ # Expand immediately and insert heading index marker
+ $s = '';
+ for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
+ $s .= $this->expand( $node, $flags );
+ }
+
+ $bits = $contextNode->splitHeading();
+ $titleText = $this->title->getPrefixedDBkey();
+ $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
+ $serial = count( $this->parser->mHeadings ) - 1;
+ $marker = "{$this->parser->mUniqPrefix}-h-$serial-{$this->parser->mMarkerSuffix}";
+ $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
+ $this->parser->mStripState->general->setPair( $marker, '' );
+ $out .= $s;
+ } else {
+ # Expand in virtual stack
+ $newIterator = $contextNode->getChildren();
+ }
+ } else {
+ # Generic recursive expansion
+ $newIterator = $contextNode->getChildren();
+ }
+ } else {
+ throw new MWException( __METHOD__.': Invalid parameter type' );
+ }
+
+ if ( $newIterator !== false ) {
+ $outStack[] = '';
+ $iteratorStack[] = $newIterator;
+ $indexStack[] = 0;
+ } elseif ( $iteratorStack[$level] === false ) {
+ // Return accumulated value to parent
+ // With tail recursion
+ while ( $iteratorStack[$level] === false && $level > 0 ) {
+ $outStack[$level - 1] .= $out;
+ array_pop( $outStack );
+ array_pop( $iteratorStack );
+ array_pop( $indexStack );
+ $level--;
+ }
+ }
+ }
+ return $outStack[0];
+ }
+
+ function implodeWithFlags( $sep, $flags /*, ... */ ) {
+ $args = array_slice( func_get_args(), 2 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node, $flags );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Implode with no flags specified
+ * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+ */
+ function implode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+ $out = array();
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ return new PPNode_Hash_Array( $out );
+ }
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+ $args = array_slice( func_get_args(), 3 );
+ $out = array( $start );
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ $out[] = $end;
+ return new PPNode_Hash_Array( $out );
+ }
+
+ function __toString() {
+ return 'frame{}';
+ }
+
+ function getPDBK( $level = false ) {
+ if ( $level === false ) {
+ return $this->title->getPrefixedDBkey();
+ } else {
+ return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+ }
+ }
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return true;
+ }
+
+ function getArgument( $name ) {
+ return false;
+ }
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title ) {
+ return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return false;
+ }
+}
+
+/**
+ * Expansion frame with template arguments
+ */
+class PPTemplateFrame_Hash extends PPFrame_Hash {
+ var $numberedArgs, $namedArgs, $parent;
+ var $numberedExpansionCache, $namedExpansionCache;
+
+ function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->parent = $parent;
+ $this->numberedArgs = $numberedArgs;
+ $this->namedArgs = $namedArgs;
+ $this->title = $title;
+ $pdbk = $title ? $title->getPrefixedDBkey() : false;
+ $this->titleCache = $parent->titleCache;
+ $this->titleCache[] = $pdbk;
+ $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+ if ( $pdbk !== false ) {
+ $this->loopCheckHash[$pdbk] = true;
+ }
+ $this->depth = $parent->depth + 1;
+ $this->numberedExpansionCache = $this->namedExpansionCache = array();
+ }
+
+ function __toString() {
+ $s = 'tplframe{';
+ $first = true;
+ $args = $this->numberedArgs + $this->namedArgs;
+ foreach ( $args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->__toString() ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+ }
+
+ function getNumberedArgument( $index ) {
+ if ( !isset( $this->numberedArgs[$index] ) ) {
+ return false;
+ }
+ if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+ # No trimming for unnamed arguments
+ $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+ }
+ return $this->numberedExpansionCache[$index];
+ }
+
+ function getNamedArgument( $name ) {
+ if ( !isset( $this->namedArgs[$name] ) ) {
+ return false;
+ }
+ if ( !isset( $this->namedExpansionCache[$name] ) ) {
+ # Trim named arguments post-expand, for backwards compatibility
+ $this->namedExpansionCache[$name] = trim(
+ $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+ }
+ return $this->namedExpansionCache[$name];
+ }
+
+ function getArgument( $name ) {
+ $text = $this->getNumberedArgument( $name );
+ if ( $text === false ) {
+ $text = $this->getNamedArgument( $name );
+ }
+ return $text;
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return true;
+ }
+}
+
+class PPNode_Hash_Tree implements PPNode {
+ var $name, $firstChild, $lastChild, $nextSibling;
+
+ function __construct( $name ) {
+ $this->name = $name;
+ $this->firstChild = $this->lastChild = $this->nextSibling = false;
+ }
+
+ function __toString() {
+ $inner = '';
+ $attribs = '';
+ for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
+ if ( $node instanceof PPNode_Hash_Attr ) {
+ $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
+ } else {
+ $inner .= $node->__toString();
+ }
+ }
+ if ( $inner === '' ) {
+ return "<{$this->name}$attribs/>";
+ } else {
+ return "<{$this->name}$attribs>$inner</{$this->name}>";
+ }
+ }
+
+ function newWithText( $name, $text ) {
+ $obj = new self( $name );
+ $obj->addChild( new PPNode_Hash_Text( $text ) );
+ return $obj;
+ }
+
+ function addChild( $node ) {
+ if ( $this->lastChild === false ) {
+ $this->firstChild = $this->lastChild = $node;
+ } else {
+ $this->lastChild->nextSibling = $node;
+ $this->lastChild = $node;
+ }
+ }
+
+ function getChildren() {
+ $children = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ $children[] = $child;
+ }
+ return new PPNode_Hash_Array( $children );
+ }
+
+ function getFirstChild() {
+ return $this->firstChild;
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildrenOfType( $name ) {
+ $children = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( isset( $child->name ) && $child->name === $name ) {
+ $children[] = $name;
+ }
+ }
+ return $children;
+ }
+
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+
+ function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg() {
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name === 'name' ) {
+ $bits['name'] = $child;
+ if ( $child->firstChild instanceof PPNode_Hash_Attr
+ && $child->firstChild->name === 'index' )
+ {
+ $bits['index'] = $child->firstChild->value;
+ }
+ } elseif ( $child->name === 'value' ) {
+ $bits['value'] = $child;
+ }
+ }
+
+ if ( !isset( $bits['name'] ) ) {
+ throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+ }
+ if ( !isset( $bits['index'] ) ) {
+ $bits['index'] = '';
+ }
+ return $bits;
+ }
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt() {
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'name' ) {
+ $bits['name'] = $child;
+ } elseif ( $child->name == 'attr' ) {
+ $bits['attr'] = $child;
+ } elseif ( $child->name == 'inner' ) {
+ $bits['inner'] = $child;
+ } elseif ( $child->name == 'close' ) {
+ $bits['close'] = $child;
+ }
+ }
+ if ( !isset( $bits['name'] ) ) {
+ throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+ }
+ return $bits;
+ }
+
+ /**
+ * Split an <h> node
+ */
+ function splitHeading() {
+ if ( $this->name !== 'h' ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'i' ) {
+ $bits['i'] = $child->value;
+ } elseif ( $child->name == 'level' ) {
+ $bits['level'] = $child->value;
+ }
+ }
+ if ( !isset( $bits['i'] ) ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ return $bits;
+ }
+
+ /**
+ * Split a <template> or <tplarg> node
+ */
+ function splitTemplate() {
+ wfDebug( 'Template: ' . var_export( $this, true ) );
+ $parts = array();
+ $bits = array( 'lineStart' => '' );
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ wfDebug( 'Child: ' . var_export( $child, true ) );
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'title' ) {
+ $bits['title'] = $child;
+ }
+ if ( $child->name == 'part' ) {
+ $parts[] = $child;
+ }
+ if ( $child->name == 'lineStart' ) {
+ $bits['lineStart'] = '1';
+ }
+ }
+ if ( !isset( $bits['title'] ) ) {
+ throw new MWException( 'Invalid node passed to ' . __METHOD__ );
+ }
+ $bits['parts'] = new PPNode_Hash_Array( $parts );
+ return $bits;
+ }
+}
+
+class PPNode_Hash_Text implements PPNode {
+ var $value, $nextSibling;
+
+ function __construct( $value ) {
+ if ( is_object( $value ) ) {
+ throw new MWException( __CLASS__ . ' given object instead of string' );
+ }
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return htmlspecialchars( $this->value );
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+ function getName() { return '#text'; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+class PPNode_Hash_Array implements PPNode {
+ var $value, $nextSibling;
+
+ function __construct( $value ) {
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return var_export( $this, true );
+ }
+
+ function getLength() {
+ return count( $this->value );
+ }
+
+ function item( $i ) {
+ return $this->value[$i];
+ }
+
+ function getName() { return '#nodelist'; }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+class PPNode_Hash_Attr implements PPNode {
+ var $name, $value, $nextSibling;
+
+ function __construct( $name, $value ) {
+ $this->name = $name;
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
+ }
+
+ function getName() {
+ return $this->name;
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php
index b07f2517..20ab99c0 100644
--- a/includes/ProfilerSimple.php
+++ b/includes/ProfilerSimple.php
@@ -72,10 +72,14 @@ class ProfilerSimple extends Profiler {
$message = "Profile section ended by close(): {$ofname}";
$functionname = $ofname;
$this->debug( "$message\n" );
+ $this->mCollated[$message] = array(
+ 'real' => 0.0, 'count' => 1);
}
elseif ($ofname != $functionname) {
$message = "Profiling error: in({$ofname}), out($functionname)";
$this->debug( "$message\n" );
+ $this->mCollated[$message] = array(
+ 'real' => 0.0, 'count' => 1);
}
$entry =& $this->mCollated[$functionname];
$elapsedcpu = $this->getCpuTime() - $octime;
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index c249ec12..a5ff4f3e 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -28,17 +28,20 @@ class ProtectionForm {
var $mReason = '';
var $mCascade = false;
var $mExpiry = null;
+ var $mPermErrors = array();
+ var $mApplicableTypes = array();
function __construct( &$article ) {
global $wgRequest, $wgUser;
global $wgRestrictionTypes, $wgRestrictionLevels;
$this->mArticle =& $article;
$this->mTitle =& $article->mTitle;
+ $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create');
if( $this->mTitle ) {
$this->mTitle->loadRestrictions();
- foreach( $wgRestrictionTypes as $action ) {
+ foreach( $this->mApplicableTypes as $action ) {
// Fixme: this form currently requires individual selections,
// but the db allows multiples separated by commas.
$this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
@@ -56,7 +59,7 @@ class ProtectionForm {
}
// The form will be available in read-only to show levels.
- $this->disabled = !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $wgUser->isBlocked();
+ $this->disabled = wfReadOnly() || ($this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser)) != array();
$this->disabledAttrib = $this->disabled
? array( 'disabled' => 'disabled' )
: array();
@@ -66,7 +69,7 @@ class ProtectionForm {
$this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
$this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' );
- foreach( $wgRestrictionTypes as $action ) {
+ foreach( $this->mApplicableTypes as $action ) {
$val = $wgRequest->getVal( "mwProtect-level-$action" );
if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
$this->mRestrictions[$action] = $val;
@@ -94,7 +97,6 @@ class ProtectionForm {
$wgOut->setRobotpolicy( 'noindex,nofollow' );
if( is_null( $this->mTitle ) ||
- !$this->mTitle->exists() ||
$this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
$wgOut->showFatalError( wfMsg( 'badarticleerror' ) );
return;
@@ -114,9 +116,7 @@ class ProtectionForm {
$titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
}
- $notice = wfMsgExt( 'protect-cascadeon', array('parsemag'), count($cascadeSources) ) . "\r\n$titles";
-
- $wgOut->addWikiText( $notice );
+ $wgOut->wrapWikiMsg( "$1\n$titles", array( 'protect-cascadeon', count($cascadeSources) ) );
}
$wgOut->setPageTitle( wfMsg( 'confirmprotect' ) );
@@ -125,22 +125,14 @@ class ProtectionForm {
# Show an appropriate message if the user isn't allowed or able to change
# the protection settings at this time
if( $this->disabled ) {
- if( $wgUser->isAllowed( 'protect' ) ) {
- if( $wgUser->isBlocked() ) {
- # Blocked
- $message = 'protect-locked-blocked';
- } else {
- # Database lock
- $message = 'protect-locked-dblock';
- }
- } else {
- # Permission error
- $message = 'protect-locked-access';
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ } elseif( $this->mPermErrors ) {
+ $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $this->mPermErrors ) );
}
} else {
- $message = 'protect-text';
+ $wgOut->addWikiMsg( 'protect-text', $this->mTitle->getPrefixedText() );
}
- $wgOut->addWikiText( wfMsg( $message, wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) );
$wgOut->addHTML( $this->buildForm() );
@@ -185,7 +177,22 @@ class ProtectionForm {
}
- $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry );
+ # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied
+ # to a semi-protected page.
+ global $wgGroupPermissions;
+
+ $edit_restriction = $this->mRestrictions['edit'];
+
+ if ($this->mCascade && ($edit_restriction != 'protect') &&
+ !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) )
+ $this->mCascade = false;
+
+ if ($this->mTitle->exists()) {
+ $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry );
+ } else {
+ $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $this->mReason, $expiry );
+ }
+
if( !$ok ) {
throw new FatalError( "Unknown error at restriction save time." );
}
@@ -222,6 +229,7 @@ class ProtectionForm {
$out .= "<table id='mwProtectSet'>";
$out .= "<tbody>";
$out .= "<tr>\n";
+
foreach( $this->mRestrictions as $action => $required ) {
/* Not all languages have V_x <-> N_x relation */
$out .= "<th>" . wfMsgHtml( 'restriction-' . $action ) . "</th>\n";
@@ -244,7 +252,7 @@ class ProtectionForm {
$out .= "<tbody>\n";
global $wgEnableCascadingProtection;
- if( $wgEnableCascadingProtection )
+ if( $wgEnableCascadingProtection && $this->mTitle->exists() )
$out .= '<tr><td></td><td>' . $this->buildCascadeInput() . "</td></tr>\n";
$out .= $this->buildExpiryInput();
@@ -311,6 +319,7 @@ class ProtectionForm {
'</td><td>' .
wfElement( 'input', array(
'size' => 60,
+ 'maxlength' => 255,
'name' => $id,
'id' => $id,
'value' => $this->mReason ) );
@@ -359,12 +368,12 @@ class ProtectionForm {
$script = 'var wgCascadeableLevels=';
$CascadeableLevels = array();
foreach( $wgRestrictionLevels as $key ) {
- if ( isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect'] ) {
+ if ( (isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect']) || $key == 'protect' ) {
$CascadeableLevels[]="'" . wfEscapeJsString($key) . "'";
}
}
$script .= "[" . implode(',',$CascadeableLevels) . "];\n";
- $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")';
+ $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")';
return '<script type="text/javascript">' . $script . '</script>';
}
@@ -383,4 +392,4 @@ class ProtectionForm {
$logViewer->showList( $out );
}
-} \ No newline at end of file
+}
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index 06710b6d..eb4e71bf 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -308,20 +308,18 @@ class QueryPage {
if( $tRow ) {
$updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true );
- $cacheNotice = wfMsg( 'perfcachedts', $updated );
$wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp );
$wgOut->addInlineScript( "var dataCacheTime = '{$tRow->qci_timestamp}';" );
+ $wgOut->addWikiMsg( 'perfcachedts', $updated );
} else {
- $cacheNotice = wfMsg( 'perfcached' );
+ $wgOut->addWikiMsg( 'perfcached' );
}
-
- $wgOut->addWikiText( $cacheNotice );
# If updates on this page have been disabled, let the user know
# that the data set won't be refreshed for now
global $wgDisableQueryPageUpdate;
if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
- $wgOut->addWikiText( wfMsg( 'querypage-no-updates' ) );
+ $wgOut->addWikiMsg( 'querypage-no-updates' );
}
}
@@ -443,9 +441,8 @@ class QueryPage {
/**
* Do any necessary preprocessing of the result object.
- * You should pass this by reference: &$db , &$res [although probably no longer necessary in PHP5]
*/
- function preprocessResults( &$db, &$res ) {}
+ function preprocessResults( $db, $res ) {}
/**
* Similar to above, but packaging in a syndicated feed instead of a web page
diff --git a/includes/RawPage.php b/includes/RawPage.php
index 9df94e50..909c300b 100644
--- a/includes/RawPage.php
+++ b/includes/RawPage.php
@@ -15,12 +15,12 @@
*/
class RawPage {
var $mArticle, $mTitle, $mRequest;
- var $mOldId, $mGen, $mCharset;
+ var $mOldId, $mGen, $mCharset, $mSection;
var $mSmaxage, $mMaxage;
var $mContentType, $mExpandTemplates;
function __construct( &$article, $request = false ) {
- global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType;
+ global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgForcedRawSMaxage, $wgGroupPermissions;
$allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit');
$this->mArticle =& $article;
@@ -35,10 +35,14 @@ class RawPage {
$ctype = $this->mRequest->getVal( 'ctype' );
$smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage );
$maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage );
+
$this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand';
$this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' );
+ $this->mSection = $this->mRequest->getIntOrNull( 'section' );
+
$oldid = $this->mRequest->getInt( 'oldid' );
+
switch ( $wgRequest->getText( 'direction' ) ) {
case 'next':
# output next revision, or nothing if there isn't one
@@ -78,12 +82,23 @@ class RawPage {
$this->mGen = false;
}
$this->mCharset = $wgInputEncoding;
- $this->mSmaxage = intval( $smaxage );
+
+ # Force caching for CSS and JS raw content, default: 5 minutes
+ if (is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType)) {
+ $this->mSmaxage = intval($wgForcedRawSMaxage);
+ } else {
+ $this->mSmaxage = intval( $smaxage );
+ }
$this->mMaxage = $maxage;
- // Output may contain user-specific data; vary for open sessions
- $this->mPrivateCache = ( $this->mSmaxage == 0 ) ||
- ( session_id() != '' );
+ # Output may contain user-specific data;
+ # vary generated content for open sessions and private wikis
+ if ($this->mGen or !$wgGroupPermissions['*']['read']) {
+ $this->mPrivateCache = ( $this->mSmaxage == 0 ) ||
+ ( session_id() != '' );
+ } else {
+ $this->mPrivateCache = false;
+ }
if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) {
$this->mContentType = 'text/x-wiki';
@@ -111,8 +126,7 @@ class RawPage {
$url = $_SERVER['PHP_SELF'];
}
- $ua = @$_SERVER['HTTP_USER_AGENT'];
- if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) {
+ if( strcmp( $wgScript, $url ) ) {
# Internet Explorer will ignore the Content-Type header if it
# thinks it sees a file extension it recognizes. Make sure that
# all raw requests are done through the script node, which will
@@ -177,7 +191,12 @@ class RawPage {
if ( $rev ) {
$lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
header( "Last-modified: $lastmod" );
- $text = $rev->getText();
+
+ if ( !is_null($this->mSection ) ) {
+ global $wgParser;
+ $text = $wgParser->getSection ( $rev->getText(), $this->mSection );
+ } else
+ $text = $rev->getText();
$found = true;
}
}
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index 79f32d0c..750404a9 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -221,8 +221,7 @@ class RecentChange
if( $wgUseEnotif ) {
# this would be better as an extension hook
global $wgUser;
- include_once( "UserMailer.php" );
- $enotif = new EmailNotification();
+ $enotif = new EmailNotification;
$title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
$enotif->notifyOnPageChange( $wgUser, $title,
$this->mAttribs['rc_timestamp'],
@@ -259,14 +258,9 @@ class RecentChange
# Makes an entry in the database corresponding to an edit
public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment,
- $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0,
+ $oldId, $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0,
$newId = 0)
{
-
- if ( $bot === 'default' ) {
- $bot = $user->isAllowed( 'bot' );
- }
-
if ( !$ip ) {
$ip = wfGetIP();
if ( !$ip ) {
@@ -313,7 +307,7 @@ class RecentChange
* Note: the title object must be loaded with the new id using resetArticleID()
* @todo Document parameters and return
*/
- public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default',
+ public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
$ip='', $size = 0, $newId = 0 )
{
if ( !$ip ) {
@@ -322,9 +316,6 @@ class RecentChange
$ip = '';
}
}
- if ( $bot === 'default' ) {
- $bot = $user->isAllowed( 'bot' );
- }
$rc = new RecentChange;
$rc->mAttribs = array(
@@ -363,6 +354,8 @@ class RecentChange
# Makes an entry in the database corresponding to a rename
public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
{
+ global $wgRequest;
+
if ( !$ip ) {
$ip = wfGetIP();
if ( !$ip ) {
@@ -384,7 +377,7 @@ class RecentChange
'rc_comment' => $comment,
'rc_this_oldid' => 0,
'rc_last_oldid' => 0,
- 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0,
+ 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
'rc_moved_to_ns' => $newTitle->getNamespace(),
'rc_moved_to_title' => $newTitle->getDBkey(),
'rc_ip' => $ip,
@@ -415,6 +408,8 @@ class RecentChange
public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
$type, $action, $target, $logComment, $params )
{
+ global $wgRequest;
+
if ( !$ip ) {
$ip = wfGetIP();
if ( !$ip ) {
@@ -436,7 +431,7 @@ class RecentChange
'rc_comment' => $comment,
'rc_this_oldid' => 0,
'rc_last_oldid' => 0,
- 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0,
+ 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
'rc_ip' => $ip,
@@ -626,3 +621,4 @@ class RecentChange
}
}
+
diff --git a/includes/Revision.php b/includes/Revision.php
index 39470923..05a4a68a 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -47,7 +47,7 @@ class Revision {
array( "rev_id=$matchId",
'page_id=rev_page',
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbkey() ) );
+ 'page_title' => $title->getDBkey() ) );
}
/**
@@ -110,7 +110,7 @@ class Revision {
array( "rev_id=$matchId",
'page_id=rev_page',
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbkey() ) );
+ 'page_title' => $title->getDBkey() ) );
}
/**
@@ -131,7 +131,7 @@ class Revision {
array( 'rev_timestamp' => $db->timestamp( $timestamp ),
'page_id=rev_page',
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbkey() ) );
+ 'page_title' => $title->getDBkey() ) );
}
/**
@@ -190,7 +190,7 @@ class Revision {
return Revision::fetchFromConds(
wfGetDB( DB_SLAVE ),
array( 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbkey(),
+ 'page_title' => $title->getDBkey(),
'page_id=rev_page' ) );
}
@@ -209,7 +209,7 @@ class Revision {
wfGetDB( DB_SLAVE ),
array( 'rev_id=page_latest',
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbkey(),
+ 'page_title' => $title->getDBkey(),
'page_id=rev_page' ) );
}
@@ -794,9 +794,8 @@ class Revision {
* @param bool $minor
* @return Revision
*/
- function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
- $fname = 'Revision::newNullRevision';
- wfProfileIn( $fname );
+ public static function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
+ wfProfileIn( __METHOD__ );
$current = $dbw->selectRow(
array( 'page', 'revision' ),
@@ -805,7 +804,7 @@ class Revision {
'page_id' => $pageId,
'page_latest=rev_id',
),
- $fname );
+ __METHOD__ );
if( $current ) {
$revision = new Revision( array(
@@ -818,7 +817,7 @@ class Revision {
$revision = null;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $revision;
}
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index f2dcbf94..c1c8daf3 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -725,7 +725,7 @@ class Sanitizer {
* @return HTML-encoded text fragment
*/
static function encodeAttribute( $text ) {
- $encValue = htmlspecialchars( $text );
+ $encValue = htmlspecialchars( $text, ENT_QUOTES );
// Whitespace is normalized during attribute decoding,
// so if we've been passed non-spaces we must encode them
diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php
index 11fc3deb..c22e58d7 100644
--- a/includes/SearchEngine.php
+++ b/includes/SearchEngine.php
@@ -51,7 +51,7 @@ class SearchEngine {
if($wgContLang->hasVariants()){
$allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm));
}
-
+
foreach($allSearchTerms as $term){
# Exact match? No need to look further.
@@ -102,6 +102,12 @@ class SearchEngine {
return $title;
}
}
+
+ // Give hooks a chance at better match variants
+ $title = null;
+ if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) {
+ return $title;
+ }
}
$title = Title::newFromText( $searchterm );
@@ -109,7 +115,7 @@ class SearchEngine {
# Entering an IP address goes to the contributions page
if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) )
|| User::isIP( trim( $searchterm ) ) ) {
- return SpecialPage::getTitleFor( 'Contributions', $title->getDbkey() );
+ return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() );
}
@@ -336,7 +342,16 @@ class SearchResultSet {
/**
* @addtogroup Search
*/
+class SearchResultTooMany {
+ ## Some search engines may bail out if too many matches are found
+}
+
+
+/**
+ * @addtogroup Search
+ */
class SearchResult {
+
function SearchResult( $row ) {
$this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
}
diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php
index 6d2bbfef..271dbe1d 100644
--- a/includes/SearchMySQL4.php
+++ b/includes/SearchMySQL4.php
@@ -32,7 +32,7 @@ class SearchMySQL4 extends SearchMySQL {
/** @todo document */
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
+ $lc = SearchEngine::legalSearchChars(); // Minus format chars
$searchon = '';
$this->searchTerms = array();
@@ -47,12 +47,14 @@ class SearchMySQL4 extends SearchMySQL {
}
$searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
if( !empty( $terms[3] ) ) {
+ // Match individual terms in result highlighting...
$regexp = preg_quote( $terms[3], '/' );
if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
} else {
+ // Match the quoted term in result highlighting...
$regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
}
- $this->searchTerms[] = $regexp;
+ $this->searchTerms[] = "\b$regexp\b";
}
wfDebug( "Would search with '$searchon'\n" );
wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" );
@@ -64,5 +66,9 @@ class SearchMySQL4 extends SearchMySQL {
$field = $this->getIndexField( $fulltext );
return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
}
+
+ public static function legalSearchChars() {
+ return "\"*" . parent::legalSearchChars();
+ }
}
diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php
index cf9e6981..59110a5a 100644
--- a/includes/SearchPostgres.php
+++ b/includes/SearchPostgres.php
@@ -37,11 +37,24 @@ class SearchPostgres extends SearchEngine {
* @access public
*/
function searchTitle( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term , 'titlevector', 'page_title' )));
+ $q = $this->searchQuery( $term , 'titlevector', 'page_title' );
+ $olderror = error_reporting(E_ERROR);
+ $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) );
+ error_reporting($olderror);
+ if (!$resultSet) {
+ // Needed for "Query requires full scan, GIN doesn't support it"
+ return new SearchResultTooMany();
+ }
return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
}
function searchText( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term, 'textvector', 'old_text' )));
+ $q = $this->searchQuery( $term, 'textvector', 'old_text' );
+ $olderror = error_reporting(E_ERROR);
+ $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) );
+ error_reporting($olderror);
+ if (!$resultSet) {
+ return new SearchResultTooMany();
+ }
return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
}
@@ -122,11 +135,12 @@ class SearchPostgres extends SearchEngine {
$this->db->getServerVersion();
$wgDBversion = $this->db->numeric_version;
}
+ $prefix = $wgDBversion < 8.3 ? "'default'," : '';
$searchstring = $this->parseQuery( $term );
## We need a separate query here so gin does not complain about empty searches
- $SQL = "SELECT to_tsquery('default',$searchstring)";
+ $SQL = "SELECT to_tsquery($prefix $searchstring)";
$res = $this->db->doQuery($SQL);
if (!$res) {
## TODO: Better output (example to catch: one 'two)
@@ -148,15 +162,16 @@ class SearchPostgres extends SearchEngine {
}
$rankscore = $wgDBversion > 8.2 ? 5 : 1;
+ $rank = $wgDBversion < 8.3 ? 'rank' : 'ts_rank';
$query = "SELECT page_id, page_namespace, page_title, ".
- "rank($fulltext, to_tsquery('default',$searchstring), $rankscore) AS score ".
+ "$rank($fulltext, to_tsquery($prefix $searchstring), $rankscore) AS score ".
"FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " .
- "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery('default',$searchstring)";
+ "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery($prefix $searchstring)";
}
## Redirects
if (! $this->showRedirects)
- $query .= ' AND page_is_redirect = 0'; ## IS FALSE
+ $query .= ' AND page_is_redirect = 0';
## Namespaces - defaults to 0
if ( count($this->namespaces) < 1)
diff --git a/includes/Setup.php b/includes/Setup.php
index 66bae0a8..53e0b949 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -198,7 +198,7 @@ $wgCookiePrefix = strtr($wgCookiePrefix, "=,; +.\"'\\[", "__________");
# If session.auto_start is there, we can't touch session name
#
-if( !ini_get( 'session.auto_start' ) )
+if( !wfIniGetBool( 'session.auto_start' ) )
session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) {
@@ -235,7 +235,8 @@ $wgRequest->interpolateTitle();
$wgUser = new StubUser;
$wgLang = new StubUserLang;
$wgOut = new StubObject( 'wgOut', 'OutputPage' );
-$wgParser = new StubObject( 'wgParser', 'Parser' );
+$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
+
$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
array( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) );
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 353f5b3a..beeeaf15 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -19,34 +19,61 @@ class SiteConfiguration {
var $localVHosts = array();
/** */
- function get( $setting, $wiki, $suffix, $params = array() ) {
- if ( array_key_exists( $setting, $this->settings ) ) {
- if ( array_key_exists( $wiki, $this->settings[$setting] ) ) {
- $retval = $this->settings[$setting][$wiki];
- } elseif ( array_key_exists( $suffix, $this->settings[$setting] ) ) {
- $retval = $this->settings[$setting][$suffix];
- } elseif ( array_key_exists( 'default', $this->settings[$setting] ) ) {
- $retval = $this->settings[$setting]['default'];
- } else {
- $retval = NULL;
- }
+ function get( $settingName, $wiki, $suffix, $params = array(), $wikiTags = array() ) {
+ if ( array_key_exists( $settingName, $this->settings ) ) {
+ $thisSetting =& $this->settings[$settingName];
+ do {
+ if ( array_key_exists( $wiki, $thisSetting ) ) {
+ $retval = $thisSetting[$wiki];
+ break;
+ }
+ foreach ( $wikiTags as $tag ) {
+ if ( array_key_exists( $tag, $thisSetting ) ) {
+ $retval = $thisSetting[$tag];
+ break 2;
+ }
+ }
+ if ( array_key_exists( $suffix, $thisSetting ) ) {
+ $retval = $thisSetting[$suffix];
+ break;
+ }
+ if ( array_key_exists( 'default', $thisSetting ) ) {
+ $retval = $thisSetting['default'];
+ break;
+ }
+ $retval = null;
+ } while ( false );
} else {
$retval = NULL;
}
if ( !is_null( $retval ) && count( $params ) ) {
foreach ( $params as $key => $value ) {
- $retval = str_replace( '$' . $key, $value, $retval );
+ $retval = $this->doReplace( '$' . $key, $value, $retval );
}
}
return $retval;
}
+
+ /** Type-safe string replace; won't do replacements on non-strings */
+ function doReplace( $from, $to, $in ) {
+ if( is_string( $in ) ) {
+ return str_replace( $from, $to, $in );
+ } elseif( is_array( $in ) ) {
+ foreach( $in as $key => $val ) {
+ $in[$key] = $this->doReplace( $from, $to, $val );
+ }
+ return $in;
+ } else {
+ return $in;
+ }
+ }
/** */
- function getAll( $wiki, $suffix, $params ) {
+ function getAll( $wiki, $suffix, $params, $wikiTags = array() ) {
$localSettings = array();
foreach ( $this->settings as $varname => $stuff ) {
- $value = $this->get( $varname, $wiki, $suffix, $params );
+ $value = $this->get( $varname, $wiki, $suffix, $params, $wikiTags );
if ( !is_null( $value ) ) {
$localSettings[$varname] = $value;
}
@@ -55,8 +82,8 @@ class SiteConfiguration {
}
/** */
- function getBool( $setting, $wiki, $suffix ) {
- return (bool)($this->get( $setting, $wiki, $suffix ));
+ function getBool( $setting, $wiki, $suffix, $wikiTags = array() ) {
+ return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) );
}
/** */
@@ -69,25 +96,25 @@ class SiteConfiguration {
}
/** */
- function extractVar( $setting, $wiki, $suffix, &$var, $params ) {
- $value = $this->get( $setting, $wiki, $suffix, $params );
+ function extractVar( $setting, $wiki, $suffix, &$var, $params, $wikiTags = array() ) {
+ $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags );
if ( !is_null( $value ) ) {
$var = $value;
}
}
/** */
- function extractGlobal( $setting, $wiki, $suffix, $params ) {
- $value = $this->get( $setting, $wiki, $suffix, $params );
+ function extractGlobal( $setting, $wiki, $suffix, $params, $wikiTags = array() ) {
+ $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags );
if ( !is_null( $value ) ) {
$GLOBALS[$setting] = $value;
}
}
/** */
- function extractAllGlobals( $wiki, $suffix, $params ) {
+ function extractAllGlobals( $wiki, $suffix, $params, $wikiTags = array() ) {
foreach ( $this->settings as $varName => $setting ) {
- $this->extractGlobal( $varName, $wiki, $suffix, $params );
+ $this->extractGlobal( $varName, $wiki, $suffix, $params, $wikiTags );
}
}
diff --git a/includes/Skin.php b/includes/Skin.php
index f9e17057..30d2c2bc 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -16,10 +16,6 @@ class Skin extends Linker {
/**#@+
* @private
*/
- var $lastdate, $lastline;
- var $rc_cache ; # Cache for Enhanced Recent Changes
- var $rcCacheIndex ; # Recent Changes Cache Counter for visibility toggle
- var $rcMoveIndex;
var $mWatchLinkNum = 0; // Appended to end of watch link id's
/**#@-*/
protected $mRevisionId; // The revision ID we're looking at, null if not applicable.
@@ -97,8 +93,7 @@ class Skin extends Linker {
if( isset( $skinNames[$key] ) ) {
return $key;
} else {
- // The old built-in skin
- return 'standard';
+ return 'monobook';
}
}
@@ -115,23 +110,25 @@ class Skin extends Linker {
$skinNames = Skin::getSkinNames();
$skinName = $skinNames[$key];
+ $className = 'Skin'.ucfirst($key);
# Grab the skin class and initialise it.
- // Preload base classes to work around APC/PHP5 bug
- $deps = "{$wgStyleDirectory}/{$skinName}.deps.php";
- if( file_exists( $deps ) ) include_once( $deps );
- require_once( "{$wgStyleDirectory}/{$skinName}.php" );
-
- # Check if we got if not failback to default skin
- $className = 'Skin'.$skinName;
- if( !class_exists( $className ) ) {
- # DO NOT die if the class isn't found. This breaks maintenance
- # scripts and can cause a user account to be unrecoverable
- # except by SQL manipulation if a previously valid skin name
- # is no longer valid.
- wfDebug( "Skin class does not exist: $className\n" );
- $className = 'SkinStandard';
- require_once( "{$wgStyleDirectory}/Standard.php" );
+ if ( !class_exists( $className ) ) {
+ // Preload base classes to work around APC/PHP5 bug
+ $deps = "{$wgStyleDirectory}/{$skinName}.deps.php";
+ if( file_exists( $deps ) ) include_once( $deps );
+ require_once( "{$wgStyleDirectory}/{$skinName}.php" );
+
+ # Check if we got if not failback to default skin
+ if( !class_exists( $className ) ) {
+ # DO NOT die if the class isn't found. This breaks maintenance
+ # scripts and can cause a user account to be unrecoverable
+ # except by SQL manipulation if a previously valid skin name
+ # is no longer valid.
+ wfDebug( "Skin class does not exist: $className\n" );
+ $className = 'SkinMonobook';
+ require_once( "{$wgStyleDirectory}/MonoBook.php" );
+ }
}
$skin = new $className;
return $skin;
@@ -156,21 +153,28 @@ class Skin extends Linker {
}
function initPage( &$out ) {
- global $wgFavicon, $wgScriptPath, $wgSitename, $wgLanguageCode, $wgLanguageNames;
+ global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgSitename, $wgContLang, $wgScriptExtension;
- $fname = 'Skin::initPage';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if( false !== $wgFavicon ) {
$out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
}
+
+ if( false !== $wgAppleTouchIcon ) {
+ $out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
+ }
+
+ $code = $wgContLang->getCode();
+ $name = $wgContLang->getLanguageName( $code );
+ $langName = $name ? $name : $code;
# OpenSearch description link
$out->addLink( array(
'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
- 'href' => "$wgScriptPath/opensearch_desc.php",
- 'title' => "$wgSitename ({$wgLanguageNames[$wgLanguageCode]})",
+ 'href' => "$wgScriptPath/opensearch_desc{$wgScriptExtension}",
+ 'title' => "$wgSitename ($langName)",
));
$this->addMetadataLinks($out);
@@ -179,7 +183,7 @@ class Skin extends Linker {
$this->preloadExistence();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -298,6 +302,7 @@ class Skin extends Linker {
global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle;
global $wgBreakFrames, $wgRequest;
global $wgUseAjax, $wgAjaxWatch;
+ global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI;
$ns = $wgTitle->getNamespace();
$nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
@@ -310,7 +315,7 @@ class Skin extends Linker {
'wgScript' => $wgScript,
'wgServer' => $wgServer,
'wgCanonicalNamespace' => $nsname,
- 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ),
+ 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ),
'wgNamespaceNumber' => $wgTitle->getNamespace(),
'wgPageName' => $wgTitle->getPrefixedDBKey(),
'wgTitle' => $wgTitle->getText(),
@@ -325,6 +330,9 @@ class Skin extends Linker {
'wgContentLanguage' => $wgContLang->getCode(),
'wgBreakFrames' => $wgBreakFrames,
'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0,
+ 'wgVersion' => $wgVersion,
+ 'wgEnableAPI' => $wgEnableAPI,
+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
);
global $wgLivePreview;
@@ -476,7 +484,7 @@ class Skin extends Linker {
function reallyDoGetUserStyles() {
global $wgUser;
$s = '';
- if (($undopt = $wgUser->getOption("underline")) != 2) {
+ if (($undopt = $wgUser->getOption("underline")) < 2) {
$underline = $undopt ? 'underline' : 'none';
$s .= "a { text-decoration: $underline; }\n";
}
@@ -487,17 +495,14 @@ class Skin extends Linker {
a.new, #quickbar a.new,
a.stub, #quickbar a.stub {
color: inherit;
- text-decoration: inherit;
}
a.new:after, #quickbar a.new:after {
content: "?";
color: #CC2200;
- text-decoration: $underline;
}
a.stub:after, #quickbar a.stub:after {
content: "!";
color: #772233;
- text-decoration: $underline;
}
END;
}
@@ -782,12 +787,12 @@ END;
}
function getUndeleteLink() {
- global $wgUser, $wgTitle, $wgContLang, $action;
+ global $wgUser, $wgTitle, $wgContLang, $wgLang, $action;
if( $wgUser->isAllowed( 'deletedhistory' ) &&
(($wgTitle->getArticleId() == 0) || ($action == "history")) &&
($n = $wgTitle->isDeleted() ) )
{
- if ( $wgUser->isAllowed( 'delete' ) ) {
+ if ( $wgUser->isAllowed( 'undelete' ) ) {
$msg = 'thisisdeleted';
} else {
$msg = 'viewdeleted';
@@ -795,7 +800,7 @@ END;
return wfMsg( $msg,
$this->makeKnownLinkObj(
SpecialPage::getTitleFor( 'Undelete', $wgTitle->getPrefixedDBkey() ),
- wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $n ) ) );
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ) ) );
}
return '';
}
@@ -836,8 +841,11 @@ END;
}
function subPageSubtitle() {
- global $wgOut,$wgTitle,$wgNamespacesWithSubpages;
$subpages = '';
+ if(!wfRunHooks('SkinSubPageSubtitle', array(&$subpages)))
+ return $retval;
+
+ global $wgOut, $wgTitle, $wgNamespacesWithSubpages;
if($wgOut->isArticle() && !empty($wgNamespacesWithSubpages[$wgTitle->getNamespace()])) {
$ptext=$wgTitle->getPrefixedText();
if(preg_match('/\//',$ptext)) {
@@ -1072,11 +1080,14 @@ END;
$dbr = wfGetDB( DB_SLAVE );
$watchlist = $dbr->tableName( 'watchlist' );
$sql = "SELECT COUNT(*) AS n FROM $watchlist
- WHERE wl_title='" . $dbr->strencode($wgTitle->getDBKey()) .
+ WHERE wl_title='" . $dbr->strencode($wgTitle->getDBkey()) .
"' AND wl_namespace=" . $wgTitle->getNamespace() ;
$res = $dbr->query( $sql, 'Skin::pageStats');
$x = $dbr->fetchObject( $res );
- $s .= ' ' . wfMsg('number_of_watching_users_pageview', $x->n );
+
+ $s .= ' ' . wfMsgExt( 'number_of_watching_users_pageview',
+ array( 'parseinline' ), $wgLang->formatNum($x->n)
+ );
}
return $s . ' ' . $this->getCopyright();
@@ -1222,7 +1233,8 @@ END;
// Otherwise, we display the link for the user, described in their
// language (which may or may not be the same as the default language),
// but we make the link target be the one site-wide page.
- return $this->makeKnownLink( wfMsgForContent( $page ), wfMsg( $desc ) );
+ return $this->makeKnownLink( wfMsgForContent( $page ),
+ wfMsgExt( $desc, array( 'parsemag', 'escapenoentities' ) ) );
}
}
@@ -1495,11 +1507,11 @@ END;
# If it's present, the link points to this page, otherwise
# it points to the talk page
if( $wgTitle->isTalkPage() ) {
- $title =& $wgTitle;
+ $title = $wgTitle;
} elseif( $wgOut->showNewSectionLink() ) {
- $title =& $wgTitle;
+ $title = $wgTitle;
} else {
- $title =& $wgTitle->getTalkPage();
+ $title = $wgTitle->getTalkPage();
}
return $this->makeKnownLinkObj( $title, wfMsg( 'postcomment' ), 'action=edit&section=new' );
@@ -1590,7 +1602,7 @@ END;
* @private
*/
function buildSidebar() {
- global $parserMemc, $wgEnableSidebarCache;
+ global $parserMemc, $wgEnableSidebarCache, $wgSidebarCacheExpiry;
global $wgLang, $wgContLang;
$fname = 'SkinTemplate::buildSidebar';
@@ -1611,6 +1623,7 @@ END;
$bar = array();
$lines = explode( "\n", wfMsgForContent( 'sidebar' ) );
+ $heading = '';
foreach ($lines as $line) {
if (strpos($line, '*') !== 0)
continue;
@@ -1650,9 +1663,9 @@ END;
}
}
if ($cacheSidebar)
- $parserMemc->set( $key, $bar, 86400 );
+ $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
wfProfileOut( $fname );
return $bar;
}
-} \ No newline at end of file
+}
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index 6ce40606..0178b866 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -219,17 +219,10 @@ class SkinTemplate extends Skin {
$tpl->set( 'catlinks', $this->getCategories());
if( $wgOut->isSyndicated() ) {
$feeds = array();
- foreach( $wgFeedClasses as $format => $class ) {
- $linktext = $format;
- if ( $format == "atom" ) {
- $linktext = wfMsg( 'feed-atom' );
- } else if ( $format == "rss" ) {
- $linktext = wfMsg( 'feed-rss' );
- }
+ foreach( $wgOut->getSyndicationLinks() as $format => $link ) {
$feeds[$format] = array(
- 'text' => $linktext,
- 'href' => $wgRequest->appendQuery( "feed=$format" )
- );
+ 'text' => wfMsg( "feed-$format" ),
+ 'href' => $link );
}
$tpl->setRef( 'feeds', $feeds );
} else {
@@ -351,13 +344,16 @@ class SkinTemplate extends Skin {
$dbr = wfGetDB( DB_SLAVE );
$watchlist = $dbr->tableName( 'watchlist' );
$sql = "SELECT COUNT(*) AS n FROM $watchlist
- WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBKey()) .
+ WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBkey()) .
"' AND wl_namespace=" . $this->mTitle->getNamespace() ;
$res = $dbr->query( $sql, 'SkinTemplate::outputPage');
$x = $dbr->fetchObject( $res );
$numberofwatchingusers = $x->n;
if ($numberofwatchingusers > 0) {
- $tpl->set('numberofwatchingusers', wfMsg('number_of_watching_users_pageview', $numberofwatchingusers));
+ $tpl->set('numberofwatchingusers',
+ wfMsgExt('number_of_watching_users_pageview', array('parseinline'),
+ $wgLang->formatNum($numberofwatchingusers))
+ );
} else {
$tpl->set('numberofwatchingusers', false);
}
@@ -602,6 +598,13 @@ class SkinTemplate extends Skin {
global $wgContLang;
$text = $wgContLang->getFormattedNsText( Namespace::getSubject( $title->getNamespace() ) );
}
+
+ $result = array();
+ if( !wfRunHooks('SkinTemplateTabAction', array(&$this,
+ $title, $message, $selected, $checkEdit,
+ &$classes, &$query, &$text, &$result)) ) {
+ return $result;
+ }
return array(
'class' => implode( ' ', $classes ),
@@ -638,7 +641,7 @@ class SkinTemplate extends Skin {
* @private
*/
function buildContentActionUrls () {
- global $wgContLang, $wgOut;
+ global $wgContLang, $wgLang, $wgOut;
$fname = 'SkinTemplate::buildContentActionUrls';
wfProfileIn( $fname );
@@ -685,7 +688,7 @@ class SkinTemplate extends Skin {
'href' => $this->mTitle->getLocalUrl( 'action=edit&section=new' )
);
}
- } else {
+ } elseif ( $this->mTitle->exists() || $this->mTitle->isAlwaysKnown() ) {
$content_actions['viewsource'] = array(
'class' => ($action == 'edit') ? 'selected' : false,
'text' => wfMsg('viewsource'),
@@ -703,6 +706,22 @@ class SkinTemplate extends Skin {
'href' => $this->mTitle->getLocalUrl( 'action=history')
);
+ if($wgUser->isAllowed('delete')){
+ $content_actions['delete'] = array(
+ 'class' => ($action == 'delete') ? 'selected' : false,
+ 'text' => wfMsg('delete'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=delete' )
+ );
+ }
+ if ( $this->mTitle->quickUserCan( 'move' ) ) {
+ $moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage );
+ $content_actions['move'] = array(
+ 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false,
+ 'text' => wfMsg('move'),
+ 'href' => $moveTitle->getLocalUrl()
+ );
+ }
+
if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) {
if(!$this->mTitle->isProtected()){
$content_actions['protect'] = array(
@@ -719,35 +738,38 @@ class SkinTemplate extends Skin {
);
}
}
- if($wgUser->isAllowed('delete')){
- $content_actions['delete'] = array(
- 'class' => ($action == 'delete') ? 'selected' : false,
- 'text' => wfMsg('delete'),
- 'href' => $this->mTitle->getLocalUrl( 'action=delete' )
- );
- }
- if ( $this->mTitle->quickUserCan( 'move' ) ) {
- $moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage );
- $content_actions['move'] = array(
- 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false,
- 'text' => wfMsg('move'),
- 'href' => $moveTitle->getLocalUrl()
- );
- }
} else {
//article doesn't exist or is deleted
- if( $wgUser->isAllowed( 'delete' ) ) {
+ if( $wgUser->isAllowed( 'deletedhistory' ) && $wgUser->isAllowed( 'undelete' ) ) {
if( $n = $this->mTitle->isDeleted() ) {
$undelTitle = SpecialPage::getTitleFor( 'Undelete' );
$content_actions['undelete'] = array(
'class' => false,
- 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $n ),
+ 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $wgLang->formatNum($n) ),
'href' => $undelTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
#'href' => self::makeSpecialUrl( "Undelete/$this->thispage" )
);
}
}
+
+ if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) {
+ if( !$this->mTitle->getRestrictions( 'create' ) ) {
+ $content_actions['protect'] = array(
+ 'class' => ($action == 'protect') ? 'selected' : false,
+ 'text' => wfMsg('protect'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=protect' )
+ );
+
+ } else {
+ $content_actions['unprotect'] = array(
+ 'class' => ($action == 'unprotect') ? 'selected' : false,
+ 'text' => wfMsg('unprotect'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=unprotect' )
+ );
+ }
+ }
}
+
wfProfileOut( "$fname-live" );
if( $this->loggedin ) {
@@ -1186,3 +1208,6 @@ class QuickTemplate {
}
}
+
+
+
diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php
index 4ba01e29..ee97b48e 100644
--- a/includes/SpecialAllmessages.php
+++ b/includes/SpecialAllmessages.php
@@ -13,7 +13,7 @@ function wfSpecialAllmessages() {
# The page isn't much use if the MediaWiki namespace is not being used
if( !$wgUseDatabaseMessages ) {
- $wgOut->addWikiText( wfMsg( 'allmessagesnotsupportedDB' ) );
+ $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
return;
}
@@ -44,25 +44,44 @@ function wfSpecialAllmessages() {
wfProfileIn( __METHOD__ . '-output' );
if ( $ot == 'php' ) {
- $navText .= makePhp( $messages );
- $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a><pre>' . htmlspecialchars( $navText ) . '</pre>' );
+ $navText .= wfAllMessagesMakePhp( $messages );
+ $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a> | ' .
+ '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' .
+ '<pre>' . htmlspecialchars( $navText ) . '</pre>' );
+ } else if ( $ot == 'xml' ) {
+ $wgOut->disable();
+ header( 'Content-type: text/xml' );
+ echo wfAllMessagesMakeXml( $messages );
} else {
- $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | HTML' );
+ $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | ' .
+ 'HTML | <a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' );
$wgOut->addWikiText( $navText );
- $wgOut->addHTML( makeHTMLText( $messages ) );
+ $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) );
}
wfProfileOut( __METHOD__ . '-output' );
wfProfileOut( __METHOD__ );
}
+function wfAllMessagesMakeXml( $messages ) {
+ global $wgLang;
+ $lang = $wgLang->getCode();
+ $txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
+ $txt .= "<messages lang=\"$lang\">\n";
+ foreach( $messages as $key => $m ) {
+ $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n";
+ }
+ $txt .= "</messages>";
+ return $txt;
+}
+
/**
* Create the messages array, formatted in PHP to copy to language files.
* @param $messages Messages array.
* @return The PHP messages array.
* @todo Make suitable for language files.
*/
-function makePhp( $messages ) {
+function wfAllMessagesMakePhp( $messages ) {
global $wgLang;
$txt = "\n\n\$messages = array(\n";
foreach( $messages as $key => $m ) {
@@ -85,7 +104,7 @@ function makePhp( $messages ) {
* @param $messages Messages array.
* @return The HTML list of messages.
*/
-function makeHTMLText( $messages ) {
+function wfAllMessagesMakeHTMLText( $messages ) {
global $wgLang, $wgContLang, $wgUser;
wfProfileIn( __METHOD__ );
diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php
index 07ff120b..9f5cf834 100644
--- a/includes/SpecialAllpages.php
+++ b/includes/SpecialAllpages.php
@@ -38,11 +38,21 @@ function wfSpecialAllpages( $par=NULL, $specialPage ) {
* @addtogroup SpecialPage
*/
class SpecialAllpages {
- var $maxPerPage=960;
- var $topLevelMax=50;
- var $name='Allpages';
- # Determines, which message describes the input field 'nsfrom' (->SpecialPrefixindex.php)
- var $nsfromMsg='allpagesfrom';
+ /**
+ * Maximum number of pages to show on single subpage.
+ */
+ protected $maxPerPage = 960;
+
+ /**
+ * Name of this special page. Used to make title objects that reference back
+ * to this page.
+ */
+ protected $name = 'Allpages';
+
+ /**
+ * Determines, which message describes the input field 'nsfrom'.
+ */
+ protected $nsfromMsg = 'allpagesfrom';
/**
* HTML for the top form
@@ -63,7 +73,7 @@ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) .
"</td>
<td>" .
- Xml::input( 'from', 20, htmlspecialchars ( $from ), array( 'id' => 'nsfrom' ) ) .
+ Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) .
"</td>
</tr>
<tr>
@@ -86,7 +96,6 @@ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
*/
function showToplevel ( $namespace = NS_MAIN, $including = false ) {
global $wgOut, $wgContLang;
- $fname = "indexShowToplevel";
$align = $wgContLang->isRtl() ? 'left' : 'right';
# TODO: Either make this *much* faster or cache the title index points
@@ -105,7 +114,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
if ( ! $dbr->implicitOrderby() ) {
$options['ORDER BY'] = 'page_title';
}
- $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, $options );
+ $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
$lastTitle = $firstTitle;
# This array is going to hold the page_titles in order.
@@ -122,7 +131,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
'page', /* FROM */
'page_title', /* WHAT */
$where + array( $chunk),
- $fname,
+ __METHOD__,
array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
if ( $s = $dbr->fetchObject( $res ) ) {
@@ -133,7 +142,7 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
array(
'page_namespace' => $namespace,
$chunk
- ), $fname );
+ ), __METHOD__ );
array_push( $lines, $endTitle );
$done = true;
}
@@ -214,7 +223,6 @@ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
global $wgOut, $wgUser, $wgContLang;
- $fname = 'indexShowChunk';
$sk = $wgUser->getSkin();
$fromList = $this->getNamespaceKeyAndText($namespace, $from);
@@ -239,7 +247,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
'page_namespace' => $namespace,
'page_title >= ' . $dbr->addQuotes( $fromKey )
),
- $fname,
+ __METHOD__,
array(
'ORDER BY' => 'page_title',
'LIMIT' => $this->maxPerPage + 1,
@@ -261,7 +269,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
if( $n % 3 == 0 ) {
$out .= '<tr>';
}
- $out .= "<td>$link</td>";
+ $out .= "<td width=\"33%\">$link</td>";
$n++;
if( $n % 3 == 0 ) {
$out .= '</tr>';
@@ -286,7 +294,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
'page',
'page_title',
array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
- $fname,
+ __METHOD__,
array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
);
@@ -301,7 +309,7 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
if ( ! $dbr->implicitOrderby() ) {
$options['ORDER BY'] = 'page_title';
}
- $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, $options );
+ $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options );
# Show the previous link if it s not the current requested chunk
if( $from != $reallyFirstPage_title ) {
$prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php
index 942ebe8b..cfbef1b3 100644
--- a/includes/SpecialBlockip.php
+++ b/includes/SpecialBlockip.php
@@ -70,19 +70,18 @@ class IPBlockForm {
global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang;
$wgOut->setPagetitle( wfMsg( 'blockip' ) );
- $wgOut->addWikiText( wfMsg( 'blockiptext' ) );
+ $wgOut->addWikiMsg( 'blockiptext' );
if($wgSysopUserBans) {
$mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
} else {
- $mIpaddress = Xml::label( wfMsg( 'ipadress' ), 'mw-bi-target' );
+ $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
}
$mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' );
$mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' );
$mIpbothertime = wfMsgHtml( 'ipbotheroption' );
$mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' );
$mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
- $mIpbreasonotherlist = wfMsgHtml( 'ipbreasonotherlist' );
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
$action = $titleObj->escapeLocalURL( "action=submit" );
@@ -111,38 +110,9 @@ class IPBlockForm {
$blockExpiryFormOptions .= "<option value=\"$value\"$selected>$show</option>";
}
- $scBlockReasonList = wfMsgForContent( 'ipbreason-dropdown' );
- $blockReasonList = '';
- if ( $scBlockReasonList != '' && $scBlockReasonList != '-' ) {
- $blockReasonList = "<option value=\"other\">$mIpbreasonotherlist</option>";
- $optgroup = "";
- foreach ( explode( "\n", $scBlockReasonList ) as $option) {
- $value = trim( htmlspecialchars($option) );
- if ( $value == '' ) {
- continue;
- } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
- // A new group is starting ...
- $value = trim( substr( $value, 1 ) );
- $blockReasonList .= "$optgroup<optgroup label=\"$value\">";
- $optgroup = "</optgroup>";
- } elseif ( substr( $value, 0, 2) == '**' ) {
- // groupmember
- $selected = "";
- $value = trim( substr( $value, 2 ) );
- if ( $this->BlockReasonList === $value)
- $selected = ' selected="selected"';
- $blockReasonList .= "<option value=\"$value\"$selected>$value</option>";
- } else {
- // groupless block reason
- $selected = "";
- if ( $this->BlockReasonList === $value)
- $selected = ' selected="selected"';
- $blockReasonList .= "$optgroup<option value=\"$value\"$selected>$value</option>";
- $optgroup = "";
- }
- }
- $blockReasonList .= $optgroup;
- }
+ $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
+ wfMsgForContent( 'ipbreason-dropdown' ),
+ wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
$token = $wgUser->editToken();
@@ -182,17 +152,13 @@ class IPBlockForm {
array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
</td>
</tr>");
- if ( $blockReasonList != '' ) {
- $wgOut->addHTML("
- <tr>
- <td align=\"$alignRight\">{$mIpbreasonother}</td>
- <td>
- <select tabindex='4' id=\"wpBlockReasonList\" name=\"wpBlockReasonList\">
- $blockReasonList
- </select>
- </td>
- </tr>");
- }
+ $wgOut->addHTML("
+ <tr>
+ <td align=\"$alignRight\">{$mIpbreasonother}</td>
+ <td>
+ $reasonDropDown
+ </td>
+ </tr>");
$wgOut->addHTML("
<tr id=\"wpBlockReason\">
<td align=\"$alignRight\">{$mIpbreason}</td>
@@ -227,34 +193,35 @@ class IPBlockForm {
</td>
</tr>
");
- // Allow some users to hide name from block log, blocklist and listusers
- if ( $wgUser->isAllowed( 'hideuser' ) ) {
+
+ global $wgSysopEmailBans;
+ if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
$wgOut->addHTML("
- <tr>
+ <tr id='wpEnableEmailBan'>
<td>&nbsp;</td>
<td>
- " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
- 'wpHideName', 'wpHideName', $this->BlockHideName,
- array( 'tabindex' => '9' ) ) . "
+ " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
+ 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+ array( 'tabindex' => '10' )) . "
</td>
</tr>
");
}
- global $wgSysopEmailBans;
-
- if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+ // Allow some users to hide name from block log, blocklist and listusers
+ if ( $wgUser->isAllowed( 'hideuser' ) ) {
$wgOut->addHTML("
- <tr id='wpEnableEmailBan'>
+ <tr id='wpEnableHideUser'>
<td>&nbsp;</td>
<td>
- " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
- 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
- array( 'tabindex' => '10' )) . "
+ " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
+ 'wpHideName', 'wpHideName', $this->BlockHideName,
+ array( 'tabindex' => '9' ) ) . "
</td>
</tr>
");
}
+
$wgOut->addHTML("
<tr>
<td style='padding-top: 1em'>&nbsp;</td>
@@ -281,8 +248,14 @@ class IPBlockForm {
}
}
- function doSubmit() {
- global $wgOut, $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
+ /**
+ * Backend block code.
+ * $userID and $expiry will be filled accordingly
+ * @return array(message key, arguments) on failure, empty array on success
+ */
+ function doBlock(&$userId = null, &$expiry = null)
+ {
+ global $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
$userId = 0;
# Expand valid IPv6 addresses, usernames are left as is
@@ -299,27 +272,23 @@ class IPBlockForm {
# IPv4
if ( $wgSysopRangeBans ) {
if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) {
- $this->showForm( wfMsg( 'ip_range_invalid' ) );
- return;
+ return array('ip_range_invalid');
}
$this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
} else {
# Range block illegal
- $this->showForm( wfMsg( 'range_block_disabled' ) );
- return;
+ return array('range_block_disabled');
}
} else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) {
# IPv6
if ( $wgSysopRangeBans ) {
if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) {
- $this->showForm( wfMsg( 'ip_range_invalid' ) );
- return;
+ return array('ip_range_invalid');
}
$this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
} else {
# Range block illegal
- $this->showForm( wfMsg( 'range_block_disabled' ) );
- return;
+ return array('range_block_disabled');
}
} else {
# Username block
@@ -327,15 +296,13 @@ class IPBlockForm {
$user = User::newFromName( $this->BlockAddress );
if( !is_null( $user ) && $user->getID() ) {
# Use canonical name
- $this->BlockAddress = $user->getName();
$userId = $user->getID();
+ $this->BlockAddress = $user->getName();
} else {
- $this->showForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->BlockAddress ) ) );
- return;
+ return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) );
}
} else {
- $this->showForm( wfMsg( 'badipaddress' ) );
- return;
+ return array('badipaddress');
}
}
}
@@ -353,8 +320,7 @@ class IPBlockForm {
$expirestr = $this->BlockOther;
if (strlen($expirestr) == 0) {
- $this->showForm( wfMsg( 'ipb_expiry_invalid' ) );
- return;
+ return array('ipb_expiry_invalid');
}
if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) {
@@ -364,8 +330,7 @@ class IPBlockForm {
$expiry = strtotime( $expirestr );
if ( $expiry < 0 || $expiry === false ) {
- $this->showForm( wfMsg( 'ipb_expiry_invalid' ) );
- return;
+ return array('ipb_expiry_invalid');
}
$expiry = wfTimestamp( TS_MW, $expiry );
@@ -381,9 +346,7 @@ class IPBlockForm {
if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) {
if ( !$block->insert() ) {
- $this->showForm( wfMsg( 'ipb_already_blocked',
- htmlspecialchars( $this->BlockAddress ) ) );
- return;
+ return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
}
wfRunHooks('BlockIpComplete', array($block, $wgUser));
@@ -400,10 +363,28 @@ class IPBlockForm {
$reasonstr, $logParams );
# Report to the user
+ return array();
+ }
+ else
+ return array('hookaborted');
+ }
+
+ /**
+ * UI entry point for blocking
+ * Wraps around doBlock()
+ */
+ function doSubmit()
+ {
+ global $wgOut;
+ $retval = $this->doBlock();
+ if(empty($retval)) {
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
$wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
urlencode( $this->BlockAddress ) ) );
+ return;
}
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
}
function showSuccess() {
@@ -411,8 +392,8 @@ class IPBlockForm {
$wgOut->setPagetitle( wfMsg( 'blockip' ) );
$wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
- $text = wfMsg( 'blockipsuccesstext', $this->BlockAddress );
- $wgOut->addWikiText( $text );
+ $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
+ $wgOut->addHtml( $text );
}
function showLogFragment( $out, $title ) {
@@ -450,7 +431,7 @@ class IPBlockForm {
private function getConvenienceLinks() {
global $wgUser;
$skin = $wgUser->getSkin();
- $links[] = $skin->makeLink ( 'MediaWiki:ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+ $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
$links[] = $this->getUnblockLink( $skin );
$links[] = $this->getBlockListLink( $skin );
return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php
index da2757ac..6c9dea06 100644
--- a/includes/SpecialBlockme.php
+++ b/includes/SpecialBlockme.php
@@ -13,13 +13,12 @@ function wfSpecialBlockme() {
$ip = wfGetIP();
if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
- $wgOut->addWikiText( wfMsg( 'disabled' ) );
+ $wgOut->addWikiMsg( 'proxyblocker-disabled' );
return;
}
$blockerName = wfMsg( "proxyblocker" );
$reason = wfMsg( "proxyblockreason" );
- $success = wfMsg( "proxyblocksuccess" );
$u = User::newFromName( $blockerName );
$id = $u->idForName();
@@ -34,6 +33,6 @@ function wfSpecialBlockme() {
$block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
$block->insert();
- $wgOut->addWikiText( $success );
+ $wgOut->addWikiMsg( "proxyblocksuccess" );
}
diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php
index 5f103495..af258872 100644
--- a/includes/SpecialBooksources.php
+++ b/includes/SpecialBooksources.php
@@ -31,7 +31,7 @@ class SpecialBookSources extends SpecialPage {
global $wgOut, $wgRequest;
$this->setHeaders();
$this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
- $wgOut->addWikiText( wfMsgNoTrans( 'booksources-summary' ) );
+ $wgOut->addWikiMsg( 'booksources-summary' );
$wgOut->addHtml( $this->makeForm() );
if( strlen( $this->isbn ) > 0 )
$this->showList();
@@ -87,7 +87,7 @@ class SpecialBookSources extends SpecialPage {
}
# Fall back to the defaults given in the language file
- $wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) );
+ $wgOut->addWikiMsg( 'booksources-text' );
$wgOut->addHtml( '<ul>' );
$items = $wgContLang->getBookstoreList();
foreach( $items as $label => $url )
diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php
index 1fb48350..f6887741 100644
--- a/includes/SpecialBrokenRedirects.php
+++ b/includes/SpecialBrokenRedirects.php
@@ -51,7 +51,7 @@ class BrokenRedirectsPage extends PageQueryPage {
if ( isset( $result->rd_title ) ) {
$toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
} else {
- $blinks = $fromObj->getBrokenLinksFrom();
+ $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
if ( $blinks ) {
$toObj = $blinks[0];
} else {
diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php
index 596569ed..efe65a78 100644
--- a/includes/SpecialCategories.php
+++ b/includes/SpecialCategories.php
@@ -9,7 +9,7 @@ function wfSpecialCategories() {
$cap = new CategoryPager();
$wgOut->addHTML(
- wfMsgWikiHtml( 'categoriespagetext' ) .
+ wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
$cap->getNavigationBar()
. '<ul>' . $cap->getBody() . '</ul>' .
$cap->getNavigationBar()
@@ -54,7 +54,7 @@ class CategoryPager extends AlphabeticPager {
global $wgLang;
$title = Title::makeTitle( NS_CATEGORY, $result->cl_to );
$titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
- $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
$wgLang->formatNum( $result->count ) );
return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
}
diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php
index ba419f25..c3aa53c2 100644
--- a/includes/SpecialConfirmemail.php
+++ b/includes/SpecialConfirmemail.php
@@ -30,7 +30,7 @@ class EmailConfirmation extends UnlistedSpecialPage {
if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
$this->showRequestForm();
} else {
- $wgOut->addWikiText( wfMsg( 'confirmemail_noemail' ) );
+ $wgOut->addWikiMsg( 'confirmemail_noemail' );
}
} else {
$title = SpecialPage::getTitleFor( 'Userlogin' );
@@ -52,19 +52,19 @@ class EmailConfirmation extends UnlistedSpecialPage {
if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
$ok = $wgUser->sendConfirmationMail();
if ( WikiError::isError( $ok ) ) {
- $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $ok->toString() ) );
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() );
} else {
- $wgOut->addWikiText( wfMsg( 'confirmemail_sent' ) );
+ $wgOut->addWikiMsg( 'confirmemail_sent' );
}
} else {
if( $wgUser->isEmailConfirmed() ) {
$time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
- $wgOut->addWikiText( wfMsg( 'emailauthenticated', $time ) );
+ $wgOut->addWikiMsg( 'emailauthenticated', $time );
}
if( $wgUser->isEmailConfirmationPending() ) {
- $wgOut->addWikiText( wfMsg( 'confirmemail_pending' ) );
+ $wgOut->addWikiMsg( 'confirmemail_pending' );
}
- $wgOut->addWikiText( wfMsg( 'confirmemail_text' ) );
+ $wgOut->addWikiMsg( 'confirmemail_text' );
$self = SpecialPage::getTitleFor( 'Confirmemail' );
$form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
$form .= wfHidden( 'token', $wgUser->editToken() );
@@ -86,16 +86,16 @@ class EmailConfirmation extends UnlistedSpecialPage {
if( is_object( $user ) ) {
if( $user->confirmEmail() ) {
$message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
- $wgOut->addWikiText( wfMsg( $message ) );
+ $wgOut->addWikiMsg( $message );
if( !$wgUser->isLoggedIn() ) {
$title = SpecialPage::getTitleFor( 'Userlogin' );
$wgOut->returnToMain( true, $title->getPrefixedText() );
}
} else {
- $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) );
+ $wgOut->addWikiMsg( 'confirmemail_error' );
}
} else {
- $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) );
+ $wgOut->addWikiMsg( 'confirmemail_invalid' );
}
}
diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php
index cc1b2e6f..6bed7905 100644
--- a/includes/SpecialContributions.php
+++ b/includes/SpecialContributions.php
@@ -4,7 +4,7 @@
* @addtogroup SpecialPage
*/
-class ContribsPager extends IndexPager {
+class ContribsPager extends ReverseChronologicalPager {
public $mDefaultDirection = true;
var $messages, $target;
var $namespace = '', $mDb;
@@ -110,33 +110,13 @@ class ContribsPager extends IndexPager {
return "</ul>\n";
}
- function getNavigationBar() {
- if ( isset( $this->mNavigationBar ) ) {
- return $this->mNavigationBar;
- }
- $linkTexts = array(
- 'prev' => wfMsgHtml( "sp-contributions-newer", $this->mLimit ),
- 'next' => wfMsgHtml( 'sp-contributions-older', $this->mLimit ),
- 'first' => wfMsgHtml('sp-contributions-newest'),
- 'last' => wfMsgHtml( 'sp-contributions-oldest' )
- );
-
- $pagingLinks = $this->getPagingLinks( $linkTexts );
- $limitLinks = $this->getLimitLinks();
- $limits = implode( ' | ', $limitLinks );
-
- $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " .
- wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
- return $this->mNavigationBar;
- }
-
/**
* Generates each row in the contributions list.
*
* Contributions which are marked "top" are currently on top of the history.
- * For these contributions, a [rollback] link is shown for users with sysop
- * privileges. The rollback link restores the most recent version that was not
- * written by the target user.
+ * For these contributions, a [rollback] link is shown for users with roll-
+ * back privileges. The rollback link restores the most recent version that
+ * was not written by the target user.
*
* @todo This would probably look a lot nicer in a table.
*/
@@ -159,7 +139,8 @@ class ContribsPager extends IndexPager {
$difftext .= $this->messages['newarticle'];
}
- if( $wgUser->isAllowed( 'rollback' ) ) {
+ if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
+ && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
$topmarktext .= ' '.$sk->generateRollback( $rev );
}
@@ -265,7 +246,7 @@ function wfSpecialContributions( $par = null ) {
} else {
$options['namespace'] = '';
}
- if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) {
+ if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) {
$options['bot'] = '1';
}
@@ -302,7 +283,7 @@ function wfSpecialContributions( $par = null ) {
$pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
if ( !$pager->getNumRows() ) {
- $wgOut->addWikiText( wfMsg( 'nocontribs' ) );
+ $wgOut->addWikiMsg( 'nocontribs' );
return;
}
@@ -323,7 +304,7 @@ function wfSpecialContributions( $par = null ) {
: 'sp-contributions-footer';
- $text = wfMsg( $message, $target );
+ $text = wfMsgNoTrans( $message, $target );
if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
$wgOut->addHtml( '<div class="mw-contributions-footer">' );
$wgOut->addWikiText( $text );
@@ -426,13 +407,20 @@ function contributionsForm( $options ) {
Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' .
Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' .
Xml::input( 'target', 20, $options['target']) . ' '.
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
Xml::namespaceSelector( $options['namespace'], '' ) .
+ '</span>' .
Xml::openElement( 'p' ) .
+ '<span style="white-space: nowrap">' .
Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
- Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) . ' '.
+ Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) .
+ '</span>' .
+ ' '.
+ '<span style="white-space: nowrap">' .
Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
Xml::monthSelector( $options['month'], -1 ) . ' '.
+ '</span>' .
Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
Xml::closeElement( 'p' );
diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php
index 6d46fc50..7e4ec360 100644
--- a/includes/SpecialDoubleRedirects.php
+++ b/includes/SpecialDoubleRedirects.php
@@ -63,7 +63,6 @@ class DoubleRedirectsPage extends PageQueryPage {
$fname = 'DoubleRedirectsPage::formatResult';
$titleA = Title::makeTitle( $result->namespace, $result->title );
- $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' );
if ( $result && !isset( $result->nsb ) ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -75,12 +74,13 @@ class DoubleRedirectsPage extends PageQueryPage {
}
}
if ( !$result ) {
- return "<s>{$linkA}</s>\n";
+ return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>';
}
$titleB = Title::makeTitle( $result->nsb, $result->tb );
$titleC = Title::makeTitle( $result->nsc, $result->tc );
+ $linkA = $skin->makeKnownLinkObj( $titleA, '', 'redirect=no' );
$edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
$linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
$linkC = $skin->makeKnownLinkObj( $titleC );
diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php
index 1d5a9647..7de89dce 100644
--- a/includes/SpecialEmailuser.php
+++ b/includes/SpecialEmailuser.php
@@ -4,8 +4,6 @@
* @addtogroup SpecialPage
*/
-require_once('UserMailer.php');
-
/**
* @todo document
*/
@@ -96,7 +94,7 @@ class EmailUserForm {
global $wgOut, $wgUser;
$wgOut->setPagetitle( wfMsg( "emailpage" ) );
- $wgOut->addWikiText( wfMsg( "emailpagetext" ) );
+ $wgOut->addWikiMsg( "emailpagetext" );
if ( $this->subject === "" ) {
$this->subject = wfMsg( "defemailsubject" );
@@ -133,7 +131,7 @@ class EmailUserForm {
</tr>
</table>
<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
-<textarea name=\"wpText\" rows='20' cols='80' wrap='virtual' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
+<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
"</textarea>
" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br />
<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
@@ -143,18 +141,47 @@ class EmailUserForm {
}
function doSubmit() {
- global $wgOut, $wgUser;
+ global $wgOut, $wgUser, $wgUserEmailUseReplyTo;
$to = new MailAddress( $this->target );
$from = new MailAddress( $wgUser );
$subject = $this->subject;
if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
+
+ if( $wgUserEmailUseReplyTo ) {
+ // Put the generic wiki autogenerated address in the From:
+ // header and reserve the user for Reply-To.
+ //
+ // This is a bit ugly, but will serve to differentiate
+ // wiki-borne mails from direct mails and protects against
+ // SPF and bounce problems with some mailers (see below).
+ global $wgPasswordSender;
+ $mailFrom = new MailAddress( $wgPasswordSender );
+ $replyTo = $from;
+ } else {
+ // Put the sending user's e-mail address in the From: header.
+ //
+ // This is clean-looking and convenient, but has issues.
+ // One is that it doesn't as clearly differentiate the wiki mail
+ // from "directly" sent mails.
+ //
+ // Another is that some mailers (like sSMTP) will use the From
+ // address as the envelope sender as well. For open sites this
+ // can cause mails to be flunked for SPF violations (since the
+ // wiki server isn't an authorized sender for various users'
+ // domains) as well as creating a privacy issue as bounces
+ // containing the recipient's e-mail address may get sent to
+ // the sending user.
+ $mailFrom = $from;
+ $replyTo = null;
+ }
- $mailResult = userMailer( $to, $from, $subject, $this->text );
+ $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo );
if( WikiError::isError( $mailResult ) ) {
- $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult);
+ $wgOut->addHTML( wfMsg( "usermailererror" ) .
+ ' ' . htmlspecialchars( $mailResult->getMessage() ) );
} else {
// if the user requested a copy of this mail, do this now,
@@ -162,14 +189,15 @@ class EmailUserForm {
if ($this->cc_me && $to != $from) {
$cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject);
if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) {
- $ccResult = userMailer( $from, $from, $cc_subject, $this->text );
+ $ccResult = UserMailer::send( $from, $from, $cc_subject, $this->text );
if( WikiError::isError( $ccResult ) ) {
// At this stage, the user's CC mail has failed, but their
// original mail has succeeded. It's unlikely, but still, what to do?
// We can either show them an error, or we can say everything was fine,
// or we can say we sort of failed AND sort of succeeded. Of these options,
// simply saying there was an error is probably best.
- $wgOut->addHTML( wfMsg( "usermailererror" ) . $ccResult);
+ $wgOut->addHTML( wfMsg( "usermailererror" ) .
+ ' ' . htmlspecialchars( $ccResult->getMessage() ) );
return;
}
}
@@ -192,4 +220,3 @@ class EmailUserForm {
$wgOut->returnToMain( false, $user->getUserPage() );
}
}
-
diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php
index 12bd4d5c..1fe2e44b 100644
--- a/includes/SpecialExport.php
+++ b/includes/SpecialExport.php
@@ -24,7 +24,7 @@
function wfExportGetPagesFromCategory( $title ) {
global $wgContLang;
- $name = $title->getDBKey();
+ $name = $title->getDBkey();
$dbr = wfGetDB( DB_SLAVE );
@@ -50,6 +50,68 @@ function wfExportGetPagesFromCategory( $title ) {
}
/**
+ * Expand a list of pages to include templates used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetTemplates( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'templatelinks',
+ array( 'tl_namespace AS namespace', 'tl_title AS title' ),
+ array( 'page_id=tl_from' ) );
+}
+
+/**
+ * Expand a list of pages to include images used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetImages( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'imagelinks',
+ array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
+ array( 'page_id=il_from' ) );
+}
+
+/**
+ * Expand a list of pages to include items used in those pages.
+ * @private
+ */
+function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ foreach( $inputPages as $page ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $pageSet[$title->getPrefixedText()] = true;
+ /// @fixme May or may not be more efficient to batch these
+ /// by namespace when given multiple input pages.
+ $result = $dbr->select(
+ array( 'page', $table ),
+ $fields,
+ array_merge( $join,
+ array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbKey() ) ),
+ __METHOD__ );
+ foreach( $result as $row ) {
+ $template = Title::makeTitle( $row->namespace, $row->title );
+ $pageSet[$template->getPrefixedText()] = true;
+ }
+ }
+ }
+ return $pageSet;
+}
+
+/**
+ * Callback function to remove empty strings from the pages array.
+ */
+function wfFilterPage( $page ) {
+ return $page !== '' && $page !== null;
+}
+
+/**
*
*/
function wfSpecialExport( $page = '' ) {
@@ -66,6 +128,11 @@ function wfSpecialExport( $page = '' ) {
if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
$t = Title::makeTitleSafe( NS_CATEGORY, $catname );
if ( $t ) {
+ /**
+ * @fixme This can lead to hitting memory limit for very large
+ * categories. Ideally we would do the lookup synchronously
+ * during the export in a single query.
+ */
$catpages = wfExportGetPagesFromCategory( $t );
if ( $catpages ) $page .= "\n" . implode( "\n", $catpages );
}
@@ -123,7 +190,7 @@ function wfSpecialExport( $page = '' ) {
$list_authors = $wgRequest->getCheck( 'listauthors' );
if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
-
+
if ( $doexport ) {
$wgOut->disable();
@@ -136,7 +203,25 @@ function wfSpecialExport( $page = '' ) {
$filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
$wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
}
- $pages = explode( "\n", $page );
+
+ /* Split up the input and look up linked pages */
+ $inputPages = array_filter( explode( "\n", $page ), 'wfFilterPage' );
+ $pageSet = array_flip( $inputPages );
+
+ if( $wgRequest->getCheck( 'templates' ) ) {
+ $pageSet = wfExportGetTemplates( $inputPages, $pageSet );
+ }
+
+ /*
+ // Enable this when we can do something useful exporting/importing image information. :)
+ if( $wgRequest->getCheck( 'images' ) ) {
+ $pageSet = wfExportGetImages( $inputPages, $pageSet );
+ }
+ */
+
+ $pages = array_keys( $pageSet );
+
+ /* Ok, let's get to it... */
$db = wfGetDB( DB_SLAVE );
$exporter = new WikiExporter( $db, $history );
@@ -160,7 +245,7 @@ function wfSpecialExport( $page = '' ) {
#Bug 8824: Only export pages the user can read
$title = Title::newFromText( $page );
if( is_null( $title ) ) continue; #TODO: perhaps output an <error> tag or something.
- if( !$title->userCan( 'read' ) ) continue; #TODO: perhaps output an <error> tag or something.
+ if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something.
$exporter->pageByTitle( $title );
}
@@ -188,6 +273,9 @@ function wfSpecialExport( $page = '' ) {
} else {
$wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
}
+ $form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />';
+ // Enable this when we can do something useful exporting/importing image information. :)
+ //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
$form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
$form .= Xml::submitButton( wfMsg( 'export-submit' ) );
diff --git a/includes/SpecialFilepath.php b/includes/SpecialFilepath.php
new file mode 100644
index 00000000..4ba8fdb0
--- /dev/null
+++ b/includes/SpecialFilepath.php
@@ -0,0 +1,69 @@
+<?php
+
+function wfSpecialFilepath( $par ) {
+ global $wgRequest, $wgOut;
+
+ $file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
+
+ $title = Title::newFromText( $file, NS_IMAGE );
+
+ if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ } else {
+ $file = wfFindFile( $title );
+ if ( $file && $file->exists() ) {
+ $wgOut->redirect( $file->getURL() );
+ } else {
+ $wgOut->setStatusCode( 404 );
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ }
+ }
+}
+
+class FilepathForm {
+ var $mTitle;
+
+ function FilepathForm( &$title ) {
+ $this->mTitle =& $title;
+ }
+
+ function execute() {
+ global $wgOut, $wgTitle, $wgScript;
+
+ $wgOut->addHTML(
+ wfElement( 'form',
+ array(
+ 'id' => 'specialfilepath',
+ 'method' => 'get',
+ 'action' => $wgScript,
+ ),
+ null
+ ) .
+ wfHidden( 'title', $wgTitle->getPrefixedText() ) .
+ wfOpenElement( 'label' ) .
+ wfMsgHtml( 'filepath-page' ) .
+ ' ' .
+ wfElement( 'input',
+ array(
+ 'type' => 'text',
+ 'size' => 25,
+ 'name' => 'file',
+ 'value' => is_object( $this->mTitle ) ? $this->mTitle->getText() : ''
+ ),
+ ''
+ ) .
+ ' ' .
+ wfElement( 'input',
+ array(
+ 'type' => 'submit',
+ 'value' => wfMsgHtml( 'filepath-submit' )
+ ),
+ ''
+ ) .
+ wfCloseElement( 'label' ) .
+ wfCloseElement( 'form' )
+ );
+ }
+}
diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php
index ad5d8e64..7a2e6221 100644
--- a/includes/SpecialImport.php
+++ b/includes/SpecialImport.php
@@ -34,6 +34,11 @@ function wfSpecialImport( $page = '' ) {
$frompage = '';
$history = true;
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
$isUpload = false;
$namespace = $wgRequest->getIntOrNull( 'namespace' );
@@ -61,9 +66,9 @@ function wfSpecialImport( $page = '' ) {
}
if( WikiError::isError( $source ) ) {
- $wgOut->addWikiText( wfEscapeWikiText( $source->getMessage() ) );
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) );
} else {
- $wgOut->addWikiText( wfMsg( "importstart" ) );
+ $wgOut->addWikiMsg( "importstart" );
$importer = new WikiImporter( $source );
if( !is_null( $namespace ) ) {
@@ -73,88 +78,93 @@ function wfSpecialImport( $page = '' ) {
$reporter->open();
$result = $importer->doImport();
- $reporter->close();
+ $resultCount = $reporter->close();
if( WikiError::isError( $result ) ) {
- $wgOut->addWikiText( wfMsg( "importfailed",
- wfEscapeWikiText( $result->getMessage() ) ) );
+ # No source or XML parse error
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $result->getMessage() ) );
+ } elseif( WikiError::isError( $resultCount ) ) {
+ # Zero revisions
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $resultCount->getMessage() ) );
} else {
# Success!
- $wgOut->addWikiText( wfMsg( "importsuccess" ) );
+ $wgOut->addWikiMsg( 'importsuccess' );
}
+ $wgOut->addWikiText( '<hr />' );
}
}
$action = $wgTitle->getLocalUrl( 'action=submit' );
if( $wgUser->isAllowed( 'importupload' ) ) {
- $wgOut->addWikiText( wfMsg( "importtext" ) );
- $wgOut->addHTML( "
-<fieldset>
- <legend>" . wfMsgHtml('upload') . "</legend>
- <form enctype='multipart/form-data' method='post' action=\"$action\">
- <input type='hidden' name='action' value='submit' />
- <input type='hidden' name='source' value='upload' />
- <input type='hidden' name='MAX_FILE_SIZE' value='2000000' />
- <input type='file' name='xmlimport' value='' size='30' />
- <input type='submit' value=\"" . wfMsgHtml( "uploadbtn" ) . "\" />
- </form>
-</fieldset>
-" );
+ $wgOut->addWikiMsg( "importtext" );
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ).
+ Xml::element( 'legend', null, wfMsg( 'upload' ) ) .
+ Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'upload' ) .
+ "<input type='file' name='xmlimport' value='' size='30' />" . // No Xml function for type=file? Todo?
+ Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
} else {
if( empty( $wgImportSources ) ) {
- $wgOut->addWikiText( wfMsg( 'importnosources' ) );
+ $wgOut->addWikiMsg( 'importnosources' );
}
}
if( !empty( $wgImportSources ) ) {
- $wgOut->addHTML( "
-<fieldset>
- <legend>" . wfMsgHtml('importinterwiki') . "</legend>
- <form method='post' action=\"$action\">" .
- $wgOut->parse( wfMsg( 'import-interwiki-text' ) ) . "
- <input type='hidden' name='action' value='submit' />
- <input type='hidden' name='source' value='interwiki' />
- <table>
- <tr>
- <td>
- <select name='interwiki'>" );
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
+ wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'interwiki' ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td>" .
+ Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
+ );
foreach( $wgImportSources as $prefix ) {
- $iw = htmlspecialchars( $prefix );
- $selected = ($interwiki === $prefix) ? ' selected="selected"' : '';
- $wgOut->addHTML( "<option value=\"$iw\"$selected>$iw</option>\n" );
+ $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : '';
+ $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
}
- $wgOut->addHTML( "
- </select>
- </td>
+ $wgOut->addHTML(
+ Xml::closeElement( 'select' ) .
+ "</td>
<td>" .
- wfInput( 'frompage', 50, $frompage ) .
+ Xml::input( 'frompage', 50, $frompage ) .
"</td>
</tr>
<tr>
- <td></td>
+ <td>
+ </td>
<td>" .
- wfCheckLabel( wfMsg( 'import-interwiki-history' ),
- 'interwikiHistory', 'interwikiHistory', $history ) .
+ Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) .
"</td>
</tr>
<tr>
- <td></td>
<td>
- " . wfMsgHtml( 'import-interwiki-namespace' ) . " " .
- HTMLnamespaceselector( $namespace, '' ) . "
</td>
+ <td>" .
+ Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
+ Xml::namespaceSelector( $namespace, '' ) .
+ "</td>
</tr>
<tr>
- <td></td>
+ <td>
+ </td>
<td>" .
- wfSubmitButton( wfMsg( 'import-interwiki-submit' ) ) .
+ Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) .
"</td>
- </tr>
- </table>
- </form>
-</fieldset>
-" );
+ </tr>" .
+ Xml::closeElement( 'table' ).
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
}
}
@@ -185,21 +195,21 @@ class ImportReporter {
$localCount = $wgLang->formatNum( $successCount );
$contentCount = $wgContLang->formatNum( $successCount );
- $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) .
- " " .
- wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
- "</li>\n" );
-
if( $successCount > 0 ) {
+ $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+ wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
+ "</li>\n"
+ );
+
$log = new LogPage( 'import' );
if( $this->mIsUpload ) {
- $detail = wfMsgForContent( 'import-logentry-upload-detail',
+ $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
$contentCount );
$log->addEntry( 'upload', $title, $detail );
} else {
$interwiki = '[[:' . $this->mInterwiki . ':' .
$origTitle->getPrefixedText() . ']]';
- $detail = wfMsgForContent( 'import-logentry-interwiki-detail',
+ $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
$contentCount, $interwiki );
$log->addEntry( 'interwiki', $title, $detail );
}
@@ -212,15 +222,20 @@ class ImportReporter {
# Update page record
$article = new Article( $title );
$article->updateRevisionOn( $dbw, $nullRevision );
+ } else {
+ $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
}
}
function close() {
global $wgOut;
if( $this->mPageCount == 0 ) {
- $wgOut->addHtml( "<li>" . wfMsgHtml( 'importnopages' ) . "</li>\n" );
+ $wgOut->addHtml( "</ul>\n" );
+ return new WikiErrorMsg( "importnopages" );
}
$wgOut->addHtml( "</ul>\n" );
+
+ return $this->mPageCount;
}
}
@@ -428,7 +443,7 @@ class WikiImporter {
$chunk = $this->mSource->readChunk();
if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
- return new WikiXmlError( $parser, 'XML import parse failure', $chunk, $offset );
+ return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
}
$offset += strlen( $chunk );
} while( $chunk !== false && !$this->mSource->atEnd() );
@@ -683,7 +698,7 @@ class WikiImporter {
$this->origTitle = Title::newFromText( $this->workTitle );
if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
$this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
- $this->origTitle->getDbKey() );
+ $this->origTitle->getDBkey() );
} else {
$this->pageTitle = Title::newFromText( $this->workTitle );
}
@@ -850,7 +865,18 @@ class ImportStreamSource {
return new WikiErrorMsg( 'importnofile' );
}
if( !empty( $upload['error'] ) ) {
- return new WikiErrorMsg( 'importuploaderror', $upload['error'] );
+ switch($upload['error']){
+ case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 3: # The uploaded file was only partially uploaded
+ return new WikiErrorMsg( 'importuploaderrorpartial' );
+ case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
+ return new WikiErrorMsg( 'importuploaderrortemp' );
+ # case else: # Currently impossible
+ }
+
}
$fname = $upload['tmp_name'];
if( is_uploaded_file( $fname ) ) {
@@ -879,6 +905,9 @@ class ImportStreamSource {
}
public static function newFromInterwiki( $interwiki, $page, $history=false ) {
+ if( $page == '' ) {
+ return new WikiErrorMsg( 'import-noarticle' );
+ }
$link = Title::newFromText( "$interwiki:Special:Export/$page" );
if( is_null( $link ) || $link->getInterwiki() == '' ) {
return new WikiErrorMsg( 'importbadinterwiki' );
@@ -890,6 +919,3 @@ class ImportStreamSource {
}
}
}
-
-
-
diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php
index 4f093dcb..c2de9e2f 100644
--- a/includes/SpecialIpblocklist.php
+++ b/includes/SpecialIpblocklist.php
@@ -80,7 +80,7 @@ class IPUnblockForm {
global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang;
$wgOut->setPagetitle( wfMsg( 'unblockip' ) );
- $wgOut->addWikiText( wfMsg( 'unblockiptext' ) );
+ $wgOut->addWikiMsg( 'unblockiptext' );
$ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
$ipr = wfMsgHtml( 'ipbreason' );
@@ -91,7 +91,7 @@ class IPUnblockForm {
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsg( "formerror" ) );
- $wgOut->addWikitext( "<span class='error'>{$err}</span>\n" );
+ $wgOut->addWikiText( "<span class='error'>{$err}</span>\n" );
}
$token = htmlspecialchars( $wgUser->editToken() );
@@ -140,49 +140,78 @@ class IPUnblockForm {
}
- function doSubmit() {
- global $wgOut;
+ const UNBLOCK_SUCCESS = 0; // Success
+ const UNBLOCK_NO_SUCH_ID = 1; // No such block ID
+ const UNBLOCK_USER_NOT_BLOCKED = 2; // IP wasn't blocked
+ const UNBLOCK_BLOCKED_AS_RANGE = 3; // IP is part of a range block
+ const UNBLOCK_UNKNOWNERR = 4; // Unknown error
- if ( $this->id ) {
- $block = Block::newFromID( $this->id );
- if ( $block ) {
- $this->ip = $block->getRedactedName();
+ /**
+ * Backend code for unblocking. doSubmit() wraps around this.
+ * $range is only used when UNBLOCK_BLOCKED_AS_RANGE is returned, in which
+ * case it contains the range $ip is part of.
+ * @return array array(message key, parameters) on failure, empty array on success
+ */
+
+ static function doUnblock(&$id, &$ip, &$reason, &$range = null)
+ {
+ if ( $id ) {
+ $block = Block::newFromID( $id );
+ if ( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
}
+ $ip = $block->getRedactedName();
} else {
$block = new Block();
- $this->ip = trim( $this->ip );
- if ( substr( $this->ip, 0, 1 ) == "#" ) {
- $id = substr( $this->ip, 1 );
+ $ip = trim( $ip );
+ if ( substr( $ip, 0, 1 ) == "#" ) {
+ $id = substr( $ip, 1 );
$block = Block::newFromID( $id );
+ if( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ $ip = $block->getRedactedName();
} else {
- $block = Block::newFromDB( $this->ip );
+ $block = Block::newFromDB( $ip );
if ( !$block ) {
- $block = null;
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ if( $block->mRangeStart != $block->mRangeEnd
+ && !strstr( $ip, "/" ) ) {
+ /* If the specified IP is a single address, and the block is
+ * a range block, don't unblock the range. */
+ $range = $block->mAddress;
+ return array('ipb_blocked_as_range', $ip, $range);
}
}
}
- $success = false;
- if ( $block ) {
- # Delete block
- if ( $block->delete() ) {
- # Make log entry
- $log = new LogPage( 'block' );
- $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason );
- $success = true;
- }
+ // Yes, this is really necessary
+ $id = $block->mId;
+
+ # Delete block
+ if ( !$block->delete() ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
}
- if ( $success ) {
- # Report to the user
- $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
- $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
- $wgOut->redirect( $success );
- } else {
- if ( !$this->ip && $this->id ) {
- $this->ip = '#' . $this->id;
- }
- $this->showForm( wfMsg( 'ipb_cant_unblock', htmlspecialchars( $this->id ) ) );
+ # Make log entry
+ $log = new LogPage( 'block' );
+ $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $ip ), $reason );
+ return array();
+ }
+
+ function doSubmit() {
+ global $wgOut;
+ $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range);
+ if(!empty($retval))
+ {
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
+ return;
}
+ # Report to the user
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
+ $wgOut->redirect( $success );
}
function showList( $msg ) {
@@ -234,9 +263,9 @@ class IPUnblockForm {
);
} elseif ( $this->ip != '') {
$wgOut->addHTML( $this->searchForm() );
- $wgOut->addWikiText( wfMsg( 'ipblocklist-no-results' ) );
+ $wgOut->addWikiMsg( 'ipblocklist-no-results' );
} else {
- $wgOut->addWikiText( wfMsg( 'ipblocklist-empty' ) );
+ $wgOut->addWikiMsg( 'ipblocklist-empty' );
}
}
@@ -268,13 +297,12 @@ class IPUnblockForm {
$sk = $wgUser->getSkin();
if( is_null( $msg ) ) {
$msg = array();
- $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink',
+ $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
foreach( $keys as $key ) {
$msg[$key] = wfMsgHtml( $key );
}
$msg['blocklistline'] = wfMsg( 'blocklistline' );
- $msg['contribslink'] = wfMsg( 'contribslink' );
}
# Prepare links to the blocker's user and talk pages
diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php
index 581ea55b..92bd66e4 100644
--- a/includes/SpecialListredirects.php
+++ b/includes/SpecialListredirects.php
@@ -30,8 +30,7 @@ class ListredirectsPage extends QueryPage {
# Make a link to the redirect itself
$rd_title = Title::makeTitle( $result->namespace, $result->title );
- $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
- $rd_link = $skin->makeKnownLinkObj( $rd_title, '', 'redirect=no' );
+ $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
# Find out where the redirect leads
$revision = Revision::newFromTitle( $rd_title );
@@ -39,19 +38,15 @@ class ListredirectsPage extends QueryPage {
# Make a link to the destination page
$target = Title::newFromRedirect( $revision->getText() );
if( $target ) {
+ $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
$targetLink = $skin->makeLinkObj( $target );
+ return "$rd_link $arr $targetLink";
} else {
- /** @todo Put in some decent error display here */
- $targetLink = '*';
+ return "<s>$rd_link</s>";
}
} else {
- /** @todo Put in some decent error display here */
- $targetLink = '*';
+ return "<s>$rd_link</s>";
}
-
- # Format the whole thing and return it
- return "$rd_link $arr $targetLink";
-
}
}
diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php
index e57717e2..b523591c 100644
--- a/includes/SpecialLockdb.php
+++ b/includes/SpecialLockdb.php
@@ -51,7 +51,7 @@ class DBLockForm {
global $wgOut, $wgUser;
$wgOut->setPagetitle( wfMsg( 'lockdb' ) );
- $wgOut->addWikiText( wfMsg( 'lockdbtext' ) );
+ $wgOut->addWikiMsg( 'lockdbtext' );
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsg( 'formerror' ) );
@@ -121,7 +121,7 @@ END
$wgOut->setPagetitle( wfMsg( 'lockdb' ) );
$wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
- $wgOut->addWikiText( wfMsg( 'lockdbsuccesstext' ) );
+ $wgOut->addWikiMsg( 'lockdbsuccesstext' );
}
public static function notWritable() {
diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php
index f0794eb5..5c28340f 100644
--- a/includes/SpecialLog.php
+++ b/includes/SpecialLog.php
@@ -123,6 +123,7 @@ class LogReader {
*/
function limitTitle( $page , $pattern ) {
global $wgMiserMode;
+
$title = Title::newFromText( $page );
if( strlen( $page ) == 0 || !$title instanceof Title )
@@ -182,7 +183,7 @@ class LogReader {
* @return ResultWrapper result object to return the relevant rows
*/
function getRows() {
- $res = $this->db->query( $this->getQuery(), 'LogReader::getRows' );
+ $res = $this->db->query( $this->getQuery(), __METHOD__ );
return $this->db->resultObject( $res );
}
@@ -341,7 +342,7 @@ class LogViewer {
}
function showError( &$out ) {
- $out->addWikiText( wfMsg( 'logempty' ) );
+ $out->addWikiMsg( 'logempty' );
}
/**
@@ -370,7 +371,7 @@ class LogViewer {
$revert = '';
// show revertmove link
if ( !( $this->flags & self::NO_ACTION_LINK ) ) {
- if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
+ if ( $s->log_type == 'move' && isset( $paramArray[0] ) && $wgUser->isAllowed( 'move' ) ) {
$destTitle = Title::newFromText( $paramArray[0] );
if ( $destTitle ) {
$revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
@@ -383,9 +384,8 @@ class LogViewer {
// show undelete link
} elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
$revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
- wfMsg( 'undeletebtn' ) ,
+ wfMsg( 'undeletelink' ) ,
'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
-
// show unblock link
} elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
$revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
@@ -394,21 +394,21 @@ class LogViewer {
// show change protection link
} elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
$revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
- // show user tool links for self created users
- // TODO: The extension should be handling this, get it out of core!
- } elseif ( $s->log_action == 'create2' ) {
- if( isset( $paramArray[0] ) ) {
- $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
- } else {
- # Fall back to a blue contributions link
- $revert = $this->skin->userToolLinks( 1, $s->log_title );
- }
- # Suppress $comment from old entries, not needed and can contain incorrect links
- $comment = '';
+ // Show unmerge link
+ } elseif ( $s->log_action == 'merge' ) {
+ $merge = SpecialPage::getTitleFor( 'Mergehistory' );
+ $revert = '(' . $this->skin->makeKnownLinkObj( $merge, wfMsg('revertmerge'),
+ wfArrayToCGI(
+ array('target' => $paramArray[0], 'dest' => $title->getPrefixedText(), 'mergepoint' => $paramArray[1] )
+ )
+ ) . ')';
+ } elseif ( wfRunHooks( 'LogLine', array( $s->log_type, $s->log_action, $title, $paramArray, &$comment, &$revert, $s->log_timestamp ) ) ) {
+ // wfDebug( "Invoked LogLine hook for " $s->log_type . ", " . $s->log_action . "\n" );
+ // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters.
}
}
- $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true );
+ $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true );
$out = "<li>$time $userLink $action $comment $revert</li>\n";
return $out;
}
@@ -525,6 +525,3 @@ class LogViewer {
$out->addHTML( '<p>' . $html . '</p>' );
}
}
-
-
-
diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php
index c89c1af6..70e44750 100644
--- a/includes/SpecialMIMEsearch.php
+++ b/includes/SpecialMIMEsearch.php
@@ -91,7 +91,7 @@ function wfSpecialMIMEsearch( $par = null ) {
array(
'id' => 'specialmimesearch',
'method' => 'get',
- 'action' => $wgTitle->escapeLocalUrl()
+ 'action' => $wgTitle->getLocalUrl()
)
) .
Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) .
diff --git a/includes/SpecialMergeHistory.php b/includes/SpecialMergeHistory.php
new file mode 100644
index 00000000..c7f42fe9
--- /dev/null
+++ b/includes/SpecialMergeHistory.php
@@ -0,0 +1,423 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to
+ * merge article histories, with some restrictions
+ *
+ * @addtogroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMergehistory( $par ) {
+ global $wgRequest;
+
+ $form = new MergehistoryForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @addtogroup SpecialPage
+ */
+class MergehistoryForm {
+ var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
+ var $mTargetObj, $mDestObj;
+
+ function MergehistoryForm( $request, $par = "" ) {
+ global $wgUser;
+
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mDest = $request->getVal( 'dest' );
+ $this->mSubmitted = $request->getBool( 'submitted' );
+
+ $this->mTargetID = intval( $request->getVal( 'targetID' ) );
+ $this->mDestID = intval( $request->getVal( 'destID' ) );
+ $this->mTimestamp = $request->getVal( 'mergepoint' );
+ if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
+ $this->mTimestamp = '';
+ }
+ $this->mComment = $request->getText( 'wpComment' );
+
+ $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ // target page
+ if( $this->mSubmitted ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ $this->mDestObj = Title::newFromURL( $this->mDest );
+ } else {
+ $this->mTargetObj = null;
+ $this->mDestObj = null;
+ }
+
+ $this->preCacheMessages();
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ function preCacheMessages() {
+ // Precache various messages
+ if( !isset( $this->message ) ) {
+ $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
+
+ if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
+ return $this->merge();
+ }
+
+ if ( !$this->mSubmitted ) {
+ $this->showMergeForm();
+ return;
+ }
+
+ $errors = array();
+ if ( !$this->mTargetObj instanceof Title ) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+ } elseif( !$this->mTargetObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+ wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
+ );
+ }
+
+ if ( !$this->mDestObj instanceof Title) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+ } elseif( !$this->mDestObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+ wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
+ );
+ }
+
+ // TODO: warn about target = dest?
+
+ if ( count( $errors ) ) {
+ $this->showMergeForm();
+ $wgOut->addHTML( implode( "\n", $errors ) );
+ } else {
+ $this->showHistory();
+ }
+
+ }
+
+ function showMergeForm() {
+ global $wgOut, $wgScript;
+
+ $wgOut->addWikiMsg( 'mergehistory-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ wfMsg( 'mergehistory-box' ) ) .
+ Xml::hidden( 'title',
+ SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
+ Xml::hidden( 'submitted', '1' ) .
+ Xml::hidden( 'mergepoint', $this->mTimestamp ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
+ <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
+ </tr><tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
+ <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
+ </tr><tr><td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
+ "</td></tr>" .
+ Xml::closeElement( 'table' ) .
+ '</fieldset>' .
+ '</form>' );
+ }
+
+ private function showHistory() {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+ $this->sk = $wgUser->getSkin();
+
+ $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
+
+ $this->showMergeForm();
+
+ # List all stored revisions
+ $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
+ $haveRevisions = $revisions && $revisions->getNumRows() > 0;
+
+ $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
+ $wgOut->addHtml( $top );
+
+ if( $haveRevisions ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgExt( 'mergehistory-merge', array('parseinline'),
+ $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'wpComment', 50, $this->mComment ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
+
+ if( $haveRevisions ) {
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ $wgOut->addHTML( "<ul>" );
+ $wgOut->addHTML( $revisions->getBody() );
+ $wgOut->addHTML( "</ul>" );
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ } else {
+ $wgOut->addWikiMsg( "mergehistory-empty" );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
+ $logViewer = new LogViewer(
+ new LogReader(
+ new FauxRequest(
+ array( 'page' => $this->mTargetObj->getPrefixedText(),
+ 'type' => 'merge' ) ) ) );
+ $logViewer->showList( $wgOut );
+
+ # Slip in the hidden controls here
+ # When we submit, go by page ID to avoid some nasty but unlikely collisions.
+ # Such would happen if a page was renamed after the form loaded, but before submit
+ $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
+ $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
+ $misc .= Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'dest', $this->mDest );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
+
+ return true;
+ }
+
+ function formatRevisionRow( $row ) {
+ global $wgUser, $wgLang;
+
+ $rev = new Revision( $row );
+
+ $stxt = '';
+ $last = $this->message['last'];
+
+ $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
+ $checkBox = wfRadio( "mergepoint", $ts, false );
+
+ $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
+ htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getID() );
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+ }
+
+ # Last link
+ if( !$rev->userCan( Revision::DELETED_TEXT ) )
+ $last = $this->message['last'];
+ else if( isset($this->prevId[$row->rev_id]) )
+ $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'],
+ "&diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );
+
+ $userLink = $this->sk->revUserTools( $rev );
+
+ if(!is_null($size = $row->rev_len)) {
+ if($size == 0)
+ $stxt = wfMsgHtml('historyempty');
+ else
+ $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
+ }
+ $comment = $this->sk->revComment( $rev );
+
+ return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ /**
+ * Fetch revision text link if it's available to all users
+ * @return string
+ */
+ function getPageLink( $row, $titleObj, $ts, $target ) {
+ global $wgLang;
+
+ if( !$this->userCan($row, Revision::DELETED_TEXT) ) {
+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $this->sk->makeKnownLinkObj( $titleObj,
+ $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
+ if( $this->isDeleted($row, Revision::DELETED_TEXT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ function merge() {
+ global $wgOut, $wgUser;
+ # Get the titles directly from the IDs, in case the target page params
+ # were spoofed. The queries are done based on the IDs, so it's best to
+ # keep it consistent...
+ $targetTitle = Title::newFromID( $this->mTargetID );
+ $destTitle = Title::newFromID( $this->mDestID );
+ if( is_null($targetTitle) || is_null($destTitle) )
+ return false; // validate these
+ if( $targetTitle->getArticleID() == $destTitle->getArticleId() )
+ return false;
+ # Verify that this timestamp is valid
+ # Must be older than the destination page
+ $dbw = wfGetDB( DB_MASTER );
+ # Get timestamp into DB format
+ $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp($this->mTimestamp) : '';
+
+ $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $this->mDestID ),
+ __METHOD__ );
+ # Destination page must exist with revisions
+ if( !$maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Leave the latest version no matter what
+ $lasttime = $dbw->selectField( array('page','revision'),
+ 'rev_timestamp',
+ array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
+ __METHOD__ );
+ # Take the most restrictive of the twain
+ $maxtimestamp = ($lasttime < $maxtimestamp) ? $lasttime : $maxtimestamp;
+ // $this->mTimestamp must be less than $maxtimestamp
+ if( $this->mTimestamp >= $maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update the revisions
+ if( $this->mTimestamp ) {
+ $timewhere = "rev_timestamp <= {$this->mTimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$this->mTimestamp);
+ } else {
+ $timewhere = "rev_timestamp < {$maxtimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$maxtimestamp);
+ }
+
+ $dbw->update( 'revision',
+ array( 'rev_page' => $this->mDestID ),
+ array( 'rev_page' => $this->mTargetID,
+ $timewhere ),
+ __METHOD__ );
+ # Check if this did anything
+ if( !$count = $dbw->affectedRows() ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update our logs
+ $log = new LogPage( 'merge' );
+ $log->addEntry( 'merge', $targetTitle, $this->mComment,
+ array($destTitle->getPrefixedText(),$TimestampLimit) );
+
+ $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
+ $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
+
+ wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
+
+ return true;
+ }
+}
+
+class MergeHistoryPager extends ReverseChronologicalPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $title, $title2 ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->title = $title;
+ $this->articleID = $title->getArticleID();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $title2->getArticleID() ),
+ __METHOD__ );
+ $this->maxTimestamp = $maxtimestamp;
+
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $batch = new LinkBatch();
+ # Give some pointers to make (last) links
+ $this->mForm->prevId = array();
+ while( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
+
+ $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
+ if( $rev_id > $row->rev_id )
+ $this->mForm->prevId[$rev_id] = $row->rev_id;
+ else if( $rev_id < $row->rev_id )
+ $this->mForm->prevId[$row->rev_id] = $rev_id;
+
+ $rev_id = $row->rev_id;
+ }
+
+ $batch->execute();
+ $this->mResult->seek( 0 );
+
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ $block = new Block;
+ return $this->mForm->formatRevisionRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds['rev_page'] = $this->articleID;
+ $conds[] = "rev_timestamp < {$this->maxTimestamp}";
+ # Skip the latest one, as that could cause problems
+ if( $page = $this->title->getLatestRevID() )
+ $conds[] = "rev_id != {$page}";
+
+ return array(
+ 'tables' => array('revision'),
+ 'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
+ 'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'rev_timestamp';
+ }
+}
diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php
index b4de0a0e..916f219b 100644
--- a/includes/SpecialMostlinked.php
+++ b/includes/SpecialMostlinked.php
@@ -39,7 +39,7 @@ class MostlinkedPage extends QueryPage {
/**
* Pre-fill the link cache
*/
- function preprocessResults( &$db, &$res ) {
+ function preprocessResults( $db, $res ) {
if( $db->numRows( $res ) > 0 ) {
$linkBatch = new LinkBatch();
while( $row = $db->fetchObject( $res ) )
diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php
index d0a99b3b..c357c8f4 100644
--- a/includes/SpecialMostlinkedcategories.php
+++ b/includes/SpecialMostlinkedcategories.php
@@ -35,7 +35,7 @@ class MostlinkedCategoriesPage extends QueryPage {
/**
* Fetch user page links and cache their existence
*/
- function preprocessResults( &$db, &$res ) {
+ function preprocessResults( $db, $res ) {
$batch = new LinkBatch;
while ( $row = $db->fetchObject( $res ) )
$batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
diff --git a/includes/SpecialMostlinkedtemplates.php b/includes/SpecialMostlinkedtemplates.php
index e7e7afcc..b0f1b196 100644
--- a/includes/SpecialMostlinkedtemplates.php
+++ b/includes/SpecialMostlinkedtemplates.php
@@ -17,7 +17,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
public function getName() {
return 'Mostlinkedtemplates';
}
-
+
/**
* Is this report expensive, i.e should it be cached?
*
@@ -26,7 +26,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
public function isExpensive() {
return true;
}
-
+
/**
* Is there a feed available?
*
@@ -44,7 +44,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
public function sortDescending() {
return true;
}
-
+
/**
* Generate SQL for the report
*
@@ -60,9 +60,9 @@ class SpecialMostlinkedtemplates extends QueryPage {
COUNT(*) AS value
FROM {$templatelinks}
WHERE tl_namespace = " . NS_TEMPLATE . "
- GROUP BY 1, 2, 3";
+ GROUP BY 1, 2, 3";
}
-
+
/**
* Pre-cache page existence to speed up link generation
*
@@ -79,7 +79,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
if( $dbr->numRows( $res ) > 0 )
$dbr->dataSeek( $res, 0 );
}
-
+
/**
* Format a result row
*
@@ -99,7 +99,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
return "Invalid title in result set; {$tsafe}";
}
}
-
+
/**
* Make a "what links here" link for a given title
*
@@ -115,7 +115,6 @@ class SpecialMostlinkedtemplates extends QueryPage {
$wgLang->formatNum( $result->value ) );
return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
}
-
}
/**
@@ -128,4 +127,3 @@ function wfSpecialMostlinkedtemplates( $par = false ) {
$mlt = new SpecialMostlinkedtemplates();
$mlt->doQuery( $offset, $limit );
}
-
diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php
index cfc434ae..e0a89bc2 100644
--- a/includes/SpecialMovepage.php
+++ b/includes/SpecialMovepage.php
@@ -65,7 +65,7 @@ class MovePageForm {
$this->watch = $wgRequest->getCheck( 'wpWatch' );
}
- function showForm( $err ) {
+ function showForm( $err, $hookErr = '' ) {
global $wgOut, $wgUser, $wgContLang;
$start = $wgContLang->isRTL() ? 'right' : 'left';
@@ -78,12 +78,15 @@ class MovePageForm {
$wgOut->showErrorPage( 'notargettitle', 'notargettext' );
return;
}
+ $sk = $wgUser->getSkin();
+ $oldTitleLink = $sk->makeLinkObj( $ot );
$oldTitle = $ot->getPrefixedText();
$encOldTitle = htmlspecialchars( $oldTitle );
if( $this->newTitle == '' ) {
# Show the current title as a default
# when the form is first opened.
+ $newTitle = $oldTitle;
$encNewTitle = $encOldTitle;
} else {
if( $err == '' ) {
@@ -98,12 +101,13 @@ class MovePageForm {
}
}
}
- $encNewTitle = htmlspecialchars( $this->newTitle );
+ $newTitle = $this->newTitle;
+ $encNewTitle = htmlspecialchars( $newTitle );
}
$encReason = htmlspecialchars( $this->reason );
if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
- $wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) );
+ $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
$movepagebtn = wfMsgHtml( 'delete_and_move' );
$submitVar = 'wpDeleteAndMove';
$confirm = "
@@ -112,7 +116,7 @@ class MovePageForm {
</tr>";
$err = '';
} else {
- $wgOut->addWikiText( wfMsg( 'movepagetext' ) );
+ $wgOut->addWikiMsg( 'movepagetext' );
$movepagebtn = wfMsgHtml( 'movepagebtn' );
$submitVar = 'wpMove';
$confirm = false;
@@ -122,7 +126,7 @@ class MovePageForm {
$considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() );
if ( $considerTalk ) {
- $wgOut->addWikiText( wfMsg( 'movepagetalktext' ) );
+ $wgOut->addWikiMsg( 'movepagetalktext' );
}
$movearticle = wfMsgHtml( 'movearticle' );
@@ -135,7 +139,13 @@ class MovePageForm {
if ( $err != '' ) {
$wgOut->setSubtitle( wfMsg( 'formerror' ) );
- $wgOut->addWikiText( '<p class="error">' . wfMsg($err) . "</p>\n" );
+ $errMsg = "";
+ if( $err == 'hookaborted' ) {
+ $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
+ } else {
+ $errMsg = '<p><strong class="error">' . wfMsgWikiHtml( $err ) . "</strong></p>\n";
+ }
+ $wgOut->addHTML( $errMsg );
}
$moveTalkChecked = $this->moveTalk ? ' checked="checked"' : '';
@@ -145,7 +155,7 @@ class MovePageForm {
<table border='0'>
<tr>
<td align='$end'>{$movearticle}</td>
- <td align='$start'><strong>{$oldTitle}</strong></td>
+ <td align='$start'><strong>{$oldTitleLink}</strong></td>
</tr>
<tr>
<td align='$end'><label for='wpNewTitle'>{$newtitle}</label></td>
@@ -186,7 +196,7 @@ class MovePageForm {
<input type='hidden' name='wpEditToken' value=\"{$token}\" />
</form>\n" );
- $this->showLogFragment( $ot, $wgOut );
+ $this->showLogFragment( $ot, $wgOut );
}
@@ -216,6 +226,12 @@ class MovePageForm {
return;
}
+ $hookErr = null;
+ if( !wfRunHooks( 'AbortMove', array( $ot, $nt, $wgUser, &$hookErr ) ) ) {
+ $this->showForm( 'hookaborted', $hookErr );
+ return;
+ }
+
$error = $ot->moveTo( $nt, true, $this->reason );
if ( $error !== true ) {
$this->showForm( $error );
@@ -280,21 +296,21 @@ class MovePageForm {
$talkmoved = $wgRequest->getVal( 'talkmoved' );
$oldUrl = $old->getFullUrl( 'redirect=no' );
- $newUrl = $new->getFullURl();
+ $newUrl = $new->getFullUrl();
$oldText = $old->getPrefixedText();
$newText = $new->getPrefixedText();
$oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
$newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
- $s = wfMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
+ $s = wfMsgNoTrans( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
if ( $talkmoved == 1 ) {
- $s .= "\n\n" . wfMsg( 'talkpagemoved' );
+ $s .= "\n\n" . wfMsgNoTrans( 'talkpagemoved' );
} elseif( 'articleexists' == $talkmoved ) {
- $s .= "\n\n" . wfMsg( 'talkexists' );
+ $s .= "\n\n" . wfMsgNoTrans( 'talkexists' );
} else {
if( !$old->isTalkPage() && $talkmoved != 'notalkpage' ) {
- $s .= "\n\n" . wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) );
+ $s .= "\n\n" . wfMsgNoTrans( 'talkpagenotmoved', wfMsgNoTrans( $talkmoved ) );
}
}
$wgOut->addWikiText( $s );
diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php
index f81a70f4..013b0986 100644
--- a/includes/SpecialNewimages.php
+++ b/includes/SpecialNewimages.php
@@ -200,7 +200,7 @@ function wfSpecialNewimages( $par, $specialPage ) {
if ($shownav)
$wgOut->addHTML( $prevnext );
} else {
- $wgOut->addWikiText( wfMsg( 'noimages' ) );
+ $wgOut->addWikiMsg( 'noimages' );
}
}
diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php
index abd5e018..1c3bee84 100644
--- a/includes/SpecialNewpages.php
+++ b/includes/SpecialNewpages.php
@@ -4,18 +4,116 @@
* @addtogroup SpecialPage
*/
+
+/**
+ * Start point
+ */
+function wfSpecialNewPages( $par, $specialPage ) {
+ $page = new NewPagesPage( $specialPage );
+ $page->execute( $par );
+}
+
/**
* implements Special:Newpages
* @addtogroup SpecialPage
*/
class NewPagesPage extends QueryPage {
- var $namespace;
- var $username = '';
+ protected $options = array();
+ protected $nondefaults = array();
+ protected $specialPage;
+
+ public function __construct( $specialPage=null ) {
+ $this->specialPage = $specialPage;
+ }
+
+ public function execute( $par ) {
+ global $wgRequest, $wgLang;
+
+ $shownavigation = is_object( $this->specialPage ) && !$this->specialPage->including();
+
+ $defaults = array(
+ /* bool */ 'hideliu' => false,
+ /* bool */ 'hidepatrolled' => false,
+ /* bool */ 'hidebots' => false,
+ /* text */ 'namespace' => "0",
+ /* text */ 'username' => '',
+ /* int */ 'offset' => 0,
+ /* int */ 'limit' => 50,
+ );
+
+ $options = $defaults;
+
+ if ( $par ) {
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'shownav' == $bit )
+ $shownavigation = true;
+ if ( 'hideliu' === $bit )
+ $options['hideliu'] = true;
+ if ( 'hidepatrolled' == $bit )
+ $options['hidepatrolled'] = true;
+ if ( 'hidebots' == $bit )
+ $options['hidebots'] = true;
+ if ( is_numeric( $bit ) )
+ $options['limit'] = intval( $bit );
+
+ $m = array();
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
+ $options['limit'] = intval($m[1]);
+ if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) )
+ $options['offset'] = intval($m[1]);
+ if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
+ $ns = $wgLang->getNsIndex( $m[1] );
+ if( $ns !== false ) {
+ $options['namespace'] = $ns;
+ }
+ }
+ }
+ }
+
+ // Override all values from requests, if specified
+ foreach ( $defaults as $v => $t ) {
+ if ( is_bool($t) ) {
+ $options[$v] = $wgRequest->getBool( $v, $options[$v] );
+ } elseif( is_int($t) ) {
+ $options[$v] = $wgRequest->getInt( $v, $options[$v] );
+ } elseif( is_string($t) ) {
+ $options[$v] = $wgRequest->getText( $v, $options[$v] );
+ }
+ }
+
+ // Validate limit and offset params
+ if ( $options['limit'] <= 0 ) {
+ $options['limit'] = $defaults['limit'];
+ }
+
+ if ( $options['offset'] < 0 ) {
+ $options['offset'] = $defaults['offset'];
+ }
+
+ $nondefaults = array();
+ foreach ( $options as $v => $t ) {
+ if ( $v === 'offset' ) continue; # Reset offset if parameters change
+ wfAppendToArrayIfNotDefault( $v, $t, $defaults, $nondefaults );
+ }
+
+ # bind to class
+ $this->options = $options;
+ $this->nondefaults = $nondefaults;
+
+ if ( !$this->doFeed( $wgRequest->getVal( 'feed' ), $options['limit'] ) ) {
+ $this->doQuery( $options['offset'], $options['limit'], $shownavigation );
+ }
+ }
- function NewPagesPage( $namespace = NS_MAIN, $username = '' ) {
- $this->namespace = $namespace;
- $this->username = $username;
+ function linkParameters() {
+ $nondefaults = $this->nondefaults;
+ // QueryPage seems to handle limit and offset itself
+ if ( isset( $nondefaults['limit'] ) ) {
+ unset($nondefaults['limit']);
+ }
+ return $nondefaults;
}
function getName() {
@@ -27,29 +125,40 @@ class NewPagesPage extends QueryPage {
return false;
}
- function makeUserWhere( &$dbo ) {
- $title = Title::makeTitleSafe( NS_USER, $this->username );
- if( $title ) {
- return ' AND rc_user_text = ' . $dbo->addQuotes( $title->getText() );
+ function makeUserWhere( $db ) {
+ global $wgGroupPermissions;
+ $conds = array();
+ if ($this->options['hidepatrolled']) {
+ $conds['rc_patrolled'] = 0;
+ }
+ if ($this->options['hidebots']) {
+ $conds['rc_bot'] = 0;
+ }
+ if ($wgGroupPermissions['*']['createpage'] == true && $this->options['hideliu']) {
+ $conds['rc_user'] = 0;
} else {
- return '';
+ $title = Title::makeTitleSafe( NS_USER, $this->options['username'] );
+ if( $title ) {
+ $conds['rc_user_text'] = $title->getText();
+ }
}
- }
-
- private function makeNamespaceWhere() {
- return $this->namespace !== 'all'
- ? ' AND rc_namespace = ' . intval( $this->namespace )
- : '';
+ return $conds;
}
function getSQL() {
- global $wgUser, $wgUseRCPatrol;
- $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0;
+ global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
+ $usepatrol = ( $wgUseNPPatrol || $wgUseRCPatrol ) ? 1 : 0;
$dbr = wfGetDB( DB_SLAVE );
list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' );
- $nsfilter = $this->makeNamespaceWhere();
- $uwhere = $this->makeUserWhere( $dbr );
+ $conds = array();
+ $conds['rc_new'] = 1;
+ if ( $this->options['namespace'] !== 'all' ) {
+ $conds['rc_namespace'] = intval( $this->options['namespace'] );
+ }
+ $conds['page_is_redirect'] = 0;
+ $conds += $this->makeUserWhere( $dbr );
+ $condstext = $dbr->makeList( $conds, LIST_AND );
# FIXME: text will break with compression
return
@@ -68,23 +177,20 @@ class NewPagesPage extends QueryPage {
page_len as length,
page_latest as rev_id
FROM $recentchanges,$page
- WHERE rc_cur_id=page_id AND rc_new=1
- {$nsfilter}
- AND page_is_redirect = 0
- {$uwhere}";
+ WHERE rc_cur_id=page_id AND $condstext";
}
-
- function preprocessResults( &$dbo, &$res ) {
+
+ function preprocessResults( $db, $res ) {
# Do a batch existence check on the user and talk pages
$linkBatch = new LinkBatch();
- while( $row = $dbo->fetchObject( $res ) ) {
- $linkBatch->addObj( Title::makeTitleSafe( NS_USER, $row->user_text ) );
- $linkBatch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_text ) );
+ while( $row = $db->fetchObject( $res ) ) {
+ $linkBatch->add( NS_USER, $row->user_text );
+ $linkBatch->add( NS_USER_TALK, $row->user_text );
}
$linkBatch->execute();
# Seek to start
- if( $dbo->numRows( $res ) > 0 )
- $dbo->dataSeek( $res, 0 );
+ if( $db->numRows( $res ) > 0 )
+ $db->dataSeek( $res, 0 );
}
/**
@@ -116,8 +222,10 @@ class NewPagesPage extends QueryPage {
* @return bool
*/
function patrollable( $result ) {
- global $wgUser, $wgUseRCPatrol;
- return $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) && !$result->patrolled;
+ global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
+ return ( $wgUseRCPatrol || $wgUseNPPatrol )
+ && $wgUser->isAllowed( 'patrol' )
+ && !$result->patrolled;
}
function feedItemDesc( $row ) {
@@ -131,82 +239,79 @@ class NewPagesPage extends QueryPage {
}
return parent::feedItemDesc( $row );
}
-
+
/**
* Show a form for filtering namespace and username
*
* @return string
- */
+ */
function getPageHeader() {
- global $wgScript;
+ global $wgScript, $wgContLang, $wgGroupPermissions, $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
+ $sk = $wgUser->getSkin();
+ $align = $wgContLang->isRTL() ? 'left' : 'right';
$self = SpecialPage::getTitleFor( $this->getName() );
- $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
- $form .= Xml::hidden( 'title', $self->getPrefixedDBkey() );
- # Namespace selector
- $form .= '<table><tr><td align="right">' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '</td>';
- $form .= '<td>' . Xml::namespaceSelector( $this->namespace, 'all' ) . '</td></tr>';
- # Username filter
- $form .= '<tr><td align="right">' . Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . '</td>';
- $form .= '<td>' . Xml::input( 'username', 30, $this->username, array( 'id' => 'mw-np-username' ) ) . '</td></tr>';
-
- $form .= '<tr><td></td><td>' . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</td></tr></table>';
- $form .= Xml::hidden( 'offset', $this->offset ) . Xml::hidden( 'limit', $this->limit ) . '</form>';
- return $form;
- }
-
- /**
- * Link parameters
- *
- * @return array
- */
- function linkParameters() {
- return( array( 'namespace' => $this->namespace, 'username' => $this->username ) );
- }
-
-}
-/**
- * constructor
- */
-function wfSpecialNewpages($par, $specialPage) {
- global $wgRequest, $wgContLang;
-
- list( $limit, $offset ) = wfCheckLimits();
- $namespace = NS_MAIN;
- $username = '';
-
- if ( $par ) {
- $bits = preg_split( '/\s*,\s*/', trim( $par ) );
- foreach ( $bits as $bit ) {
- if ( 'shownav' == $bit )
- $shownavigation = true;
- if ( is_numeric( $bit ) )
- $limit = $bit;
-
- $m = array();
- if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
- $limit = intval($m[1]);
- if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) )
- $offset = intval($m[1]);
- if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
- $ns = $wgContLang->getNsIndex( $m[1] );
- if( $ns !== false ) {
- $namespace = $ns;
- }
- }
+ // show/hide links
+ $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ));
+
+ $hidelinks = array();
+
+ if ( $wgGroupPermissions['*']['createpage'] === true ) {
+ $hidelinks['hideliu'] = 'rcshowhideliu';
}
- } else {
- if( $ns = $wgRequest->getText( 'namespace', NS_MAIN ) )
- $namespace = $ns;
- if( $un = $wgRequest->getText( 'username' ) )
- $username = $un;
- }
-
- if ( ! isset( $shownavigation ) )
- $shownavigation = ! $specialPage->including();
+ if ( $wgUseNPPatrol || $wgUseRCPatrol ) {
+ $hidelinks['hidepatrolled'] = 'rcshowhidepatr';
+ }
+ $hidelinks['hidebots'] = 'rcshowhidebots';
- $npp = new NewPagesPage( $namespace, $username );
+ $links = array();
+ foreach ( $hidelinks as $key => $msg ) {
+ $reversed = 1-$this->options[$key];
+ $link = $sk->makeKnownLinkObj( $self, $showhide[$reversed],
+ wfArrayToCGI( array( $key => $reversed ), $this->nondefaults )
+ );
+ $links[$key] = wfMsgHtml( $msg, $link );
+ }
+
+ $hl = implode( ' | ', $links );
+
+ // Store query values in hidden fields so that form submission doesn't lose them
+ $hidden = array();
+ foreach ( $this->nondefaults as $key => $value ) {
+ if ( $key === 'namespace' ) continue;
+ if ( $key === 'username' ) continue;
+ $hidden[] = Xml::hidden( $key, $value );
+ }
+ $hidden = implode( "\n", $hidden );
- if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) )
- $npp->doQuery( $offset, $limit, $shownavigation );
-} \ No newline at end of file
+ $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::hidden( 'title', $self->getPrefixedDBkey() ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td align=\"$align\">" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td>" .
+ Xml::namespaceSelector( $this->options['namespace'], 'all' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align=\"$align\">" .
+ Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'username', 30, $this->options['username'], array( 'id' => 'mw-np-username' ) ) .
+ "</td>
+ </tr>
+ <tr> <td></td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>" .
+ "<tr><td></td><td>" . $hl . "</td></tr>" .
+ Xml::closeElement( 'table' ) .
+ $hidden .
+ Xml::closeElement( 'form' );
+ return $form;
+ }
+}
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index 89fd15bb..c9037ea7 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -80,6 +80,7 @@ class SpecialPage
'Userlogin' => array( 'SpecialPage', 'Userlogin' ),
'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ),
+ 'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ),
'Preferences' => array( 'SpecialPage', 'Preferences' ),
'Watchlist' => array( 'SpecialPage', 'Watchlist' ),
@@ -89,36 +90,37 @@ class SpecialPage
'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
'Listusers' => array( 'SpecialPage', 'Listusers' ),
'Statistics' => array( 'SpecialPage', 'Statistics' ),
- 'Randompage' => array( 'SpecialPage', 'Randompage' ),
+ 'Randompage' => 'Randompage',
'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ),
'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ),
'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ),
'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ),
- 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ),
+ 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ),
'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ),
'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ),
'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ),
'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ),
'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ),
- 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ),
+ 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ),
'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ),
'Mostimages' => array( 'SpecialPage', 'Mostimages' ),
'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ),
- 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ),
+ 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ),
'Shortpages' => array( 'SpecialPage', 'Shortpages' ),
'Longpages' => array( 'SpecialPage', 'Longpages' ),
'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ),
'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ),
'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ),
'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ),
+ 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ),
'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ),
'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) ,
'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ),
'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ),
'Contributions' => array( 'SpecialPage', 'Contributions' ),
'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ),
- 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ),
+ 'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ),
'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ),
'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ),
'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
@@ -131,23 +133,26 @@ class SpecialPage
'Log' => array( 'SpecialPage', 'Log' ),
'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ),
'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
- 'Import' => array( 'SpecialPage', "Import", 'import' ),
+ 'Import' => array( 'SpecialPage', 'Import', 'import' ),
'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
- 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ),
+ 'Userrights' => 'UserrightsPage',
'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ),
'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ),
'Listredirects' => array( 'SpecialPage', 'Listredirects' ),
- 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ),
+ 'Revisiondelete' => array( 'UnlistedSpecialPage', 'Revisiondelete', 'deleterevision' ),
'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ),
- 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ),
- 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ),
+ 'Randomredirect' => 'SpecialRandomredirect',
+ 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ),
+ 'Filepath' => array( 'SpecialPage', 'Filepath' ),
'Mypage' => array( 'SpecialMypage' ),
'Mytalk' => array( 'SpecialMytalk' ),
'Mycontributions' => array( 'SpecialMycontributions' ),
'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ),
- );
+ 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ),
+ 'Listbots' => array( 'SpecialRedirectToSpecial', 'Listbots', 'Listusers', 'bot' ),
+ );
static public $mAliases;
static public $mListInitialised = false;
@@ -349,7 +354,7 @@ class SpecialPage
foreach ( self::$mList as $name => $rec ) {
$page = self::getPage( $name );
- if ( $page->isListed() && $page->getRestriction() == '' ) {
+ if ( $page->isListed() && !$page->isRestricted() ) {
$pages[$name] = $page;
}
}
@@ -370,11 +375,12 @@ class SpecialPage
foreach ( self::$mList as $name => $rec ) {
$page = self::getPage( $name );
- if ( $page->isListed() ) {
- $restriction = $page->getRestriction();
- if ( $restriction != '' && $wgUser->isAllowed( $restriction ) ) {
- $pages[$name] = $page;
- }
+ if (
+ $page->isListed()
+ and $page->isRestricted()
+ and $page->userCanExecute( $wgUser )
+ ) {
+ $pages[$name] = $page;
}
}
return $pages;
@@ -404,7 +410,6 @@ class SpecialPage
$par = $bits[1];
}
$page = SpecialPage::getPageByAlias( $name );
-
# Nonexistent?
if ( !$page ) {
if ( !$including ) {
@@ -486,6 +491,11 @@ class SpecialPage
/**
* Get the local name for a specified canonical name
+ *
+ * @param $name
+ * @param mixed $subpage Boolean false, or string
+ *
+ * @return string
*/
static function getLocalNameFor( $name, $subpage = false ) {
global $wgContLang;
@@ -603,10 +613,25 @@ class SpecialPage
}
/**
+ * Can be overridden by subclasses with more complicated permissions
+ * schemes.
+ *
+ * @return bool Should the page be displayed with the restricted-access
+ * pages?
+ */
+ public function isRestricted() {
+ return $this->mRestriction != '';
+ }
+
+ /**
* Checks if the given user (identified by an object) can execute this
- * special page (as defined by $mRestriction)
+ * special page (as defined by $mRestriction). Can be overridden by sub-
+ * classes with more complicated permissions schemes.
+ *
+ * @param User $user The user to check
+ * @return bool Does the user have permission to view the page?
*/
- function userCanExecute( &$user ) {
+ public function userCanExecute( $user ) {
return $user->isAllowed( $this->mRestriction );
}
@@ -642,7 +667,7 @@ class SpecialPage
if ( $this->userCanExecute( $wgUser ) ) {
$func = $this->mFunction;
// only load file if the function does not exist
- if(!function_exists($func) and $this->mFile) {
+ if(!is_callable($func) and $this->mFile) {
require_once( $this->mFile );
}
# FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage.
@@ -650,7 +675,7 @@ class SpecialPage
$this->outputHeader();
if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) )
return;
- $func( $par, $this );
+ call_user_func( $func, $par, $this );
if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) )
return;
} else {
@@ -662,9 +687,10 @@ class SpecialPage
global $wgOut, $wgContLang;
$msg = $wgContLang->lc( $this->name() ) . '-summary';
- $out = wfMsg( $msg );
- if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() )
+ $out = wfMsgNoTrans( $msg );
+ if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) {
$wgOut->addWikiText( $out );
+ }
}
@@ -776,7 +802,7 @@ class SpecialRedirectToSpecial extends UnlistedSpecialPage {
class SpecialMypage extends UnlistedSpecialPage {
function __construct() {
parent::__construct( 'Mypage' );
- $this->mAllowedRedirectParams = array( 'action' );
+ $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', 'section' );
}
function getRedirect( $subpage ) {
@@ -796,7 +822,7 @@ class SpecialMypage extends UnlistedSpecialPage {
class SpecialMytalk extends UnlistedSpecialPage {
function __construct() {
parent::__construct( 'Mytalk' );
- $this->mAllowedRedirectParams = array( 'action' );
+ $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', 'section' );
}
function getRedirect( $subpage ) {
@@ -823,5 +849,3 @@ class SpecialMycontributions extends UnlistedSpecialPage {
return SpecialPage::getTitleFor( 'Contributions', $wgUser->getName() );
}
}
-
-
diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php
index a36be289..ca163a6a 100644
--- a/includes/SpecialPreferences.php
+++ b/includes/SpecialPreferences.php
@@ -24,7 +24,7 @@ class PreferencesForm {
var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
var $mUserLanguage, $mUserVariant;
var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
- var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize;
+ var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize;
var $mUnderline, $mWatchlistEdits;
/**
@@ -65,6 +65,7 @@ class PreferencesForm {
$this->mSuccess = $request->getCheck( 'success' );
$this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
$this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
+ $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
$this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
$this->mPosted &&
@@ -98,7 +99,7 @@ class PreferencesForm {
$this->mUserLanguage = 'nolanguage';
}
- wfRunHooks( "InitPreferencesForm", array( $this, $request ) );
+ wfRunHooks( 'InitPreferencesForm', array( $this, $request ) );
}
function execute() {
@@ -207,29 +208,29 @@ class PreferencesForm {
function savePreferences() {
global $wgUser, $wgOut, $wgParser;
global $wgEnableUserEmail, $wgEnableEmail;
- global $wgEmailAuthentication;
- global $wgAuth;
+ global $wgEmailAuthentication, $wgRCMaxAge;
+ global $wgAuth, $wgEmailConfirmToEdit;
if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
if ( $this->mNewpass != $this->mRetypePass ) {
- wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'badretype' ) );
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'badretype' ) );
$this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
return;
}
if (!$wgUser->checkPassword( $this->mOldpass )) {
- wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
$this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
return;
}
try {
$wgUser->setPassword( $this->mNewpass );
- wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'success' ) );
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) );
$this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
} catch( PasswordError $e ) {
- wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'error' ) );
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) );
$this->mainPrefsForm( 'error', $e->getMessage() );
return;
}
@@ -250,7 +251,7 @@ class PreferencesForm {
wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) );
return;
} elseif( $this->mToggles['fancysig'] ) {
- if( Parser::validateSig( $this->mNick ) !== false ) {
+ if( $wgParser->validateSig( $this->mNick ) !== false ) {
$this->mNick = $wgParser->cleanSig( $this->mNick );
} else {
$this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
@@ -275,7 +276,7 @@ class PreferencesForm {
$wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) );
$wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) );
$wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) );
- $wgUser->setOption( 'rcdays', $this->validateInt( $this->mRecentDays, 1, 7 ) );
+ $wgUser->setOption( 'rcdays', $this->validateInt($this->mRecentDays, 1, ceil($wgRCMaxAge / (3600*24))));
$wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) );
$wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) );
$wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) );
@@ -285,6 +286,7 @@ class PreferencesForm {
$wgUser->setOption( 'thumbsize', $this->mThumbSize );
$wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
$wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
+ $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
# Set search namespace options
foreach( $this->mSearchNs as $i => $value ) {
@@ -299,20 +301,6 @@ class PreferencesForm {
foreach ( $this->mToggles as $tname => $tvalue ) {
$wgUser->setOption( $tname, $tvalue );
}
- if (!$wgAuth->updateExternalDB($wgUser)) {
- $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
- return;
- }
-
- $msg = '';
- if ( !wfRunHooks( "SavePreferences", array( $this, $wgUser, &$msg ) ) ) {
- print "(($msg))";
- $this->mainPrefsForm( 'error', $msg );
- return;
- }
-
- $wgUser->setCookies();
- $wgUser->saveSettings();
$error = false;
if( $wgEnableEmail ) {
@@ -323,7 +311,6 @@ class PreferencesForm {
if( $wgUser->isValidEmailAddr( $newadr ) ) {
$wgUser->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record
$wgUser->mEmailAuthenticated = null; # but flag as "dirty" = unauthenticated
- $wgUser->saveSettings();
if ($wgEmailAuthentication) {
# Mail a temporary password to the dirty address.
# User can come back through the confirmation URL to re-enable email.
@@ -338,17 +325,34 @@ class PreferencesForm {
$error = wfMsg( 'invalidemailaddress' );
}
} else {
+ if( $wgEmailConfirmToEdit && empty( $newadr ) ) {
+ $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) );
+ return;
+ }
$wgUser->setEmail( $this->mUserEmail );
- $wgUser->setCookies();
- $wgUser->saveSettings();
}
if( $oldadr != $newadr ) {
- wfRunHooks( "PrefsEmailAudit", array( $wgUser, $oldadr, $newadr ) );
+ wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
}
}
+ if (!$wgAuth->updateExternalDB($wgUser)) {
+ $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+ return;
+ }
+
+ $msg = '';
+ if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg ) ) ) {
+ print "(($msg))";
+ $this->mainPrefsForm( 'error', $msg );
+ return;
+ }
+
+ $wgUser->setCookies();
+ $wgUser->saveSettings();
+
if( $needRedirect && $error === false ) {
- $title =& SpecialPage::getTitleFor( "Preferences" );
+ $title = SpecialPage::getTitleFor( 'Preferences' );
$wgOut->redirect($title->getFullURL('success'));
return;
}
@@ -393,6 +397,7 @@ class PreferencesForm {
$this->mWatchlistEdits = $wgUser->getOption( 'wllimit' );
$this->mUnderline = $wgUser->getOption( 'underline' );
$this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
+ $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
$togs = User::getToggles();
foreach ( $togs as $tname ) {
@@ -406,7 +411,7 @@ class PreferencesForm {
}
}
- wfRunHooks( "ResetPreferences", array( $this, $wgUser ) );
+ wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) );
}
/**
@@ -510,6 +515,7 @@ class PreferencesForm {
global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
+ global $wgEmailConfirmToEdit, $wgAjaxSearch;
$wgOut->setPageTitle( wfMsg( 'preferences' ) );
$wgOut->setArticleRelated( false );
@@ -518,11 +524,11 @@ class PreferencesForm {
$wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
if ( $this->mSuccess || 'success' == $status ) {
- $wgOut->addWikitext( '<div class="successbox"><strong>'. wfMsg( 'savedprefs' ) . '</strong></div>' );
+ $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' );
} else if ( 'error' == $status ) {
- $wgOut->addWikitext( '<div class="errorbox"><strong>' . $message . '</strong></div>' );
+ $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message . '</strong></div>' );
} else if ( '' != $status ) {
- $wgOut->addWikitext( $message . "\n----" );
+ $wgOut->addWikiText( $message . "\n----" );
}
$qbs = $wgLang->getQuickbarSettings();
@@ -619,7 +625,7 @@ class PreferencesForm {
Xml::label( wfMsg('youremail'), 'wpUserEmail' ),
Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ),
Xml::tags('div', array( 'class' => 'prefsectiontip' ),
- wfMsgExt( 'prefs-help-email', 'parseinline' )
+ wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' )
)
)
);
@@ -967,7 +973,13 @@ class PreferencesForm {
$wgOut->addHtml( '</fieldset>' );
# Search
+ $ajaxsearch = $wgAjaxSearch ?
+ $this->addRow(
+ wfLabel( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
+ wfCheck( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
+ ) : '';
$wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' .
+ $ajaxsearch .
$this->addRow(
wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ),
wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
@@ -1010,7 +1022,7 @@ class PreferencesForm {
}
$wgOut->addHTML( '</fieldset>' );
- wfRunHooks( "RenderPreferencesForm", array( $this, $wgOut ) );
+ wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
$token = htmlspecialchars( $wgUser->editToken() );
$skin = $wgUser->getSkin();
diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php
index 6bb26d67..bfab21b6 100644
--- a/includes/SpecialPrefixindex.php
+++ b/includes/SpecialPrefixindex.php
@@ -40,11 +40,11 @@ function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
* @addtogroup SpecialPage
*/
class SpecialPrefixindex extends SpecialAllpages {
- var $maxPerPage=960;
- var $topLevelMax=50;
- var $name='Prefixindex';
- # Determines, which message describes the input field 'nsfrom', used in function namespaceForm (see superclass SpecialAllpages)
- var $nsfromMsg='allpagesprefix';
+ // Inherit $maxPerPage
+
+ // Define other properties
+ protected $name = 'Prefixindex';
+ protected $nsfromMsg = 'allpagesprefix';
/**
* @param integer $namespace (Default NS_MAIN)
diff --git a/includes/SpecialProtectedpages.php b/includes/SpecialProtectedpages.php
index 122ca8fc..60a8d602 100644
--- a/includes/SpecialProtectedpages.php
+++ b/includes/SpecialProtectedpages.php
@@ -52,7 +52,7 @@ class ProtectedPagesForm {
* Callback function to output a restriction
*/
function formatRow( $row ) {
- global $wgUser, $wgLang;
+ global $wgUser, $wgLang, $wgContLang;
wfProfileIn( __METHOD__ );
@@ -89,6 +89,7 @@ class ProtectedPagesForm {
$stxt = ' <small>' . wfMsgHtml('historyempty') . '</small>';
else
$stxt = ' <small>' . wfMsgHtml('historysize', $wgLang->formatNum( $size ) ) . '</small>';
+ $stxt = $wgContLang->getDirMark() . $stxt;
}
wfProfileOut( __METHOD__ );
diff --git a/includes/SpecialProtectedtitles.php b/includes/SpecialProtectedtitles.php
new file mode 100755
index 00000000..4bc303bb
--- /dev/null
+++ b/includes/SpecialProtectedtitles.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ *
+ * @addtogroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @addtogroup SpecialPage
+ */
+class ProtectedTitlesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
+ function showList( $msg = '' ) {
+ global $wgOut, $wgRequest;
+
+ $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Title::purgeExpiredRestrictions();
+ }
+
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
+ $NS = $wgRequest->getIntOrNull( 'namespace' );
+
+ $pager = new ProtectedTitlesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
+
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
+
+ if ( $pager->getNumRows() ) {
+ $s = $pager->getNavigationBar();
+ $s .= "<ul>" .
+ $pager->getBody() .
+ "</ul>";
+ $s .= $pager->getNavigationBar();
+ } else {
+ $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
+ }
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Callback function to output a restriction
+ */
+ function formatRow( $row ) {
+ global $wgUser, $wgLang, $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $skin=null;
+
+ if( is_null( $skin ) )
+ $skin = $wgUser->getSkin();
+
+ $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
+ $link = $skin->makeLinkObj( $title );
+
+ $description_items = array ();
+
+ $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
+
+ $description_items[] = $protType;
+
+ $expiry_description = ''; $stxt = '';
+
+ if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
+ $expiry = Block::decodeExpiry( $row->pt_expiry );
+
+ $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+ $description_items[] = $expiry_description;
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . "</li>\n";
+ }
+
+ /**
+ * @param $namespace int
+ * @param $type string
+ * @param $level string
+ * @param $minsize int
+ * @private
+ */
+ function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
+ global $wgScript;
+ $action = htmlspecialchars( $wgScript );
+ $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
+ $special = htmlspecialchars( $title->getPrefixedDBkey() );
+ return "<form action=\"$action\" method=\"get\">\n" .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
+ Xml::hidden( 'title', $special ) . "&nbsp;\n" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ // $this->getLevelMenu( $level ) . "<br/>\n" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ "</fieldset></form>";
+ }
+
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ function getNamespaceMenu( $namespace = null ) {
+ return Xml::label( wfMsg( 'namespace' ), 'namespace' )
+ . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getLevelMenu( $pr_level ) {
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionLevels as $type ) {
+ if ( $type !='' && $type !='*') {
+ $text = wfMsg("restriction-level-$type");
+ $m[$text] = $type;
+ }
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_level );
+ $options[] = Xml::option( $text, $type, $selected );
+ }
+
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
+ }
+}
+
+/**
+ * @todo document
+ * @addtogroup Pager
+ */
+class ProtectedtitlesPager extends AlphabeticPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->level = $level;
+ $this->namespace = $namespace;
+ $this->size = intval($size);
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->pt_namespace, $row->pt_title );
+ }
+
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+
+ if( !is_null($this->namespace) )
+ $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ return array(
+ 'tables' => 'protected_titles',
+ 'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'pt_timestamp';
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedtitles() {
+
+ $ppForm = new ProtectedTitlesForm();
+
+ $ppForm->showList();
+}
+
+
+
diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php
index 42734274..9f324bd0 100644
--- a/includes/SpecialRandompage.php
+++ b/includes/SpecialRandompage.php
@@ -9,56 +9,56 @@
*/
/**
- * Main execution point
- * @param $par Namespace to select the page from
- */
-function wfSpecialRandompage( $par = null ) {
- global $wgOut, $wgContLang;
-
- $rnd = new RandomPage();
- $rnd->setNamespace( $wgContLang->getNsIndex( $par ) );
- $rnd->setRedirect( false );
-
- $title = $rnd->getRandomTitle();
-
- if( is_null( $title ) ) {
- $wgOut->addWikiText( wfMsg( 'randompage-nopages' ) );
- return;
- }
-
- $wgOut->reportTime();
- $wgOut->redirect( $title->getFullUrl() );
-}
-
-
-/**
* Special page to direct the user to a random page
*
* @addtogroup SpecialPage
*/
-class RandomPage {
+class RandomPage extends SpecialPage {
private $namespace = NS_MAIN; // namespace to select pages from
- private $redirect = false; // select redirects instead of normal pages?
- public function getNamespace ( ) {
+ function __construct( $name = 'Randompage' ){
+ parent::__construct( $name );
+ }
+
+ public function getNamespace() {
return $this->namespace;
}
+
public function setNamespace ( $ns ) {
if( $ns < NS_MAIN ) $ns = NS_MAIN;
$this->namespace = $ns;
}
- public function getRedirect ( ) {
- return $this->redirect;
+
+ // select redirects instead of normal pages?
+ // Overriden by SpecialRandomredirect
+ public function isRedirect(){
+ return false;
}
- public function setRedirect ( $redirect ) {
- $this->redirect = $redirect;
+
+ public function execute( $par ) {
+ global $wgOut, $wgContLang;
+
+ if ($par)
+ $this->setNamespace( $wgContLang->getNsIndex( $par ) );
+
+ $title = $this->getRandomTitle();
+
+ if( is_null( $title ) ) {
+ $this->setHeaders();
+ $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
+ return;
+ }
+
+ $query = $this->isRedirect() ? 'redirect=no' : '';
+ $wgOut->redirect( $title->getFullUrl( $query ) );
}
+
/**
* Choose a random title.
* @return Title object (or null if nothing to choose from)
*/
- public function getRandomTitle ( ) {
+ public function getRandomTitle() {
$randstr = wfRandom();
$row = $this->selectRandomPageFromDB( $randstr );
@@ -78,7 +78,7 @@ class RandomPage {
return null;
}
- private function selectRandomPageFromDB ( $randstr ) {
+ private function selectRandomPageFromDB( $randstr ) {
global $wgExtraRandompageSQL;
$fname = 'RandomPage::selectRandomPageFromDB';
@@ -88,7 +88,7 @@ class RandomPage {
$page = $dbr->tableName( 'page' );
$ns = (int) $this->namespace;
- $redirect = $this->redirect ? 1 : 0;
+ $redirect = $this->isRedirect() ? 1 : 0;
$extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
$sql = "SELECT page_title
diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php
index b7aa3e49..ccf5cbcd 100644
--- a/includes/SpecialRandomredirect.php
+++ b/includes/SpecialRandomredirect.php
@@ -7,27 +7,14 @@
* @author Rob Church <robchur@gmail.com>, Ilmari Karonen
* @license GNU General Public Licence 2.0 or later
*/
-
-/**
- * Main execution point
- * @param $par Namespace to select the redirect from
- */
-function wfSpecialRandomredirect( $par = null ) {
- global $wgOut, $wgContLang;
-
- $rnd = new RandomPage();
- $rnd->setNamespace( $wgContLang->getNsIndex( $par ) );
- $rnd->setRedirect( true );
-
- $title = $rnd->getRandomTitle();
-
- if( is_null( $title ) ) {
- $wgOut->addWikiText( wfMsg( 'randomredirect-nopages' ) );
- return;
+class SpecialRandomredirect extends RandomPage {
+ function __construct(){
+ parent::__construct( 'Randomredirect' );
}
- $wgOut->reportTime();
- $wgOut->redirect( $title->getFullUrl( 'redirect=no' ) );
+ // Override parent::isRedirect()
+ public function isRedirect(){
+ return true;
+ }
}
-
diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php
index 7565481b..60a04e00 100644
--- a/includes/SpecialRecentchanges.php
+++ b/includes/SpecialRecentchanges.php
@@ -172,13 +172,9 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
while( $row = $dbr->fetchObject( $res ) ){
$rows[] = $row;
if ( !$feedFormat ) {
- // User page link
- $title = Title::makeTitleSafe( NS_USER, $row->rc_user_text );
- $batch->addObj( $title );
-
- // User talk
- $title = Title::makeTitleSafe( NS_USER_TALK, $row->rc_user_text );
- $batch->addObj( $title );
+ // User page and talk links
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
}
}
@@ -221,7 +217,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
// And now for the content
$wgOut->setSyndicated( true );
-
+
$list = ChangesList::newFromUser( $wgUser );
if ( $wgAllowCategorizedRecentChanges ) {
@@ -233,6 +229,10 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
$s = $list->beginRecentChangesList();
$counter = 1;
+
+ $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' );
+ $watcherCache = array();
+
foreach( $rows as $obj ){
if( $limit == 0) {
break;
@@ -251,13 +251,19 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
$rc->notificationtimestamp = false;
}
- if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
- $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" . $dbr->strencode($obj->rc_title) ."' AND wl_namespace=$obj->rc_namespace" ;
- $res3 = $dbr->query( $sql3, 'wfSpecialRecentChanges');
- $x = $dbr->fetchObject( $res3 );
- $rc->numberofWatchingusers = $x->n;
- } else {
- $rc->numberofWatchingusers = 0;
+ $rc->numberofWatchingusers = 0; // Default
+ if ($showWatcherCount && $obj->rc_namespace >= 0) {
+ if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) {
+ $watcherCache[$obj->rc_namespace][$obj->rc_title] =
+ $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ . '-watchers' );
+ }
+ $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
}
$s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
--$limit;
@@ -269,6 +275,10 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
}
function rcFilterByCategories ( &$rows , $categories , $any ) {
+ if( empty( $categories ) ) {
+ return;
+ }
+
# Filter categories
$cats = array () ;
foreach ( $categories AS $cat ) {
@@ -281,7 +291,7 @@ function rcFilterByCategories ( &$rows , $categories , $any ) {
$articles = array () ;
$a2r = array () ;
foreach ( $rows AS $k => $r ) {
- $nt = Title::newFromText ( $r->rc_title , $r->rc_namespace ) ;
+ $nt = Title::makeTitle( $r->rc_title , $r->rc_namespace );
$id = $nt->getArticleID() ;
if ( $id == 0 ) continue ; # Page might have been deleted...
if ( !in_array ( $id , $articles ) ) {
@@ -332,6 +342,14 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ),
$wgTitle->getFullUrl() );
+ //purge cache if requested
+ global $wgRequest, $wgUser;
+ $purge = $wgRequest->getVal( 'action' ) == 'purge';
+ if ( $purge && $wgUser->isAllowed('purge') ) {
+ $messageMemc->delete( $timekey );
+ $messageMemc->delete( $key );
+ }
+
/**
* Bumping around loading up diffs can be pretty slow, so where
* possible we want to cache the feed output so the next visitor
@@ -373,9 +391,12 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
return true;
}
+/**
+ * @todo document
+ * @param $rows Database resource with recentchanges rows
+ */
function rcDoOutputFeed( $rows, &$feed ) {
- $fname = 'rcDoOutputFeed';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$feed->outHeader();
@@ -400,7 +421,7 @@ function rcDoOutputFeed( $rows, &$feed ) {
$item = new FeedItem(
$title->getPrefixedText(),
rcFormatDiff( $obj ),
- $title->getFullURL(),
+ $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ),
$obj->rc_timestamp,
$obj->rc_user_text,
$talkpage->getFullURL()
@@ -408,7 +429,7 @@ function rcDoOutputFeed( $rows, &$feed ) {
$feed->outItem( $item );
}
$feed->outFooter();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -622,7 +643,13 @@ function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) {
$skin = $wgUser->getSkin();
$completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
- if( $title->getNamespace() >= 0 && $title->userCan( 'read' ) ) {
+ //NOTE: Check permissions for anonymous users, not current user.
+ // No "privileged" version should end up in the cache.
+ // Most feed readers will not log in anway.
+ $anon = new User();
+ $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
+
+ if( $title->getNamespace() >= 0 && !$accErrors ) {
if( $oldid ) {
wfProfileIn( "$fname-dodiff" );
diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php
index 2a8ac32d..bc6bbf4a 100644
--- a/includes/SpecialRecentchangeslinked.php
+++ b/includes/SpecialRecentchangeslinked.php
@@ -14,7 +14,7 @@ require_once( 'SpecialRecentchanges.php' );
* @param string $par parent page we will look at
*/
function wfSpecialRecentchangeslinked( $par = NULL ) {
- global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest;
+ global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle;
$fname = 'wfSpecialRecentchangeslinked';
$days = $wgRequest->getInt( 'days' );
@@ -36,7 +36,8 @@ function wfSpecialRecentchangeslinked( $par = NULL ) {
$id = $nt->getArticleId();
$wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) );
- $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) );
+ $wgOut->setSyndicated();
+ $wgOut->setFeedAppendQuery( "target=" . urlencode( $target ) );
if ( ! $days ) {
$days = (int)$wgUser->getOption( 'rcdays', 7 );
@@ -75,7 +76,7 @@ function wfSpecialRecentchangeslinked( $par = NULL ) {
// If target is a Category, use categorylinks and invert from and to
if( $nt->getNamespace() == NS_CATEGORY ) {
- $catkey = $dbr->addQuotes( $nt->getDBKey() );
+ $catkey = $dbr->addQuotes( $nt->getDBkey() );
$sql = "SELECT /* wfSpecialRecentchangeslinked */
rc_id,
rc_cur_id,
@@ -152,6 +153,7 @@ $GROUPBY
$s = $list->beginRecentChangesList();
$count = $dbr->numRows( $res );
+ $rchanges = array();
if ( $count ) {
$counter = 1;
while ( $limit ) {
@@ -162,14 +164,27 @@ $GROUPBY
$rc->counter = $counter++;
$s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) );
--$limit;
+ $rchanges[] = $obj;
}
} else {
- $wgOut->addWikiText( wfMsg('recentchangeslinked-noresult') );
+ $wgOut->addWikiMsg('recentchangeslinked-noresult');
}
$s .= $list->endRecentChangesList();
$dbr->freeResult( $res );
$wgOut->addHTML( $s );
+
+ global $wgSitename, $wgFeedClasses, $wgContLanguageCode;
+ $feedFormat = $wgRequest->getVal( 'feed' );
+ if( $feedFormat && isset( $wgFeedClasses[$feedFormat] ) ) {
+ $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchangeslinked-title', $nt->getPrefixedText() ) . ' [' . $wgContLanguageCode . ']';
+ $feed = new $wgFeedClasses[$feedFormat]( $feedTitle,
+ htmlspecialchars( wfMsgForContent('recentchangeslinked') ), $wgTitle->getFullUrl() );
+
+ require_once( "SpecialRecentchanges.php" );
+ $wgOut->disable();
+ rcDoOutputFeed( $rchanges, $feed );
+ }
}
diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php
index 281a78b6..2ecd15b0 100644
--- a/includes/SpecialResetpass.php
+++ b/includes/SpecialResetpass.php
@@ -25,7 +25,7 @@ class PasswordResetForm extends SpecialPage {
/**
* Main execution point
*/
- function execute( $par='' ) {
+ function execute( $par ) {
global $wgUser, $wgAuth, $wgOut, $wgRequest;
if( !$wgAuth->allowPasswordChange() ) {
@@ -43,7 +43,7 @@ class PasswordResetForm extends SpecialPage {
$retype = $wgRequest->getVal( 'wpRetype' );
try {
$this->attemptReset( $newpass, $retype );
- $wgOut->addWikiText( wfMsg( 'resetpass_success' ) );
+ $wgOut->addWikiMsg( 'resetpass_success' );
$data = array(
'action' => 'submitlogin',
diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php
index 34e9dfbc..b6ca7e14 100644
--- a/includes/SpecialRevisiondelete.php
+++ b/includes/SpecialRevisiondelete.php
@@ -66,7 +66,7 @@ class RevisionDeleteForm {
function show( $request ) {
global $wgOut, $wgUser;
- $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) );
+ $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText() );
$wgOut->addHtml( "<ul>" );
foreach( $this->revisions as $revid ) {
@@ -80,7 +80,7 @@ class RevisionDeleteForm {
}
$wgOut->addHtml( "</ul>" );
- $wgOut->addWikiText( wfMsg( 'revdelete-text' ) );
+ $wgOut->addWikiMsg( 'revdelete-text' );
$items = array(
wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php
index 3fc8bab4..dcbbb903 100644
--- a/includes/SpecialSearch.php
+++ b/includes/SpecialSearch.php
@@ -103,7 +103,11 @@ class SpecialSearch {
return;
}
}
- $wgOut->addWikiText( wfMsg( 'noexactmatch', wfEscapeWikiText( $term ) ) );
+ if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
+ $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
+ } else {
+ $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
+ }
return $this->showResults( $term );
}
@@ -119,12 +123,13 @@ class SpecialSearch {
$this->setupPage( $term );
global $wgOut;
- $wgOut->addWikiText( wfMsg( 'searchresulttext' ) );
+ $wgOut->addWikiMsg( 'searchresulttext' );
- #if ( !$this->parseQuery() ) {
if( '' === trim( $term ) ) {
+ // Empty query -- straight view of search form
$wgOut->setSubtitle( '' );
$wgOut->addHTML( $this->powerSearchBox( $term ) );
+ $wgOut->addHTML( $this->powerSearchFocus() );
wfProfileOut( $fname );
return;
}
@@ -155,6 +160,15 @@ class SpecialSearch {
$search->setNamespaces( $this->namespaces );
$search->showRedirects = $this->searchRedirects;
$titleMatches = $search->searchTitle( $term );
+
+ // Sometimes the search engine knows there are too many hits
+ if ($titleMatches instanceof SearchResultTooMany) {
+ $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ $wgOut->addHTML( $this->powerSearchFocus() );
+ wfProfileOut( $fname );
+ return;
+ }
$textMatches = $search->searchText( $term );
$num = ( $titleMatches ? $titleMatches->numRows() : 0 )
@@ -180,27 +194,27 @@ class SpecialSearch {
if( $titleMatches ) {
if( $titleMatches->numRows() ) {
- $wgOut->addWikiText( '==' . wfMsg( 'titlematches' ) . "==\n" );
+ $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
$wgOut->addHTML( $this->showMatches( $titleMatches ) );
} else {
- $wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" );
+ $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
}
$titleMatches->free();
}
if( $textMatches ) {
if( $textMatches->numRows() ) {
- $wgOut->addWikiText( '==' . wfMsg( 'textmatches' ) . "==\n" );
+ $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
$wgOut->addHTML( $this->showMatches( $textMatches ) );
} elseif( $num == 0 ) {
# Don't show the 'no text matches' if we received title matches
- $wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" );
+ $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
}
$textMatches->free();
}
if ( $num == 0 ) {
- $wgOut->addWikiText( wfMsg( 'nonefound' ) );
+ $wgOut->addWikiMsg( 'nonefound' );
}
if( $num || $this->offset ) {
$wgOut->addHTML( "<p>{$prevnext}</p>\n" );
@@ -387,8 +401,9 @@ class SpecialSearch {
if( '' == $name ) {
$name = wfMsg( 'blanknamespace' );
}
+ $encName = htmlspecialchars( $name );
$namespaces .= " <label><input type='checkbox' value=\"1\" name=\"" .
- "ns{$ns}\"{$checked} />{$name}</label>\n";
+ "ns{$ns}\"{$checked} />{$encName}</label>\n";
}
$checked = $this->searchRedirects
@@ -396,7 +411,7 @@ class SpecialSearch {
: '';
$redirect = "<input type='checkbox' value='1' name=\"redirs\"{$checked} />\n";
- $searchField = '<input type="text" name="search" value="' .
+ $searchField = '<input type="text" id="powerSearchText" name="search" value="' .
htmlspecialchars( $term ) ."\" size=\"16\" />\n";
$searchButton = '<input type="submit" name="searchx" value="' .
@@ -412,6 +427,12 @@ class SpecialSearch {
return "<br /><br />\n<form id=\"powersearch\" method=\"get\" " .
"action=\"$action\">\n{$ret}\n</form>\n";
}
+
+ function powerSearchFocus() {
+ return "<script type='text/javascript'>" .
+ "document.getElementById('powerSearchText').focus();" .
+ "</script>";
+ }
}
diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php
index 973656dd..5aa36386 100644
--- a/includes/SpecialShortpages.php
+++ b/includes/SpecialShortpages.php
@@ -41,7 +41,7 @@ class ShortPagesPage extends QueryPage {
WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0";
}
- function preprocessResults( &$db, &$res ) {
+ function preprocessResults( $db, $res ) {
# There's no point doing a batch check if we aren't caching results;
# the page must exist for it to have been pulled out of the table
if( $this->isCached() ) {
diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php
index a893966c..4ea956b8 100644
--- a/includes/SpecialSpecialpages.php
+++ b/includes/SpecialSpecialpages.php
@@ -12,7 +12,7 @@ function wfSpecialSpecialpages() {
$wgMessageCache->loadAllMessages();
- $wgOut->setRobotpolicy( 'index,nofollow' );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed?
$sk = $wgUser->getSkin();
/** Pages available to all */
diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php
index a29811da..983dc896 100644
--- a/includes/SpecialStatistics.php
+++ b/includes/SpecialStatistics.php
@@ -32,7 +32,7 @@ function wfSpecialStatistics( $par = '' ) {
return;
} else {
$text = "__NOTOC__\n";
- $text .= '==' . wfMsg( 'sitestats' ) . "==\n";
+ $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n";
$text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
$wgLang->formatNum( $total ),
$wgLang->formatNum( $good ),
@@ -44,7 +44,7 @@ function wfSpecialStatistics( $par = '' ) {
$wgLang->formatNum( $images )
)."\n";
- $text .= "==" . wfMsg( 'userstats' ) . "==\n";
+ $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n";
$text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ),
$wgLang->formatNum( $users ),
$wgLang->formatNum( $admins ),
@@ -73,7 +73,7 @@ function wfSpecialStatistics( $par = '' ) {
)
);
if( $res->numRows() > 0 ) {
- $text .= "==" . wfMsg( 'statistics-mostpopular' ) . "==\n";
+ $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
while( $row = $res->fetchObject() ) {
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
if( $title instanceof Title )
@@ -83,11 +83,11 @@ function wfSpecialStatistics( $par = '' ) {
}
}
- $footer = wfMsg( 'statistics-footer' );
+ $footer = wfMsgNoTrans( 'statistics-footer' );
if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
$text .= "\n" . $footer;
$wgOut->addWikiText( $text );
}
-} \ No newline at end of file
+}
diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php
index 5678a81e..e6f6298c 100644
--- a/includes/SpecialUndelete.php
+++ b/includes/SpecialUndelete.php
@@ -57,7 +57,7 @@ class PageArchive {
$title = Title::newFromText( $prefix );
if( $title ) {
$ns = $title->getNamespace();
- $encPrefix = $dbr->escapeLike( $title->getDbKey() );
+ $encPrefix = $dbr->escapeLike( $title->getDBkey() );
} else {
// Prolly won't work too good
// @todo handle bare namespace names cleanly?
@@ -132,7 +132,7 @@ class PageArchive {
'fa_user',
'fa_user_text',
'fa_timestamp' ),
- array( 'fa_name' => $this->title->getDbKey() ),
+ array( 'fa_name' => $this->title->getDBkey() ),
__METHOD__,
array( 'ORDER BY' => 'fa_timestamp DESC' ) );
$ret = $dbr->resultObject( $res );
@@ -174,7 +174,7 @@ class PageArchive {
'ar_text_id',
'ar_len' ),
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDbkey(),
+ 'ar_title' => $this->title->getDBkey(),
'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
__METHOD__ );
if( $row ) {
@@ -194,6 +194,59 @@ class PageArchive {
return null;
}
}
+
+ /**
+ * Return the most-previous revision, either live or deleted, against
+ * the deleted revision given by timestamp.
+ *
+ * May produce unexpected results in case of history merges or other
+ * unusual time issues.
+ *
+ * @param string $timestamp
+ * @return Revision or null
+ */
+ function getPreviousRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Check the previous deleted revision...
+ $row = $dbr->selectRow( 'archive',
+ 'ar_timestamp',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'ar_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+ $row = $dbr->selectRow( array( 'page', 'revision' ),
+ array( 'rev_id', 'rev_timestamp' ),
+ array(
+ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey(),
+ 'page_id = rev_page',
+ 'rev_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'rev_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+ $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+ if( $prevLive && $prevLive > $prevDeleted ) {
+ // Most prior revision was live
+ return Revision::newFromId( $prevLiveId );
+ } elseif( $prevDeleted ) {
+ // Most prior revision was deleted
+ return $this->getRevision( $prevDeleted );
+ } else {
+ // No prior revision on this page.
+ return null;
+ }
+ }
/**
* Get the text from an archive row containing ar_text, ar_flags and ar_text_id
@@ -259,7 +312,8 @@ class PageArchive {
* @param string $comment
* @param array $fileVersions
*
- * @return true on success.
+ * @return array(number of file revisions restored, number of image revisions restored, log message)
+ * on success, false on failure
*/
function undelete( $timestamps, $comment = '', $fileVersions = array() ) {
// If both the set of text revisions and file revisions are empty,
@@ -279,6 +333,8 @@ class PageArchive {
if( $restoreText ) {
$textRestored = $this->undeleteRevisions( $timestamps );
+ if($textRestored === false) // It must be one of UNDELETE_*
+ return false;
} else {
$textRestored = 0;
}
@@ -288,14 +344,14 @@ class PageArchive {
$log = new LogPage( 'delete' );
if( $textRestored && $filesRestored ) {
- $reason = wfMsgForContent( 'undeletedrevisions-files',
+ $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
$wgContLang->formatNum( $textRestored ),
$wgContLang->formatNum( $filesRestored ) );
} elseif( $textRestored ) {
- $reason = wfMsgForContent( 'undeletedrevisions',
+ $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
$wgContLang->formatNum( $textRestored ) );
} elseif( $filesRestored ) {
- $reason = wfMsgForContent( 'undeletedfiles',
+ $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
$wgContLang->formatNum( $filesRestored ) );
} else {
wfDebug( "Undelete: nothing undeleted...\n" );
@@ -306,11 +362,7 @@ class PageArchive {
$reason .= ": {$comment}";
$log->addEntry( 'restore', $this->title, $reason );
- if ( $this->fileStatus && !$this->fileStatus->ok ) {
- return false;
- } else {
- return true;
- }
+ return array($textRestored, $filesRestored, $reason);
}
/**
@@ -322,9 +374,12 @@ class PageArchive {
* @param string $comment
* @param array $fileVersions
*
- * @return int number of revisions restored
+ * @return mixed number of revisions restored or false on failure
*/
private function undeleteRevisions( $timestamps ) {
+ if ( wfReadOnly() )
+ return false;
+
$restoreAll = empty( $timestamps );
$dbw = wfGetDB( DB_MASTER );
@@ -376,6 +431,7 @@ class PageArchive {
'ar_minor_edit',
'ar_flags',
'ar_text_id',
+ 'ar_page_id',
'ar_len' ),
/* WHERE */ array(
'ar_namespace' => $this->title->getNamespace(),
@@ -420,7 +476,12 @@ class PageArchive {
) );
$revision->insertOn( $dbw );
$restored++;
+
+ wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
}
+ // Was anything restored at all?
+ if($restored == 0)
+ return 0;
if( $revision ) {
// Attach the latest revision to the page...
@@ -438,8 +499,14 @@ class PageArchive {
wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
Article::onArticleEdit( $this->title );
}
+
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
+ $update->doUpdate();
+ }
} else {
- # Something went terribly wrong!
+ // Revision couldn't be created. This is very weird
+ return self::UNDELETE_UNKNOWNERR;
}
# Now that it's safely stored, take it out of the archive
@@ -478,12 +545,13 @@ class UndeleteForm {
$wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
$this->mRestore = $request->getCheck( 'restore' ) && $posted;
$this->mPreview = $request->getCheck( 'preview' ) && $posted;
+ $this->mDiff = $request->getCheck( 'diff' );
$this->mComment = $request->getText( 'wpComment' );
if( $par != "" ) {
$this->mTarget = $par;
}
- if ( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
+ if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
$this->mAllowed = true;
} else {
$this->mAllowed = false;
@@ -546,7 +614,7 @@ class UndeleteForm {
function showSearchForm() {
global $wgOut, $wgScript;
- $wgOut->addWikiText( wfMsg( 'undelete-header' ) );
+ $wgOut->addWikiMsg( 'undelete-header' );
$wgOut->addHtml(
Xml::openElement( 'form', array(
@@ -569,11 +637,11 @@ class UndeleteForm {
global $wgLang, $wgContLang, $wgUser, $wgOut;
if( $result->numRows() == 0 ) {
- $wgOut->addWikiText( wfMsg( 'undelete-no-results' ) );
+ $wgOut->addWikiMsg( 'undelete-no-results' );
return;
}
- $wgOut->addWikiText( wfMsg( "undeletepagetext" ) );
+ $wgOut->addWikiMsg( "undeletepagetext" );
$sk = $wgUser->getSkin();
$undelete = SpecialPage::getTitleFor( 'Undelete' );
@@ -604,21 +672,34 @@ class UndeleteForm {
$rev = $archive->getRevision( $timestamp );
if( !$rev ) {
- $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) );
+ $wgOut->addWikiMsg( 'undeleterevision-missing' );
return;
}
$wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
$link = $skin->makeKnownLinkObj(
- $self,
- htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
- 'target=' . $this->mTargetObj->getPrefixedUrl()
+ SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
+ htmlspecialchars( $this->mTargetObj->getPrefixedText() )
);
- $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp ) );
+ $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
$user = $skin->userLink( $rev->getUser(), $rev->getUserText() )
. $skin->userToolLinks( $rev->getUser(), $rev->getUserText() );
-
+
+ if( $this->mDiff ) {
+ $previousRev = $archive->getPreviousRevision( $timestamp );
+ if( $previousRev ) {
+ $this->showDiff( $previousRev, $rev );
+ if( $wgUser->getOption( 'diffonly' ) ) {
+ return;
+ } else {
+ $wgOut->addHtml( '<hr />' );
+ }
+ } else {
+ $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
+ }
+ }
+
$wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
@@ -651,17 +732,84 @@ class UndeleteForm {
'name' => 'wpEditToken',
'value' => $wgUser->editToken() ) ) .
wfElement( 'input', array(
- 'type' => 'hidden',
+ 'type' => 'submit',
'name' => 'preview',
- 'value' => '1' ) ) .
+ 'value' => wfMsg( 'showpreview' ) ) ) .
wfElement( 'input', array(
+ 'name' => 'diff',
'type' => 'submit',
- 'value' => wfMsg( 'showpreview' ) ) ) .
+ 'value' => wfMsg( 'showdiff' ) ) ) .
wfCloseElement( 'form' ) .
wfCloseElement( 'div' ) );
}
/**
+ * Build a diff display between this and the previous either deleted
+ * or non-deleted edit.
+ * @param Revision $previousRev
+ * @param Revision $currentRev
+ * @return string HTML
+ */
+ function showDiff( $previousRev, $currentRev ) {
+ global $wgOut, $wgUser;
+
+ $diffEngine = new DifferenceEngine();
+ $diffEngine->showDiffStyle();
+ $wgOut->addHtml(
+ "<div>" .
+ "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<tr>" .
+ "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+ $this->diffHeader( $previousRev ) .
+ "</td>" .
+ "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+ $this->diffHeader( $currentRev ) .
+ "</td>" .
+ "</tr>" .
+ $diffEngine->generateDiffBody(
+ $previousRev->getText(), $currentRev->getText() ) .
+ "</table>" .
+ "</div>\n" );
+
+ }
+
+ private function diffHeader( $rev ) {
+ global $wgUser, $wgLang, $wgLang;
+ $sk = $wgUser->getSkin();
+ $isDeleted = !( $rev->getId() && $rev->getTitle() );
+ if( $isDeleted ) {
+ /// @fixme $rev->getTitle() is null for deleted revs...?
+ $targetPage = SpecialPage::getTitleFor( 'Undelete' );
+ $targetQuery = 'target=' .
+ $this->mTargetObj->getPrefixedUrl() .
+ '&timestamp=' .
+ wfTimestamp( TS_MW, $rev->getTimestamp() );
+ } else {
+ /// @fixme getId() may return non-zero for deleted revs...
+ $targetPage = $rev->getTitle();
+ $targetQuery = 'oldid=' . $rev->getId();
+ }
+ return
+ '<div id="mw-diff-otitle1"><strong>' .
+ $sk->makeLinkObj( $targetPage,
+ wfMsgHtml( 'revisionasof',
+ $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+ $targetQuery ) .
+ ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+ '</strong></div>' .
+ '<div id="mw-diff-otitle2">' .
+ $sk->revUserTools( $rev ) . '<br/>' .
+ '</div>' .
+ '<div id="mw-diff-otitle3">' .
+ $sk->revComment( $rev ) . '<br/>' .
+ '</div>';
+ }
+
+ /**
* Show a deleted file version requested by the visitor.
*/
function showFile( $key ) {
@@ -694,14 +842,14 @@ class UndeleteForm {
/*
$text = $archive->getLastRevisionText();
if( is_null( $text ) ) {
- $wgOut->addWikiText( wfMsg( "nohistory" ) );
+ $wgOut->addWikiMsg( "nohistory" );
return;
}
*/
if ( $this->mAllowed ) {
- $wgOut->addWikiText( wfMsg( "undeletehistory" ) );
+ $wgOut->addWikiMsg( "undeletehistory" );
} else {
- $wgOut->addWikiText( wfMsg( "undeletehistorynoadmin" ) );
+ $wgOut->addWikiMsg( "undeletehistorynoadmin" );
}
# List all stored revisions
@@ -792,16 +940,32 @@ class UndeleteForm {
# The page's stored (deleted) history:
$wgOut->addHTML("<ul>");
$target = urlencode( $this->mTarget );
+ $remaining = $revisions->numRows();
+ $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
+
while( $row = $revisions->fetchObject() ) {
+ $remaining--;
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
if ( $this->mAllowed ) {
$checkBox = Xml::check( "ts$ts" );
$pageLink = $sk->makeKnownLinkObj( $titleObj,
$wgLang->timeanddate( $ts, true ),
"target=$target&timestamp=$ts" );
+ if( ($remaining > 0) ||
+ ($earliestLiveTime && $ts > $earliestLiveTime ) ) {
+ $diffLink = '(' .
+ $sk->makeKnownLinkObj( $titleObj,
+ wfMsgHtml( 'diff' ),
+ "target=$target&timestamp=$ts&diff=prev" ) .
+ ')';
+ } else {
+ // No older revision to diff against
+ $diffLink = '';
+ }
} else {
$checkBox = '';
$pageLink = $wgLang->timeanddate( $ts, true );
+ $diffLink = '';
}
$userLink = $sk->userLink( $row->ar_user, $row->ar_user_text ) . $sk->userToolLinks( $row->ar_user, $row->ar_user_text );
$stxt = '';
@@ -813,13 +977,13 @@ class UndeleteForm {
}
}
$comment = $sk->commentBlock( $row->ar_comment );
- $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $stxt $comment</li>\n" );
+ $wgOut->addHTML( "<li>$checkBox $pageLink $diffLink . . $userLink $stxt $comment</li>\n" );
}
$revisions->free();
$wgOut->addHTML("</ul>");
} else {
- $wgOut->addWikiText( wfMsg( "nohistory" ) );
+ $wgOut->addWikiMsg( "nohistory" );
}
if( $haveFiles ) {
@@ -863,9 +1027,25 @@ class UndeleteForm {
return true;
}
+
+ private function getEarliestTime( $title ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ if( $title->exists() ) {
+ $min = $dbr->selectField( 'revision',
+ 'MIN(rev_timestamp)',
+ array( 'rev_page' => $title->getArticleId() ),
+ __METHOD__ );
+ return wfTimestampOrNull( TS_MW, $min );
+ }
+ return null;
+ }
function undelete() {
global $wgOut, $wgUser;
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
if( !is_null( $this->mTargetObj ) ) {
$archive = new PageArchive( $this->mTargetObj );
@@ -874,7 +1054,7 @@ class UndeleteForm {
$this->mComment,
$this->mFileVersions );
- if( $ok ) {
+ if( is_array($ok) ) {
$skin = $wgUser->getSkin();
$link = $skin->makeKnownLinkObj( $this->mTargetObj );
$wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
@@ -893,5 +1073,3 @@ class UndeleteForm {
return false;
}
}
-
-
diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php
index 52025e53..74b794dd 100644
--- a/includes/SpecialUnlockdb.php
+++ b/includes/SpecialUnlockdb.php
@@ -39,12 +39,12 @@ class DBUnlockForm {
global $wgReadOnlyFile;
if( !file_exists( $wgReadOnlyFile ) ) {
- $wgOut->addWikiText( wfMsg( 'databasenotlocked' ) );
+ $wgOut->addWikiMsg( 'databasenotlocked' );
return;
}
$wgOut->setPagetitle( wfMsg( "unlockdb" ) );
- $wgOut->addWikiText( wfMsg( "unlockdbtext" ) );
+ $wgOut->addWikiMsg( "unlockdbtext" );
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsg( "formerror" ) );
@@ -103,7 +103,7 @@ END
$wgOut->setPagetitle( wfMsg( "unlockdb" ) );
$wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
- $wgOut->addWikiText( wfMsg( "unlockdbsuccesstext", $ip ) );
+ $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip );
}
}
diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php
index 52aa19d2..623137c0 100644
--- a/includes/SpecialUnusedimages.php
+++ b/includes/SpecialUnusedimages.php
@@ -9,7 +9,9 @@
* @addtogroup SpecialPage
*/
class UnusedimagesPage extends ImageQueryPage {
-
+
+ function isExpensive() { return true; }
+
function getName() {
return 'Unusedimages';
}
@@ -26,21 +28,23 @@ class UnusedimagesPage extends ImageQueryPage {
if ( $wgCountCategorizedImagesAsUsed ) {
list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
- return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description
- FROM ((('.$page.' AS I LEFT JOIN '.$categorylinks.' AS L ON I.page_id = L.cl_from)
- LEFT JOIN '.$imagelinks.' AS P ON I.page_title = P.il_to)
- INNER JOIN '.$image.' AS G ON I.page_title = G.img_name)
- WHERE I.page_namespace = '.NS_IMAGE.' AND L.cl_from IS NULL AND P.il_to IS NULL';
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from)
+ LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to)
+ INNER JOIN $image AS G ON I.page_title = G.img_name)
+ WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL";
} else {
list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
- return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description' .
- ' FROM '.$image.' LEFT JOIN '.$imagelinks.' ON img_name=il_to WHERE il_to IS NULL ';
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ FROM $image LEFT JOIN $imagelinks ON img_name=il_to WHERE il_to IS NULL ";
}
}
function getPageHeader() {
- return wfMsg( "unusedimagestext" );
+ return wfMsgExt( 'unusedimagestext', array( 'parse') );
}
}
diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php
index 18c6dd9e..36bae4f7 100644
--- a/includes/SpecialUpload.php
+++ b/includes/SpecialUpload.php
@@ -19,6 +19,21 @@ function wfSpecialUpload() {
* @addtogroup SpecialPage
*/
class UploadForm {
+ const SUCCESS = 0;
+ const BEFORE_PROCESSING = 1;
+ const LARGE_FILE_SERVER = 2;
+ const EMPTY_FILE = 3;
+ const MIN_LENGHT_PARTNAME = 4;
+ const ILLEGAL_FILENAME = 5;
+ const PROTECTED_PAGE = 6;
+ const OVERWRITE_EXISTING_FILE = 7;
+ const FILETYPE_MISSING = 8;
+ const FILETYPE_BADTYPE = 9;
+ const VERIFICATION_ERROR = 10;
+ const UPLOAD_VERIFICATION_ERROR = 11;
+ const UPLOAD_WARNING = 12;
+ const INTERNAL_ERROR = 13;
+
/**#@+
* @access private
*/
@@ -126,7 +141,8 @@ class UploadForm {
$this->mTempPath = $local_file;
$this->mFileSize = 0; # Will be set by curlCopy
$this->mCurlError = $this->curlCopy( $url, $local_file );
- $this->mSrcName = array_pop( explode( '/', $url ) );
+ $pathParts = explode( '/', $url );
+ $this->mSrcName = array_pop( $pathParts );
$this->mSessionKey = false;
$this->mStashed = false;
@@ -253,34 +269,123 @@ class UploadForm {
$this->cleanupTempFile();
}
- /* -------------------------------------------------------------- */
+ /**
+ * Do the upload
+ * Checks are made in SpecialUpload::execute()
+ *
+ * @access private
+ */
+ function processUpload(){
+ global $wgUser, $wgOut, $wgFileExtensions;
+ $details = null;
+ $value = null;
+ $value = $this->internalProcessUpload( $details );
+
+ switch($value) {
+ case self::SUCCESS:
+ $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
+ break;
+
+ case self::BEFORE_PROCESSING:
+ break;
+
+ case self::LARGE_FILE_SERVER:
+ $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
+ break;
+
+ case self::EMPTY_FILE:
+ $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
+ break;
+
+ case self::MIN_LENGHT_PARTNAME:
+ $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
+ break;
+
+ case self::ILLEGAL_FILENAME:
+ $filtered = $details['filtered'];
+ $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
+ break;
+
+ case self::PROTECTED_PAGE:
+ $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) );
+ break;
+
+ case self::OVERWRITE_EXISTING_FILE:
+ $errorText = $details['overwrite'];
+ $overwrite = new WikiError( $wgOut->parse( $errorText ) );
+ $this->uploadError( $overwrite->toString() );
+ break;
+
+ case self::FILETYPE_MISSING:
+ $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
+ break;
+
+ case self::FILETYPE_BADTYPE:
+ $finalExt = $details['finalExt'];
+ $this->uploadError(
+ wfMsgExt( 'filetype-banned-type',
+ array( 'parseinline' ),
+ htmlspecialchars( $finalExt ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ )
+ )
+ );
+ break;
+
+ case self::VERIFICATION_ERROR:
+ $veri = $details['veri'];
+ $this->uploadError( $veri->toString() );
+ break;
+
+ case self::UPLOAD_VERIFICATION_ERROR:
+ $error = $details['error'];
+ $this->uploadError( $error );
+ break;
+
+ case self::UPLOAD_WARNING:
+ $warning = $details['warning'];
+ $this->uploadWarning( $warning );
+ break;
+
+ case self::INTERNAL_ERROR:
+ $internal = $details['internal'];
+ $this->showError( $internal );
+ break;
+
+ default:
+ throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
+ }
+ }
/**
* Really do the upload
* Checks are made in SpecialUpload::execute()
+ *
+ * @param array $resultDetails contains result-specific dict of additional values
+ *
* @access private
*/
- function processUpload() {
- global $wgUser, $wgOut;
+ function internalProcessUpload( &$resultDetails ) {
+ global $wgUser;
if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
{
wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
- return false;
+ return self::BEFORE_PROCESSING;
}
/* Check for PHP error if any, requires php 4.2 or newer */
if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
- $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
- return;
+ return self::LARGE_FILE_SERVER;
}
/**
* If there was no filename or a zero size given, give up quick.
*/
if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
- $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
- return;
+ return self::EMPTY_FILE;
}
# Chop off any directories in the given filename
@@ -311,8 +416,7 @@ class UploadForm {
}
if( strlen( $partname ) < 1 ) {
- $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
- return;
+ return self::MIN_LENGHT_PARTNAME;
}
/**
@@ -322,8 +426,8 @@ class UploadForm {
$filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered );
$nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
if( is_null( $nt ) ) {
- $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
- return;
+ $resultDetails = array( 'filtered' => $filtered );
+ return self::ILLEGAL_FILENAME;
}
$this->mLocalFile = wfLocalFile( $nt );
$this->mDestName = $this->mLocalFile->getName();
@@ -332,27 +436,28 @@ class UploadForm {
* If the image is protected, non-sysop users won't be able
* to modify it by uploading a new revision.
*/
- if( !$nt->userCan( 'edit' ) ) {
- return $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) );
+ if( !$nt->userCan( 'edit' ) || !$nt->userCan( 'create' ) ) {
+ return self::PROTECTED_PAGE;
}
/**
* In some cases we may forbid overwriting of existing files.
*/
$overwrite = $this->checkOverwrite( $this->mDestName );
- if( WikiError::isError( $overwrite ) ) {
- return $this->uploadError( $overwrite->toString() );
+ if( $overwrite !== true ) {
+ $resultDetails = array( 'overwrite' => $overwrite );
+ return self::OVERWRITE_EXISTING_FILE;
}
/* Don't allow users to override the blacklist (check file extension) */
global $wgStrictFileExtensions;
global $wgFileExtensions, $wgFileBlacklist;
if ($finalExt == '') {
- return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
+ return self::FILETYPE_MISSING;
} elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
- return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ),
- htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) );
+ $resultDetails = array( 'finalExt' => $finalExt );
+ return self::FILETYPE_BADTYPE;
}
/**
@@ -366,7 +471,8 @@ class UploadForm {
$veri = $this->verify( $this->mTempPath, $finalExt );
if( $veri !== true ) { //it's a wiki error...
- return $this->uploadError( $veri->toString() );
+ $resultDetails = array( 'veri' => $veri );
+ return self::VERIFICATION_ERROR;
}
/**
@@ -375,7 +481,8 @@ class UploadForm {
$error = '';
if( !wfRunHooks( 'UploadVerification',
array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
- return $this->uploadError( $error );
+ $resultDetails = array( 'error' => $error );
+ return self::UPLOAD_VERIFICATION_ERROR;
}
}
@@ -396,9 +503,16 @@ class UploadForm {
global $wgCheckFileExtensions;
if ( $wgCheckFileExtensions ) {
- if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
- $warning .= '<li>'.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ),
- htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'</li>';
+ if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
+ $warning .= '<li>' .
+ wfMsgExt( 'filetype-unwanted-type',
+ array( 'parseinline' ),
+ htmlspecialchars( $finalExt ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ )
+ ) . '</li>';
}
}
@@ -421,7 +535,8 @@ class UploadForm {
* Stash the file in a temporary location; the user can choose
* to let it through and we'll complete the upload then.
*/
- return $this->uploadWarning( $warning );
+ $resultDetails = array( 'warning' => $warning );
+ return self::UPLOAD_WARNING;
}
}
@@ -432,19 +547,20 @@ class UploadForm {
$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
$this->mCopyrightStatus, $this->mCopyrightSource );
- $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+ $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
File::DELETE_SOURCE, $this->mFileProps );
if ( !$status->isGood() ) {
- $this->showError( $status->getWikiText() );
+ $resultDetails = array( 'internal' => $status->getWikiText() );
+ return self::INTERNAL_ERROR;
} else {
if ( $this->mWatchthis ) {
global $wgUser;
$wgUser->addWatch( $this->mLocalFile->getTitle() );
}
// Success, redirect to description page
- $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
$img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
- wfRunHooks( 'UploadComplete', array( &$img ) );
+ wfRunHooks( 'UploadComplete', array( &$this ) );
+ return self::SUCCESS;
}
}
@@ -455,11 +571,12 @@ class UploadForm {
* Returns an empty string if there is no warning
*/
static function getExistsWarning( $file ) {
- global $wgUser;
+ global $wgUser, $wgContLang;
// Check for uppercase extension. We allow these filenames but check if an image
// with lowercase extension exists already
$warning = '';
-
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
if( strpos( $file->getName(), '.' ) == false ) {
$partname = $file->getName();
$rawExtension = '';
@@ -483,28 +600,31 @@ class UploadForm {
if( $file->exists() ) {
$dlink = $sk->makeKnownLinkObj( $file->getTitle() );
if ( $file->allowInlineDisplay() ) {
- $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
- $file->getName(), 'right', array(), false, true );
+ $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $file->getName(), $align, array(), false, true );
} elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
$icon = $file->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
$icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
} else {
$dlink2 = '';
}
- $warning .= '<li>' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '</li>' . $dlink2;
+ $warning .= '<li>' . wfMsgExt( 'fileexists', array(), $dlink ) . '</li>' . $dlink2;
+ } elseif( $file->getTitle()->getArticleID() ) {
+ $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' );
+ $warning .= '<li>' . wfMsgExt( 'filepageexists', array(), $lnk ) . '</li>';
} elseif ( $file_lc && $file_lc->exists() ) {
# Check if image with lowercase extension exists.
# It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
$dlink = $sk->makeKnownLinkObj( $nt_lc );
if ( $file_lc->allowInlineDisplay() ) {
- $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
- $nt_lc->getText(), 'right', array(), false, true );
+ $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $nt_lc->getText(), $align, array(), false, true );
} elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
$icon = $file_lc->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
$icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
} else {
$dlink2 = '';
@@ -523,11 +643,11 @@ class UploadForm {
$dlink = $sk->makeKnownLinkObj( $nt_thb);
if ( $file_thb->allowInlineDisplay() ) {
$dlink2 = $sk->makeImageLinkObj( $nt_thb,
- wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
- $nt_thb->getText(), 'right', array(), false, true );
+ wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $nt_thb->getText(), $align, array(), false, true );
} elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
$icon = $file_thb->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
$icon->toHtml( array( 'desc-link' => true ) ) . '<br />' .
$dlink . '</div>';
} else {
@@ -542,9 +662,19 @@ class UploadForm {
substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
}
}
- if ( $file->wasDeleted() ) {
+
+ $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
+ # Do the match
+ foreach( $filenamePrefixBlacklist as $prefix ) {
+ if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
+ $warning .= '<li>' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '</li>';
+ break;
+ }
+ }
+
+ if ( $file->wasDeleted() && !$file->exists() ) {
# If the file existed before and was deleted, warn the user of this
- # Don't bother doing so if the image exists now, however
+ # Don't bother doing so if the file exists now, however
$ltitle = SpecialPage::getTitleFor( 'Log' );
$llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
@@ -553,6 +683,12 @@ class UploadForm {
return $warning;
}
+ /**
+ * Get a list of warnings
+ *
+ * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
+ * @return array list of warning messages
+ */
static function ajaxGetExistsWarning( $filename ) {
$file = wfFindFile( $filename );
if( !$file ) {
@@ -590,6 +726,33 @@ class UploadForm {
}
/**
+ * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
+ *
+ * @return array list of prefixes
+ */
+ public static function getFilenamePrefixBlacklist() {
+ $blacklist = array();
+ $message = wfMsgForContent( 'filename-prefix-blacklist' );
+ if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
+ $lines = explode( "\n", $message );
+ foreach( $lines as $line ) {
+ // Remove comment lines
+ $comment = substr( trim( $line ), 0, 1 );
+ if ( $comment == '#' || $comment == '' ) {
+ continue;
+ }
+ // Remove additional comments after a prefix
+ $comment = strpos( $line, '#' );
+ if ( $comment > 0 ) {
+ $line = substr( $line, 0, $comment-1 );
+ }
+ $blacklist[] = trim( $line );
+ }
+ }
+ return $blacklist;
+ }
+
+ /**
* Stash a file in a temporary directory for later processing
* after the user has confirmed it.
*
@@ -752,10 +915,10 @@ class UploadForm {
$useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
$useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
-
+
$adc = wfBoolToStr( $useAjaxDestCheck );
$alp = wfBoolToStr( $useAjaxLicensePreview );
-
+
$wgOut->addScript( "<script type=\"text/javascript\">
wgAjaxUploadDestCheck = {$adc};
wgAjaxLicensePreview = {$alp};
@@ -768,26 +931,35 @@ wgAjaxLicensePreview = {$alp};
wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
return false;
}
-
- if( $this->mDesiredDestName && $wgUser->isAllowed( 'deletedhistory' ) ) {
+
+ if( $this->mDesiredDestName ) {
$title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
- if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 ) {
+ // Show a subtitle link to deleted revisions (to sysops et al only)
+ if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
$link = wfMsgExt(
$wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
array( 'parse', 'replaceafter' ),
$wgUser->getSkin()->makeKnownLinkObj(
SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
- wfMsgHtml( 'restorelink', $count )
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
)
);
$wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
- }
+ }
+
+ // Show the relevant lines from deletion log (for still deleted files only)
+ if( $title instanceof Title && $title->isDeleted() > 0 && !$title->exists() ) {
+ $this->showDeletionLog( $wgOut, $title->getPrefixedText() );
+ }
}
$cols = intval($wgUser->getOption( 'cols' ));
- $ew = $wgUser->getOption( 'editwidth' );
- if ( $ew ) $ew = " style=\"width:100%\"";
- else $ew = '';
+
+ if( $wgUser->getOption( 'editwidth' ) ) {
+ $width = " style=\"width:100%\"";
+ } else {
+ $width = '';
+ }
if ( '' != $msg ) {
$sub = wfMsgHtml( 'uploaderror' );
@@ -795,8 +967,34 @@ wgAjaxLicensePreview = {$alp};
"<span class='error'>{$msg}</span>\n" );
}
$wgOut->addHTML( '<div id="uploadtext">' );
- $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) );
- $wgOut->addHTML( '</div>' );
+ $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName );
+ $wgOut->addHTML( "</div>\n" );
+
+ # Print a list of allowed file extensions, if so configured. We ignore
+ # MIME type here, it's incomprehensible to most people and too long.
+ global $wgCheckFileExtensions, $wgStrictFileExtensions,
+ $wgFileExtensions, $wgFileBlacklist;
+ if( $wgCheckFileExtensions ) {
+ $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
+ if( $wgStrictFileExtensions ) {
+ # Everything not permitted is banned
+ $wgOut->addHTML(
+ '<div id="mw-upload-permitted">' .
+ wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) .
+ "</div>\n"
+ );
+ } else {
+ # We have to list both preferred and prohibited
+ $wgOut->addHTML(
+ '<div id="mw-upload-preferred">' .
+ wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
+ "</div>\n" .
+ '<div id="mw-upload-prohibited">' .
+ wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
+ "</div>\n"
+ );
+ }
+ }
$sourcefilename = wfMsgHtml( 'sourcefilename' );
$destfilename = wfMsgHtml( 'destfilename' );
@@ -881,7 +1079,7 @@ wgAjaxLicensePreview = {$alp};
<td align='$align1'><label for='wpUploadDescription'>{$summary}</label></td>
<td align='$align2'>
<textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6'
- cols='{$cols}'{$ew}>$encComment</textarea>
+ cols='{$cols}'{$width}>$encComment</textarea>
{$this->uploadFormTextAfterSummary}
</td>
</tr>
@@ -1353,7 +1551,7 @@ EOT
if( $error ) {
$errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
- return new WikiError( $wgOut->parse( $errorText ) );
+ return $errorText;
}
// Rockin', go ahead and upload
@@ -1420,4 +1618,29 @@ EOT
}
return $pageText;
}
+
+ /**
+ * If there are rows in the deletion log for this file, show them,
+ * along with a nice little note for the user
+ *
+ * @param OutputPage $out
+ * @param string filename
+ */
+ private function showDeletionLog( $out, $filename ) {
+ $reader = new LogReader(
+ new FauxRequest(
+ array(
+ 'page' => $filename,
+ 'type' => 'delete',
+ )
+ )
+ );
+ if( $reader->hasRows() ) {
+ $out->addHtml( '<div id="mw-upload-deleted-warn">' );
+ $out->addWikiMsg( 'upload-wasdeleted' );
+ $viewer = new LogViewer( $reader );
+ $viewer->showList( $out );
+ $out->addHtml( '</div>' );
+ }
+ }
}
diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php
index f358c1fd..3651fdc8 100644
--- a/includes/SpecialUserlogin.php
+++ b/includes/SpecialUserlogin.php
@@ -7,13 +7,13 @@
/**
* constructor
*/
-function wfSpecialUserlogin() {
+function wfSpecialUserlogin( $par = '' ) {
global $wgRequest;
if( session_id() == '' ) {
wfSetupSession();
}
- $form = new LoginForm( $wgRequest );
+ $form = new LoginForm( $wgRequest, $par );
$form->execute();
}
@@ -41,11 +41,11 @@ class LoginForm {
* Constructor
* @param WebRequest $request A WebRequest object passed by reference
*/
- function LoginForm( &$request ) {
+ function LoginForm( &$request, $par = '' ) {
global $wgLang, $wgAllowRealName, $wgEnableEmail;
global $wgAuth;
- $this->mType = $request->getText( 'type' );
+ $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
$this->mName = $request->getText( 'wpName' );
$this->mPassword = $request->getText( 'wpPassword' );
$this->mRetype = $request->getText( 'wpRetype' );
@@ -123,9 +123,9 @@ class LoginForm {
// Wipe the initial password and mail a temporary one
$u->setPassword( null );
$u->saveSettings();
- $result = $this->mailPasswordInternal( $u, false );
+ $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
- wfRunHooks( 'AddNewAccount', array( $u ) );
+ wfRunHooks( 'AddNewAccount', array( $u, true ) );
$wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
$wgOut->setRobotpolicy( 'noindex,nofollow' );
@@ -134,7 +134,7 @@ class LoginForm {
if( WikiError::isError( $result ) ) {
$this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
} else {
- $wgOut->addWikiText( wfMsg( 'accmailtext', $u->getName(), $u->getEmail() ) );
+ $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
$wgOut->returnToMain( false );
}
$u = 0;
@@ -164,9 +164,9 @@ class LoginForm {
global $wgOut;
$error = $u->sendConfirmationMail();
if( WikiError::isError( $error ) ) {
- $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $error->getMessage() ) );
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() );
} else {
- $wgOut->addWikiText( wfMsg( 'confirmemail_oncreate' ) );
+ $wgOut->addWikiMsg( 'confirmemail_oncreate' );
}
}
@@ -189,7 +189,7 @@ class LoginForm {
$wgOut->setArticleRelated( false );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
- $wgOut->returnToMain( $self->getPrefixedText() );
+ $wgOut->returnToMain( false, $self );
wfRunHooks( 'AddNewAccount', array( $u ) );
return true;
}
@@ -203,6 +203,7 @@ class LoginForm {
global $wgEnableSorbs, $wgProxyWhitelist;
global $wgMemc, $wgAccountCreationThrottle;
global $wgAuth, $wgMinimalPasswordLength;
+ global $wgEmailConfirmToEdit;
// If the user passes an invalid domain, something is fishy
if( !$wgAuth->validDomain( $this->mDomain ) ) {
@@ -228,10 +229,13 @@ class LoginForm {
return false;
}
- # Check anonymous user ($wgUser) limitations :
- if (!$wgUser->isAllowedToCreateAccount()) {
+ # Check permissions
+ if ( !$wgUser->isAllowed( 'createaccount' ) ) {
$this->userNotPrivilegedMessage();
return false;
+ } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+ $this->userBlockedMessage();
+ return false;
}
$ip = wfGetIP();
@@ -260,11 +264,29 @@ class LoginForm {
return false;
}
+ # check for minimal password length
if ( !$u->isValidPassword( $this->mPassword ) ) {
- $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
+ if ( !$this->mCreateaccountMail ) {
+ $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
+ return false;
+ } else {
+ # do not force a password for account creation by email
+ # set pseudo password, it will be replaced later by a random generated password
+ $this->mPassword = '-';
+ }
+ }
+
+ # if you need a confirmed email address to edit, then obviously you need an email address.
+ if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
+ $this->mainLoginForm( wfMsg( 'noemailtitle' ) );
return false;
}
-
+
+ if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
+ $this->mainLoginForm( wfMsg( 'invalidemailaddress' ) );
+ return false;
+ }
+
# Set some additional data so the AbortNewAccount hook can be
# used for more than just username validation
$u->setEmail( $this->mEmail );
@@ -449,7 +471,11 @@ class LoginForm {
$this->mainLoginForm( wfMsg( 'wrongpassword' ) );
break;
case self::NOT_EXISTS:
- $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+ if( $wgUser->isAllowed( 'createaccount' ) ){
+ $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
+ }
break;
case self::WRONG_PASS:
$this->mainLoginForm( wfMsg( 'wrongpassword' ) );
@@ -469,7 +495,7 @@ class LoginForm {
global $wgOut;
$wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
$reset = new PasswordResetForm( $this->mName, $this->mPassword );
- $reset->execute();
+ $reset->execute( null );
}
/**
@@ -519,7 +545,7 @@ class LoginForm {
return;
}
- $result = $this->mailPasswordInternal( $u, true );
+ $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
if( WikiError::isError( $result ) ) {
$this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
} else {
@@ -529,10 +555,14 @@ class LoginForm {
/**
+ * @param object user
+ * @param bool throttle
+ * @param string message name of email title
+ * @param string message name of email text
* @return mixed true on success, WikiError on failure
* @private
*/
- function mailPasswordInternal( $u, $throttle = true ) {
+ function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
global $wgServer, $wgScript;
@@ -550,9 +580,9 @@ class LoginForm {
$ip = wfGetIP();
if ( '' == $ip ) { $ip = '(Unknown)'; }
- $m = wfMsg( 'passwordremindertext', $ip, $u->getName(), $np, $wgServer . $wgScript );
+ $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript );
+ $result = $u->sendMail( wfMsg( $emailTitle ), $m );
- $result = $u->sendMail( wfMsg( 'passwordremindertitle' ), $m );
return $result;
}
@@ -589,14 +619,14 @@ class LoginForm {
$wgOut->setRobotpolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
- $wgOut->addWikiText( wfMsg( 'whitelistacctext' ) );
+ $wgOut->addWikiMsg( 'whitelistacctext' );
$wgOut->returnToMain( false );
}
/** */
function userBlockedMessage() {
- global $wgOut;
+ global $wgOut, $wgUser;
# Let's be nice about this, it's likely that this feature will be used
# for blocking large numbers of innocent people, e.g. range blocks on
@@ -611,7 +641,10 @@ class LoginForm {
$wgOut->setArticleRelated( false );
$ip = wfGetIP();
- $wgOut->addWikiText( wfMsg( 'cantcreateaccounttext', $ip ) );
+ $blocker = User::whoIs( $wgUser->mBlock->mBy );
+ $block_reason = $wgUser->mBlock->mReason;
+
+ $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker );
$wgOut->returnToMain( false );
}
@@ -621,7 +654,7 @@ class LoginForm {
function mainLoginForm( $msg, $msgtype = 'error' ) {
global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
- global $wgAuth;
+ global $wgAuth, $wgEmailConfirmToEdit;
if ( $this->mType == 'signup' ) {
if ( !$wgUser->isAllowed( 'createaccount' ) ) {
@@ -666,7 +699,7 @@ class LoginForm {
$linkq .= '&uselang=' . $this->mLanguage;
$link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
- $link .= wfMsgHtml( $linkmsg . 'link' );
+ $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink'
$link .= '</a>';
# Don't show a "create account" link if the user can't
@@ -689,6 +722,7 @@ class LoginForm {
$template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
$template->set( 'userealname', $wgAllowRealName );
$template->set( 'useemail', $wgEnableEmail );
+ $template->set( 'emailrequired', $wgEmailConfirmToEdit );
$template->set( 'canreset', $wgAuth->allowPasswordChange() );
$template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
@@ -779,7 +813,7 @@ class LoginForm {
function throttleHit( $limit ) {
global $wgOut;
- $wgOut->addWikiText( wfMsg( 'acct_creation_throttle_hit', $limit ) );
+ $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
}
/**
@@ -796,7 +830,9 @@ class LoginForm {
foreach( $langs as $lang ) {
$lang = trim( $lang, '* ' );
$parts = explode( '|', $lang );
- $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+ if (count($parts) >= 2) {
+ $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+ }
}
return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
} else {
@@ -824,3 +860,4 @@ class LoginForm {
}
}
+
diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php
index 6e464ced..d9952ea5 100644
--- a/includes/SpecialUserlogout.php
+++ b/includes/SpecialUserlogout.php
@@ -10,17 +10,10 @@
function wfSpecialUserlogout() {
global $wgUser, $wgOut;
- if (wfRunHooks('UserLogout', array(&$wgUser))) {
-
- $wgUser->logout();
-
- wfRunHooks('UserLogoutComplete', array(&$wgUser));
-
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
- $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) );
- $wgOut->returnToMain();
-
- }
+ $wgUser->logout();
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) );
+ $wgOut->returnToMain();
}
diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php
index b97e5168..48fb3628 100644
--- a/includes/SpecialUserrights.php
+++ b/includes/SpecialUserrights.php
@@ -4,64 +4,111 @@
* Special page to allow managing user group membership
*
* @addtogroup SpecialPage
- * @todo This code is disgusting and needs a total rewrite
+ * @todo Use checkboxes or something, this list thing is incomprehensible to
+ * normal human beings.
*/
-/** */
-require_once( dirname(__FILE__) . '/HTMLForm.php');
-
-/** Entry point */
-function wfSpecialUserrights() {
- global $wgRequest;
- $form = new UserrightsForm($wgRequest);
- $form->execute();
-}
-
/**
* A class to manage user levels rights.
* @addtogroup SpecialPage
*/
-class UserrightsForm extends HTMLForm {
- var $mPosted, $mRequest, $mSaveprefs;
- /** Escaped local url name*/
- var $action;
-
- /** Constructor*/
- public function __construct( &$request ) {
- $this->mPosted = $request->wasPosted();
- $this->mRequest =& $request;
- $this->mName = 'userrights';
-
- $titleObj = SpecialPage::getTitleFor( 'Userrights' );
- $this->action = $titleObj->escapeLocalURL();
+class UserrightsPage extends SpecialPage {
+ # The target of the local right-adjuster's interest. Can be gotten from
+ # either a GET parameter or a subpage-style parameter, so have a member
+ # variable for it.
+ protected $mTarget;
+ protected $isself = false;
+
+ public function __construct() {
+ parent::__construct( 'Userrights' );
+ }
+
+ public function isRestricted() {
+ return true;
+ }
+
+ public function userCanExecute( $user ) {
+ $available = $this->changeableGroups();
+ return !empty( $available['add'] )
+ or !empty( $available['remove'] )
+ or ($this->isself and
+ (!empty( $available['add-self'] )
+ or !empty( $available['remove-self'] )));
}
/**
* Manage forms to be shown according to posted data.
* Depending on the submit button used, call a form or a save function.
+ *
+ * @param mixed $par String if any subpage provided, else null
*/
- function execute() {
+ function execute( $par ) {
+ // If the visitor doesn't have permissions to assign or remove
+ // any groups, it's a bit silly to give them the user search prompt.
+ global $wgUser, $wgRequest;
+
+ if( $par ) {
+ $this->mTarget = $par;
+ } else {
+ $this->mTarget = $wgRequest->getVal( 'user' );
+ }
+
+ if (!$this->mTarget) {
+ /*
+ * If the user specified no target, and they can only
+ * edit their own groups, automatically set them as the
+ * target.
+ */
+ $available = $this->changeableGroups();
+ if (empty($available['add']) && empty($available['remove']))
+ $this->mTarget = $wgUser->getName();
+ }
+
+ if ($this->mTarget == $wgUser->getName())
+ $this->isself = true;
+
+ if( !$this->userCanExecute( $wgUser ) ) {
+ // fixme... there may be intermediate groups we can mention.
+ global $wgOut;
+ $wgOut->showPermissionsErrorPage( array(
+ $wgUser->isAnon()
+ ? 'userrights-nologin'
+ : 'userrights-notallowed' ) );
+ return;
+ }
+
+ if ( wfReadOnly() ) {
+ global $wgOut;
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $this->outputHeader();
+
+ $this->setHeaders();
+
// show the general form
$this->switchForm();
- if( $this->mPosted ) {
- // show some more forms
- if( $this->mRequest->getCheck( 'ssearchuser' ) ) {
- $this->editUserGroupsForm( $this->mRequest->getVal( 'user-editname' ) );
- }
+ if( $wgRequest->wasPosted() ) {
// save settings
- if( $this->mRequest->getCheck( 'saveusergroups' ) ) {
- global $wgUser;
- $username = $this->mRequest->getVal( 'user-editname' );
- $reason = $this->mRequest->getVal( 'user-reason' );
- if( $wgUser->matchEditToken( $this->mRequest->getVal( 'wpEditToken' ), $username ) ) {
- $this->saveUserGroups( $username,
- $this->mRequest->getArray( 'member' ),
- $this->mRequest->getArray( 'available' ),
- $reason );
+ if( $wgRequest->getCheck( 'saveusergroups' ) ) {
+ $reason = $wgRequest->getVal( 'user-reason' );
+ if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) {
+ $this->saveUserGroups(
+ $this->mTarget,
+ $wgRequest->getArray( 'removable' ),
+ $wgRequest->getArray( 'available' ),
+ $reason
+ );
}
}
}
+
+ // show some more forms
+ if( $this->mTarget ) {
+ $this->editUserGroupsForm( $this->mTarget );
+ }
}
/**
@@ -72,50 +119,155 @@ class UserrightsForm extends HTMLForm {
* @param array $removegroup id of groups to be removed.
* @param array $addgroup id of groups to be added.
* @param string $reason Reason for group change
- *
+ * @return null
*/
- function saveUserGroups( $username, $removegroup, $addgroup, $reason = '' ) {
- global $wgOut;
- $u = User::newFromName($username);
+ function saveUserGroups( $username, $removegroup, $addgroup, $reason = '') {
+ global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
- if(is_null($u)) {
- $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) );
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
return;
}
-
- if($u->getID() == 0) {
- $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) );
- return;
+
+ // Validate input set...
+ $changeable = $this->changeableGroups();
+ if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) {
+ $addable = array_merge($changeable['add'], $wgGroupsAddToSelf);
+ $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf);
+ } else {
+ $addable = $changeable['add'];
+ $removable = $changeable['remove'];
}
- $oldGroups = $u->getGroups();
+ $removegroup = array_unique(
+ array_intersect( (array)$removegroup, $removable ) );
+ $addgroup = array_unique(
+ array_intersect( (array)$addgroup, $addable ) );
+
+ $oldGroups = $user->getGroups();
$newGroups = $oldGroups;
// remove then add groups
- if(isset($removegroup)) {
+ if( $removegroup ) {
$newGroups = array_diff($newGroups, $removegroup);
foreach( $removegroup as $group ) {
- if ( $this->canRemove( $group ) ) {
- $u->removeGroup( $group );
- }
+ $user->removeGroup( $group );
}
}
- if(isset($addgroup)) {
+ if( $addgroup ) {
$newGroups = array_merge($newGroups, $addgroup);
foreach( $addgroup as $group ) {
- if ( $this->canAdd( $group ) ) {
- $u->addGroup( $group );
- }
+ $user->addGroup( $group );
}
}
$newGroups = array_unique( $newGroups );
+ // Ensure that caches are cleared
+ $user->invalidateCache();
+
wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
+ if( $user instanceof User ) {
+ // hmmm
+ wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
+ }
+
+ if( $newGroups != $oldGroups ) {
+ $log = new LogPage( 'rights' );
+
+ global $wgRequest;
+ $log->addEntry( 'rights',
+ $user->getUserPage(),
+ $wgRequest->getText( 'user-reason' ),
+ array(
+ $this->makeGroupNameList( $oldGroups ),
+ $this->makeGroupNameList( $newGroups )
+ )
+ );
+ }
+ }
+
+ /**
+ * Edit user groups membership
+ * @param string $username Name of the user.
+ */
+ function editUserGroupsForm( $username ) {
+ global $wgOut;
- wfRunHooks( 'UserRights', array( &$u, $addgroup, $removegroup ) );
- $log = new LogPage( 'rights' );
- $log->addEntry( 'rights', Title::makeTitle( NS_USER, $u->getName() ), $reason, array( $this->makeGroupNameList( $oldGroups ),
- $this->makeGroupNameList( $newGroups ) ) );
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
+ return;
+ }
+
+ $groups = $user->getGroups();
+
+ $this->showEditUserGroupsForm( $user, $groups );
+
+ // This isn't really ideal logging behavior, but let's not hide the
+ // interwiki logs if we're using them as is.
+ $this->showLogFragment( $user, $wgOut );
+ }
+
+ /**
+ * Normalize the input username, which may be local or remote, and
+ * return a user (or proxy) object for manipulating it.
+ *
+ * Side effects: error output for invalid access
+ * @return mixed User, UserRightsProxy, or null
+ */
+ function fetchUser( $username ) {
+ global $wgOut, $wgUser;
+
+ $parts = explode( '@', $username );
+ if( count( $parts ) < 2 ) {
+ $name = trim( $username );
+ $database = '';
+ } else {
+ list( $name, $database ) = array_map( 'trim', $parts );
+
+ if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
+ $wgOut->addWikiMsg( 'userrights-no-interwiki' );
+ return null;
+ }
+ if( !UserRightsProxy::validDatabase( $database ) ) {
+ $wgOut->addWikiMsg( 'userrights-nodatabase', $database );
+ return null;
+ }
+ }
+
+ if( $name == '' ) {
+ $wgOut->addWikiMsg( 'nouserspecified' );
+ return false;
+ }
+
+ if( $name{0} == '#' ) {
+ // Numeric ID can be specified...
+ // We'll do a lookup for the name internally.
+ $id = intval( substr( $name, 1 ) );
+
+ if( $database == '' ) {
+ $name = User::whoIs( $id );
+ } else {
+ $name = UserRightsProxy::whoIs( $database, $id );
+ }
+
+ if( !$name ) {
+ $wgOut->addWikiMsg( 'noname' );
+ return null;
+ }
+ }
+
+ if( $database == '' ) {
+ $user = User::newFromName( $name );
+ } else {
+ $user = UserRightsProxy::newFromName( $database, $name );
+ }
+
+ if( !$user || $user->isAnon() ) {
+ $wgOut->addWikiMsg( 'nosuchusershort', $username );
+ return null;
+ }
+
+ return $user;
}
function makeGroupNameList( $ids ) {
@@ -126,36 +278,16 @@ class UserrightsForm extends HTMLForm {
* Output a form to allow searching for a user
*/
function switchForm() {
- global $wgOut, $wgRequest;
- $username = $wgRequest->getText( 'user-editname' );
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) );
+ global $wgOut, $wgScript;
+ $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser' ) );
+ $form .= Xml::hidden( 'title', 'Special:Userrights' );
$form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>';
- $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>';
- $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>';
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . '</p>';
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ) ) . '</p>';
$form .= '</fieldset>';
$form .= '</form>';
$wgOut->addHTML( $form );
}
-
- /**
- * Edit user groups membership
- * @param string $username Name of the user.
- */
- function editUserGroupsForm($username) {
- global $wgOut;
-
- $user = User::newFromName($username);
- if( is_null( $user ) ) {
- $wgOut->addWikiText( wfMsg( 'nouserspecified' ) );
- return;
- } elseif( $user->getID() == 0 ) {
- $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) );
- return;
- }
-
- $this->showEditUserGroupsForm( $username, $user->getGroups() );
- $this->showLogFragment( $user, $wgOut );
- }
/**
* Go through used and available groups and return the ones that this
@@ -166,9 +298,15 @@ class UserrightsForm extends HTMLForm {
* @return Array: Tuple of addable, then removable groups
*/
protected function splitGroups( $groups ) {
+ global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
list($addable, $removable) = array_values( $this->changeableGroups() );
- $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have
- $addable = array_diff( $addable, $groups ); // Can't add groups the user does have
+
+ $removable = array_intersect(
+ array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
+ $groups ); // Can't remove groups the user doesn't have
+ $addable = array_diff(
+ array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
+ $groups ); // Can't add groups the user does have
return array( $addable, $removable );
}
@@ -177,21 +315,31 @@ class UserrightsForm extends HTMLForm {
* Show the form to edit group memberships.
*
* @todo make all CSS-y and semantic
- * @param $username String: Name of user you're editing
+ * @param $user User or UserRightsProxy you're editing
* @param $groups Array: Array of groups the user is in
*/
- protected function showEditUserGroupsForm( $username, $groups ) {
+ protected function showEditUserGroupsForm( $user, $groups ) {
global $wgOut, $wgUser;
-
+
list( $addable, $removable ) = $this->splitGroups( $groups );
+ $list = array();
+ foreach( $user->getGroups() as $group )
+ $list[] = self::buildGroupLink( $group );
+
+ $grouplist = '';
+ if( count( $list ) > 0 ) {
+ $grouplist = '<p>' . wfMsgHtml( 'userrights-groupsmember' ) . ' ' . implode( ', ', $list ) . '</p>';
+ }
$wgOut->addHTML(
- Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
- Xml::hidden( 'user-editname', $username ) .
- Xml::hidden( 'wpEditToken', $wgUser->editToken( $username ) ) .
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->escapeLocalURL(), 'name' => 'editGroup' ) ) .
+ Xml::hidden( 'user', $user->getName() ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken( $user->getName() ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
- $wgOut->parse( wfMsg( 'editinguser', $username ) ) .
+ wfMsgExt( 'editinguser', array( 'parse' ),
+ wfEscapeWikiText( $user->getName() ) ) .
+ $grouplist .
$this->explainRights() .
"<table border='0'>
<tr>
@@ -214,7 +362,7 @@ class UserrightsForm extends HTMLForm {
Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
"</td>
<td>" .
- Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason' ) ) .
+ Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
"</td>
</tr>
<tr>
@@ -228,6 +376,19 @@ class UserrightsForm extends HTMLForm {
Xml::closeElement( 'form' ) . "\n"
);
}
+
+ /**
+ * Format a link to a group description page
+ *
+ * @param string $group
+ * @return string
+ */
+ private static function buildGroupLink( $group ) {
+ static $cache = array();
+ if( !isset( $cache[$group] ) )
+ $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
+ return $cache[$group];
+ }
/**
* Prepare a list of groups the user is able to add and remove
@@ -236,17 +397,25 @@ class UserrightsForm extends HTMLForm {
*/
private function explainRights() {
global $wgUser, $wgLang;
-
+
$out = array();
- list( $add, $remove ) = array_values( $this->changeableGroups() );
-
+ list( $add, $remove, $addself, $rmself ) = array_values( $this->changeableGroups() );
+
if( count( $add ) > 0 )
- $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) );
+ $out[] = wfMsgExt( 'userrights-available-add', 'parseinline',
+ $wgLang->listToText( $add ), count( $add ) );
if( count( $remove ) > 0 )
- $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) );
-
+ $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline',
+ $wgLang->listToText( $remove ), count( $add ) );
+ if( count( $addself ) > 0 )
+ $out[] = wfMsgExt( 'userrights-available-add-self', 'parseinline',
+ $wgLang->listToText( $addself ), count( $addself ) );
+ if( count( $rmself ) > 0 )
+ $out[] = wfMsgExt( 'userrights-available-remove-self', 'parseinline',
+ $wgLang->listToText( $rmself ), count( $rmself ) );
+
return count( $out ) > 0
- ? implode( ' ', $out )
+ ? implode( '<br />', $out )
: wfMsgExt( 'userrights-available-none', 'parseinline' );
}
@@ -257,7 +426,7 @@ class UserrightsForm extends HTMLForm {
* @return string XHTML <select> element
*/
private function removeSelect( $groups ) {
- return $this->doSelect( $groups, 'member' );
+ return $this->doSelect( $groups, 'removable' );
}
/**
@@ -274,7 +443,7 @@ class UserrightsForm extends HTMLForm {
* Adds the <select> thingie where you can select what groups to add/remove
*
* @param array $groups The groups that can be added/removed
- * @param string $name 'member' or 'available'
+ * @param string $name 'removable' or 'available'
* @return string XHTML <select> element
*/
private function doSelect( $groups, $name ) {
@@ -318,10 +487,29 @@ class UserrightsForm extends HTMLForm {
*
* @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
*/
- private function changeableGroups() {
- global $wgUser;
+ function changeableGroups() {
+ global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
- $groups = array( 'add' => array(), 'remove' => array() );
+ if( $wgUser->isAllowed( 'userrights' ) ) {
+ // This group gives the right to modify everything (reverse-
+ // compatibility with old "userrights lets you change
+ // everything")
+ // Using array_merge to make the groups reindexed
+ $all = array_merge( User::getAllGroups() );
+ return array(
+ 'add' => $all,
+ 'remove' => $all,
+ 'add-self' => array(),
+ 'remove-self' => array()
+ );
+ }
+
+ // Okay, it's not so simple, we will have to go through the arrays
+ $groups = array(
+ 'add' => array(),
+ 'remove' => array(),
+ 'add-self' => $wgGroupsAddToSelf,
+ 'remove-self' => $wgGroupsRemoveFromSelf);
$addergroups = $wgUser->getEffectiveGroups();
foreach ($addergroups as $addergroup) {
@@ -339,25 +527,10 @@ class UserrightsForm extends HTMLForm {
*
* @param String $group The group to check for whether it can add/remove
* @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
- */
+ */
private function changeableByGroup( $group ) {
- global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
-
- if( empty($wgGroupPermissions[$group]['userrights']) ) {
- // This group doesn't give the right to modify anything
- return array( 'add' => array(), 'remove' => array() );
- }
- if( empty($wgAddGroups[$group]) and empty($wgRemoveGroups[$group]) ) {
- // This group gives the right to modify everything (reverse-
- // compatibility with old "userrights lets you change
- // everything")
- return array(
- 'add' => User::getAllGroups(),
- 'remove' => User::getAllGroups()
- );
- }
-
- // Okay, it's not so simple, we have to go through the arrays
+ global $wgAddGroups, $wgRemoveGroups;
+
$groups = array( 'add' => array(), 'remove' => array() );
if( empty($wgAddGroups[$group]) ) {
// Don't add anything to $groups
@@ -390,7 +563,7 @@ class UserrightsForm extends HTMLForm {
new FauxRequest(
array(
'type' => 'rights',
- 'page' => $user->getUserPage()->getPrefixedUrl(),
+ 'page' => $user->getUserPage()->getPrefixedText(),
)
)
)
@@ -399,4 +572,4 @@ class UserrightsForm extends HTMLForm {
$viewer->showList( $output );
}
-} \ No newline at end of file
+}
diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php
index a6a132e0..70203832 100644
--- a/includes/SpecialVersion.php
+++ b/includes/SpecialVersion.php
@@ -24,11 +24,13 @@ class SpecialVersion {
* main()
*/
function execute() {
- global $wgOut;
+ global $wgOut, $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
$wgOut->addHTML( '<div dir="ltr">' );
$wgOut->addWikiText(
$this->MediaWikiCredits() .
+ $this->softwareInformation() .
$this->extensionCredits() .
$this->wgHooks()
);
@@ -41,18 +43,13 @@ class SpecialVersion {
*/
/**
- * Return wiki text showing the licence information and third party
- * software versions (apache, php, mysql).
- * @static
+ * @return wiki text showing the license information
*/
- function MediaWikiCredits() {
- $version = self::getVersion();
- $dbr = wfGetDB( DB_SLAVE );
-
- $ret =
+ static function MediaWikiCredits() {
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
"__NOTOC__
This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
- copyright (C) 2001-2007 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+ copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason,
Niklas Laxström, Domas Mituzas, Rob Church and others.
@@ -68,40 +65,68 @@ class SpecialVersion {
You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
along with this program; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- or [http://www.gnu.org/copyleft/gpl.html read it online]
-
- * [http://www.mediawiki.org/ MediaWiki]: $version
- * [http://www.php.net/ PHP]: " . phpversion() . " (" . php_sapi_name() . ")
- * " . $dbr->getSoftwareLink() . ": " . $dbr->getServerVersion();
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ or [http://www.gnu.org/copyleft/gpl.html read it online].
+ ";
return str_replace( "\t\t", '', $ret ) . "\n";
}
+ /**
+ * @return wiki text showing the third party software versions (apache, php, mysql).
+ */
+ static function softwareInformation() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
+ "<tr>
+ <th>" . wfMsg( 'version-software-product' ) . "</th>
+ <th>" . wfMsg( 'version-software-version' ) . "</th>
+ </tr>\n
+ <tr>
+ <td>[http://www.mediawiki.org/ MediaWiki]</td>
+ <td>" . self::getVersion() . "</td>
+ </tr>\n
+ <tr>
+ <td>[http://www.php.net/ PHP]</td>
+ <td>" . phpversion() . " (" . php_sapi_name() . ")</td>
+ </tr>\n
+ <tr>
+ <td>" . $dbr->getSoftwareLink() . "</td>
+ <td>" . $dbr->getServerVersion() . "</td>
+ </tr>\n" .
+ Xml::closeElement( 'table' );
+ }
+
/** Return a string of the MediaWiki version with SVN revision if available */
public static function getVersion() {
global $wgVersion, $IP;
+ wfProfileIn( __METHOD__ );
$svn = self::getSvnRevision( $IP );
- return $svn ? "$wgVersion (r$svn)" : $wgVersion;
+ $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
+ wfProfileOut( __METHOD__ );
+ return $version;
}
/** Generate wikitext showing extensions name, URL, author and description */
function extensionCredits() {
- global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunction;
+ global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
- if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunction ) )
+ if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
return '';
$extensionTypes = array(
- 'specialpage' => 'Special pages',
- 'parserhook' => 'Parser hooks',
- 'variable' => 'Variables',
- 'other' => 'Other',
+ 'specialpage' => wfMsg( 'version-specialpages' ),
+ 'parserhook' => wfMsg( 'version-parserhooks' ),
+ 'variable' => wfMsg( 'version-variables' ),
+ 'media' => wfMsg( 'version-mediahandlers' ),
+ 'other' => wfMsg( 'version-other' ),
);
wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
- $out = "<h2>Extensions</h2>\n";
- $out .= wfOpenElement('table', array('id' => 'sv-ext') );
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
foreach ( $extensionTypes as $type => $text ) {
if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
@@ -111,38 +136,39 @@ class SpecialVersion {
foreach ( $wgExtensionCredits[$type] as $extension ) {
$out .= $this->formatCredits(
- isset ( $extension['name'] ) ? $extension['name'] : '',
- isset ( $extension['version'] ) ? $extension['version'] : null,
- isset ( $extension['author'] ) ? $extension['author'] : '',
- isset ( $extension['url'] ) ? $extension['url'] : null,
- isset ( $extension['description'] ) ? $extension['description'] : ''
+ isset ( $extension['name'] ) ? $extension['name'] : '',
+ isset ( $extension['version'] ) ? $extension['version'] : null,
+ isset ( $extension['author'] ) ? $extension['author'] : '',
+ isset ( $extension['url'] ) ? $extension['url'] : null,
+ isset ( $extension['description'] ) ? $extension['description'] : '',
+ isset ( $extension['descriptionmsg'] ) ? $extension['descriptionmsg'] : ''
);
}
}
}
if ( count( $wgExtensionFunctions ) ) {
- $out .= $this->openExtType('Extension functions');
+ $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
$out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
}
if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
for ( $i = 0; $i < $cnt; ++$i )
$tags[$i] = "&lt;{$tags[$i]}&gt;";
- $out .= $this->openExtType('Parser extension tags');
+ $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) );
$out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n";
}
if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
- $out .= $this->openExtType('Parser function hooks');
+ $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) );
$out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n";
}
- if ( count( $wgSkinExtensionFunction ) ) {
- $out .= $this->openExtType('Skin extension functions');
- $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunction ) . "</td></tr>\n";
+ if ( count( $wgSkinExtensionFunctions ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
}
- $out .= wfCloseElement( 'table' );
+ $out .= Xml::closeElement( 'table' );
return $out;
}
@@ -158,21 +184,23 @@ class SpecialVersion {
}
}
- function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) {
- $ret = '<tr><td>';
- if ( isset( $url ) )
- $ret .= "[$url ";
- $ret .= "''$name";
- if ( isset( $version ) )
- $ret .= " (version $version)";
- $ret .= "''";
- if ( isset( $url ) )
- $ret .= ']';
- $ret .= '</td>';
- $ret .= "<td>$description</td>";
- $ret .= "<td>" . $this->listToText( (array)$author ) . "</td>";
- $ret .= '</tr>';
- return "$ret\n";
+ function formatCredits( $name, $version = null, $author = null, $url = null, $description = null, $descriptionMsg = null ) {
+ $extension = isset( $url ) ? "[$url $name]" : $name;
+ $version = isset( $version ) ? "(" . wfMsg( 'version-version' ) . " $version)" : '';
+
+ # Look for a localized description
+ if( isset( $descriptionMsg ) ) {
+ $msg = wfMsg( $descriptionMsg );
+ if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
+ $description = $msg;
+ }
+ }
+
+ return "<tr>
+ <td><em>$extension $version</em></td>
+ <td>$description</td>
+ <td>" . $this->listToText( (array)$author ) . "</td>
+ </tr>\n";
}
/**
@@ -185,14 +213,20 @@ class SpecialVersion {
$myWgHooks = $wgHooks;
ksort( $myWgHooks );
- $ret = "<h2>Hooks</h2>\n"
- . wfOpenElement('table', array('id' => 'sv-hooks') )
- . "<tr><th>Hook name</th><th>Subscribed by</th></tr>\n";
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
+ "<tr>
+ <th>" . wfMsg( 'version-hook-name' ) . "</th>
+ <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
+ </tr>\n";
- foreach ($myWgHooks as $hook => $hooks)
- $ret .= "<tr><td>$hook</td><td>" . $this->listToText( $hooks ) . "</td></tr>\n";
+ foreach ( $myWgHooks as $hook => $hooks )
+ $ret .= "<tr>
+ <td>$hook</td>
+ <td>" . $this->listToText( $hooks ) . "</td>
+ </tr>\n";
- $ret .= '</table>';
+ $ret .= Xml::closeElement( 'table' );
return $ret;
} else
return '';
@@ -204,13 +238,13 @@ class SpecialVersion {
if(!$this->firstExtOpened) {
// Insert a spacing line
- $out .= '<tr class="sv-space">' . wfElement( 'td', $opt ) . "</tr>\n";
+ $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n";
}
$this->firstExtOpened = false;
if($name) { $opt['id'] = "sv-$name"; }
- $out .= "<tr>" . wfElement( 'th', $opt, $text) . "</tr>\n";
+ $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
return $out;
}
@@ -232,18 +266,20 @@ class SpecialVersion {
function listToText( $list ) {
$cnt = count( $list );
- if ( $cnt == 1 ) {
+ if ( $cnt == 1 ) {
// Enforce always returning a string
return (string)$this->arrayToString( $list[0] );
- } elseif ( $cnt == 0 ) {
+ } elseif ( $cnt == 0 ) {
return '';
} else {
+ sort( $list );
$t = array_slice( $list, 0, $cnt - 1 );
$one = array_map( array( &$this, 'arrayToString' ), $t );
$two = $this->arrayToString( $list[$cnt - 1] );
+ $and = wfMsg( 'and' );
- return implode( ', ', $one ) . " and $two";
- }
+ return implode( ', ', $one ) . " $and $two";
+ }
}
/**
@@ -316,3 +352,4 @@ class SpecialVersion {
/**#@-*/
+
diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php
index f3e8966c..580cc6de 100644
--- a/includes/SpecialWantedcategories.php
+++ b/includes/SpecialWantedcategories.php
@@ -37,7 +37,7 @@ class WantedCategoriesPage extends QueryPage {
/**
* Fetch user page links and cache their existence
*/
- function preprocessResults( &$db, &$res ) {
+ function preprocessResults( $db, $res ) {
$batch = new LinkBatch;
while ( $row = $db->fetchObject( $res ) )
$batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php
index 5fc45a88..1fb8cdbb 100644
--- a/includes/SpecialWantedpages.php
+++ b/includes/SpecialWantedpages.php
@@ -51,7 +51,7 @@ class WantedPagesPage extends QueryPage {
/**
* Cache page existence for performance
*/
- function preprocessResults( &$db, &$res ) {
+ function preprocessResults( $db, $res ) {
$batch = new LinkBatch;
while ( $row = $db->fetchObject( $res ) )
$batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php
index e9aa7e68..2dfa8ae5 100644
--- a/includes/SpecialWatchlist.php
+++ b/includes/SpecialWatchlist.php
@@ -87,19 +87,11 @@ function wfSpecialWatchlist( $par ) {
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
- $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_user=$uid";
- $res = $dbr->query( $sql, $fname );
- $s = $dbr->fetchObject( $res );
-
-# Patch *** A1 *** (see A2 below)
-# adjust for page X, talk:page X, which are both stored separately, but treated together
- $nitems = floor($s->n / 2);
-# $nitems = $s->n;
-
- if($nitems == 0) {
- $wgOut->addWikiText( wfMsg( 'nowatchlist' ) );
- return;
- }
+ $watchlistCount = $dbr->selectField( 'watchlist', 'COUNT(*)',
+ array( 'wl_user' => $uid ), __METHOD__ );
+ // Adjust for page X, talk:page X, which are both stored separately,
+ // but treated together
+ $nitems = floor($watchlistCount / 2);
if( is_null($days) || !is_numeric($days) ) {
$big = 1000; /* The magical big */
@@ -122,6 +114,16 @@ function wfSpecialWatchlist( $par ) {
wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
wfAppendToArrayIfNotDefault('namespace', $nameSpace , $defaults, $nondefaults);
+ $hookSql = "";
+ if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) {
+ return;
+ }
+
+ if($nitems == 0) {
+ $wgOut->addWikiMsg( 'nowatchlist' );
+ return;
+ }
+
if ( $days <= 0 ) {
$andcutoff = '';
} else {
@@ -180,8 +182,10 @@ function wfSpecialWatchlist( $par ) {
'" /><input type="hidden" name="reset" value="all" /></form>' .
"\n\n" );
}
-
- $sql = "SELECT *
+ if ( $wgShowUpdatedMarker ) {
+ $wltsfield=", ${watchlist}.wl_notificationtimestamp ";
+ }
+ $sql = "SELECT ${recentchanges}.* ${wltsfield}
FROM $watchlist,$recentchanges,$page
WHERE wl_user=$uid
AND wl_namespace=rc_namespace
@@ -193,6 +197,7 @@ function wfSpecialWatchlist( $par ) {
$andHideBots
$andHideMinor
$nameSpaceClause
+ $hookSql
ORDER BY rc_timestamp DESC
$limitWatchlist";
@@ -251,7 +256,7 @@ function wfSpecialWatchlist( $par ) {
# If there's nothing to show, stop here
if( $numRows == 0 ) {
- $wgOut->addWikiText( wfMsgNoTrans( 'watchnochange' ) );
+ $wgOut->addWikiMsg( 'watchnochange' );
return;
}
@@ -286,10 +291,13 @@ function wfSpecialWatchlist( $par ) {
}
if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
- $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" .$dbr->strencode($obj->page_title). "' AND wl_namespace='{$obj->page_namespace}'" ;
- $res3 = $dbr->query( $sql3, $fname );
- $x = $dbr->fetchObject( $res3 );
- $rc->numberofWatchingusers = $x->n;
+ $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ );
} else {
$rc->numberofWatchingusers = 0;
}
@@ -364,4 +372,4 @@ function wlCountItems( &$user, $talk = true ) {
$count = floor( $count / 2 );
return( $count );
-} \ No newline at end of file
+}
diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php
index d944f6b4..16a44ee6 100644
--- a/includes/SpecialWhatlinkshere.php
+++ b/includes/SpecialWhatlinkshere.php
@@ -44,23 +44,23 @@ class WhatLinksHerePage {
$targetString = isset($this->par) ? $this->par : $this->request->getVal( 'target' );
- if (is_null($targetString)) {
- $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ if ( is_null( $targetString ) ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
return;
}
$this->target = Title::newFromURL( $targetString );
if( !$this->target ) {
- $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ $wgOut->addHTML( $this->whatlinkshereForm() );
return;
}
$this->selfTitle = Title::makeTitleSafe( NS_SPECIAL,
'Whatlinkshere/' . $this->target->getPrefixedDBkey() );
-
+
$wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
$wgOut->setSubtitle( wfMsg( 'linklistsub' ) );
- $wgOut->addHTML( wfMsg( 'whatlinkshere-barrow' ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
+ $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
$this->showIndirectLinks( 0, $this->target, $this->limit, $this->from, $this->back );
}
@@ -113,34 +113,32 @@ class WhatLinksHerePage {
// Read an extra row as an at-end check
$queryLimit = $limit + 1;
-
+
// enforce join order, sometimes namespace selector may
// trigger filesorts which are far less efficient than scanning many entries
$options[] = 'STRAIGHT_JOIN';
-
+
$options['LIMIT'] = $queryLimit;
$fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
$options['ORDER BY'] = 'pl_from';
$plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
$plConds, $fname, $options );
-
+
$options['ORDER BY'] = 'tl_from';
$tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
$tlConds, $fname, $options );
-
+
if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) {
- if ( 0 == $level && !isset( $this->namespace ) ) {
- // really no links to here
- $wgOut->addWikiText( wfMsg( 'nolinkshere', $this->target->getPrefixedText() ) );
- } elseif ( 0 == $level && isset( $this->namespace ) ) {
- // no links from requested namespace to here
+ if ( 0 == $level ) {
$options = array(); // reinitialize for a further namespace search
+ // really no links to here
$options['namespace'] = $this->namespace;
$options['target'] = $this->target->getPrefixedText();
list( $options['limit'], $options['offset']) = wfCheckLimits();
$wgOut->addHTML( $this->whatlinkshereForm( $options ) );
- $wgOut->addWikiText( wfMsg( 'nolinkshere-ns', $this->target->getPrefixedText() ) );
+ $errMsg = isset( $this->namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
+ $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
}
return;
}
@@ -157,8 +155,8 @@ class WhatLinksHerePage {
$options['offset'] = $this->request->getVal( 'offset' );
/* Offset must be an integral. */
if ( !strlen( $options['offset'] ) || !preg_match( '/^[0-9]+$/', $options['offset'] ) )
- $options['offset'] = '';
- $options['target'] = $this->target->getPrefixedDBkey();
+ $options['offset'] = '';
+ $options['target'] = $this->target->getPrefixedText();
// Read the rows into an array and remove duplicates
// templatelinks comes second so that the templatelinks row overwrites the
@@ -195,12 +193,8 @@ class WhatLinksHerePage {
if ( $level == 0 ) {
$wgOut->addHTML( $this->whatlinkshereForm( $options ) );
- $wgOut->addWikiText( wfMsg( 'linkshere', $this->target->getPrefixedText() ) );
- }
- $isredir = wfMsg( 'isredirect' );
- $istemplate = wfMsg( 'istemplate' );
+ $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
- if( $level == 0 ) {
$prevnext = $this->getPrevNext( $limit, $prevId, $nextId, $options['namespace'] );
$wgOut->addHTML( $prevnext );
}
@@ -221,14 +215,14 @@ class WhatLinksHerePage {
// Display properties (redirect or template)
$props = array();
if ( $row->page_is_redirect ) {
- $props[] = $isredir;
+ $props[] = wfMsgHtml( 'isredirect' );
}
if ( $row->is_template ) {
- $props[] = $istemplate;
+ $props[] = wfMsgHtml( 'istemplate' );
}
if ( count( $props ) ) {
- // FIXME? Cultural assumption, hard-coded punctuation
- $wgOut->addHTML( ' (' . implode( ', ', $props ) . ') ' );
+ $list = implode( wfMsgHtml( 'semicolon-separator' ), $props );
+ $wgOut->addHTML( " ($list) " );
}
# Space for utilities links, with a what-links-here link provided
@@ -237,7 +231,7 @@ class WhatLinksHerePage {
wfMsgHtml( 'whatlinkshere-links' ),
'target=' . $nt->getPrefixedUrl()
);
- $wgOut->addHtml( ' <span class="mw-whatlinkshere-tools">(' . $wlh . ')</span>' );
+ $wgOut->addHtml( ' <span class="mw-whatlinkshere-tools">(' . $wlh . ')</span>' );
if ( $row->page_is_redirect ) {
if ( $level < 2 ) {
@@ -284,7 +278,7 @@ class WhatLinksHerePage {
$this->numLink( 250, $prevId ) . ' | ' .
$this->numLink( 500, $prevId );
- return wfMsg( 'viewprevnext', $prevLink, $nextLink, $nums );
+ return wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $nums );
}
function numLink( $limit, $from, $ns = null ) {
@@ -295,24 +289,26 @@ class WhatLinksHerePage {
return $this->makeSelfLink( $fmtLimit, $query );
}
- function whatlinkshereForm( $options ) {
+ function whatlinkshereForm( $options = array( 'target' => '', 'namespace' => '' ) ) {
global $wgScript, $wgTitle;
$options['title'] = $wgTitle->getPrefixedText();
- $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => "$wgScript" ) ) .
- '<fieldset>' .
- Xml::element( 'legend', array(), wfMsg( 'whatlinkshere' ) );
+ $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'whatlinkshere' ) ) .
+ Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target', 'mw-whatlinkshere-target', 40, $options['target'] ) . ' ';
foreach ( $options as $name => $value ) {
- if( $name === 'namespace') continue;
+ if( $name === 'namespace' || $name === 'target' )
+ continue;
$f .= "\t" . Xml::hidden( $name, $value ). "\n";
}
$f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
Xml::namespaceSelector( $options['namespace'], '' ) .
Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
- '</fieldset>' .
+ Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) . "\n";
return $f;
@@ -324,5 +320,3 @@ class WhatLinksHerePage {
}
}
-
-
diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php
index 33464586..37d9a282 100644
--- a/includes/SpecialWithoutinterwiki.php
+++ b/includes/SpecialWithoutinterwiki.php
@@ -8,13 +8,41 @@
* @author Rob Church <robchur@gmail.com>
*/
class WithoutInterwikiPage extends PageQueryPage {
+ private $prefix = '';
function getName() {
return 'Withoutinterwiki';
}
function getPageHeader() {
- return '<p>' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '</p>';
+ global $wgScript, $wgContLang;
+ $prefix = $this->prefix;
+ $t = SpecialPage::getTitleFor( $this->getName() );
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ $s = '<p>' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '</p>';
+ $s .= Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+ $s .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $s .= Xml::hidden( 'title', $t->getPrefixedText() );
+ $s .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'withoutinterwiki' ) );
+ $s .= "<tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'allpagesprefix' ), 'wiprefix' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'prefix', 20, htmlspecialchars ( $prefix ), array( 'id' => 'wiprefix' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'></td>
+ <td>" .
+ Xml::submitButton( wfMsgHtml( 'withoutinterwiki-submit' ) ) .
+ "</td>
+ </tr>";
+ $s .= Xml::closeElement( 'table' );
+ $s .= Xml::closeElement( 'form' );
+ $s .= Xml::closeElement( 'div' );
+ return $s;
}
function sortDescending() {
@@ -32,6 +60,7 @@ class WithoutInterwikiPage extends PageQueryPage {
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
+ $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
return
"SELECT 'Withoutinterwiki' AS type,
page_namespace AS namespace,
@@ -42,14 +71,22 @@ class WithoutInterwikiPage extends PageQueryPage {
ON ll_from = page_id
WHERE ll_title IS NULL
AND page_namespace=" . NS_MAIN . "
- AND page_is_redirect = 0";
+ AND page_is_redirect = 0
+ {$prefix}";
+ }
+
+ function setPrefix( $prefix = '' ) {
+ $this->prefix = $prefix;
}
}
function wfSpecialWithoutinterwiki() {
+ global $wgRequest;
list( $limit, $offset ) = wfCheckLimits();
+ $prefix = $wgRequest->getVal( 'prefix' );
$wip = new WithoutInterwikiPage();
+ $wip->setPrefix( $prefix );
$wip->doQuery( $offset, $limit );
}
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
index 5d7350a9..db2750cd 100644
--- a/includes/SquidUpdate.php
+++ b/includes/SquidUpdate.php
@@ -32,7 +32,7 @@ class SquidUpdate {
array( 'page_namespace', 'page_title' ),
array(
'pl_namespace' => $title->getNamespace(),
- 'pl_title' => $title->getDbKey(),
+ 'pl_title' => $title->getDBkey(),
'pl_from=page_id' ),
$fname );
$blurlArr = $title->getSquidURLs();
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index 8ecaa4f0..2dbbe6de 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -2,7 +2,7 @@
/** */
/** */
-function wfStreamFile( $fname ) {
+function wfStreamFile( $fname, $headers = array() ) {
$stat = @stat( $fname );
if ( !$stat ) {
header( 'HTTP/1.0 404 Not Found' );
@@ -34,6 +34,10 @@ function wfStreamFile( $fname ) {
global $wgContLanguageCode;
header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) ) );
+ foreach ( $headers as $header ) {
+ header( $header );
+ }
+
if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
$modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
$sinceTime = strtotime( $modsince );
diff --git a/includes/StubObject.php b/includes/StubObject.php
index a9a6bde9..aa72c360 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -105,7 +105,7 @@ class StubUserLang extends StubObject {
$code = $wgContLanguageCode;
}
- if( $code == $wgContLanguageCode ) {
+ if( $code === $wgContLanguageCode || !Language::localisationExist( $code ) ) {
return $wgContLang;
} else {
$obj = Language::factory( $code );
@@ -135,3 +135,4 @@ class StubUser extends StubObject {
}
+
diff --git a/includes/Title.php b/includes/Title.php
index c4db4172..8a9d3eee 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -207,6 +207,9 @@ class Title {
* Make an array of titles from an array of IDs
*/
public static function newFromIDs( $ids ) {
+ if ( !count( $ids ) ) {
+ return array();
+ }
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ),
'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ );
@@ -265,7 +268,12 @@ class Title {
* @return Title the new object
*/
public static function newMainPage() {
- return Title::newFromText( wfMsgForContent( 'mainpage' ) );
+ $title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
+ // Don't give fatal errors if the message is broken
+ if ( !$title ) {
+ $title = Title::newFromText( 'Main Page' );
+ }
+ return $title;
}
/**
@@ -670,7 +678,7 @@ class Title {
*/
public function getBaseText() {
global $wgNamespacesWithSubpages;
- if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
+ if( !empty( $wgNamespacesWithSubpages[$this->mNamespace] ) ) {
$parts = explode( '/', $this->getText() );
# Don't discard the real title if there's no subpage involved
if( count( $parts ) > 1 )
@@ -794,16 +802,15 @@ class Title {
} else {
$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
if ( $query == '' ) {
- if($variant!=false && $wgContLang->hasVariants()){
- if($wgVariantArticlePath==false) {
+ if( $variant != false && $wgContLang->hasVariants() ) {
+ if( $wgVariantArticlePath == false ) {
$variantArticlePath = "$wgScript?title=$1&variant=$2"; // default
} else {
$variantArticlePath = $wgVariantArticlePath;
}
$url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
$url = str_replace( '$1', $dbkey, $url );
- }
- else {
+ } else {
$url = str_replace( '$1', $dbkey, $wgArticlePath );
}
} else {
@@ -931,7 +938,7 @@ class Title {
/**
* Does the title correspond to a protected article?
* @param string $what the action the page is protected from,
- * by default checks move and edit
+ * by default checks move and edit
* @return boolean
*/
public function isProtected( $action = '' ) {
@@ -980,7 +987,7 @@ class Title {
return $this->mWatched;
}
- /**
+ /**
* Can $wgUser perform $action on this page?
* This skips potentially expensive cascading permission checks.
*
@@ -999,7 +1006,7 @@ class Title {
/**
* Determines if $wgUser is unable to edit this page because it has been protected
* by $wgNamespaceProtection.
- *
+ *
* @return boolean
*/
public function isNamespaceProtected() {
@@ -1013,7 +1020,7 @@ class Title {
return false;
}
- /**
+ /**
* Can $wgUser perform $action on this page?
* @param string $action action that permission needs to be checked for
* @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
@@ -1024,27 +1031,23 @@ class Title {
return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array());
}
- /**
+ /**
* Can $user perform $action on this page?
+ *
+ * FIXME: This *does not* check throttles (User::pingLimiter()).
+ *
* @param string $action action that permission needs to be checked for
* @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
* @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
- */
+ */
public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) {
$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
global $wgContLang;
global $wgLang;
-
- if ( wfReadOnly() && $action != 'read' ) {
- global $wgReadOnly;
- $errors[] = array( 'readonlytext', $wgReadOnly );
- }
-
global $wgEmailConfirmToEdit, $wgUser;
- if ( $wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed() )
- {
+ if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
$errors[] = array( 'confirmedittext' );
}
@@ -1056,6 +1059,9 @@ class Title {
$id = $user->blockedBy();
$reason = $user->blockedFor();
+ if( $reason == '' ) {
+ $reason = wfMsg( 'blockednoreason' );
+ }
$ip = wfGetIP();
if ( is_numeric( $id ) ) {
@@ -1067,7 +1073,7 @@ class Title {
$link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
$blockid = $block->mId;
$blockExpiry = $user->mBlock->mExpiry;
- $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true );
+ $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
if ( $blockExpiry == 'infinity' ) {
// Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
@@ -1097,22 +1103,45 @@ class Title {
}
/**
- * Can $user perform $action on this page?
- * This is an internal function, which checks ONLY that previously checked by userCan (i.e. it leaves out checks on wfReadOnly() and blocks)
+ * Can $user perform $action on this page? This is an internal function,
+ * which checks ONLY that previously checked by userCan (i.e. it leaves out
+ * checks on wfReadOnly() and blocks)
+ *
* @param string $action action that permission needs to be checked for
* @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
* @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
*/
private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) {
- $fname = 'Title::userCan';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$errors = array();
+ // Use getUserPermissionsErrors instead
if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
return $result ? array() : array( array( 'badaccess-group0' ) );
}
+ if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
+ if ($result != array() && is_array($result) && !is_array($result[0]))
+ $errors[] = $result; # A single array representing an error
+ else if (is_array($result) && is_array($result[0]))
+ $errors = array_merge( $errors, $result ); # A nested array representing multiple errors
+ else if ($result != '' && $result != null && $result !== true && $result !== false)
+ $errors[] = array($result); # A string representing a message-id
+ else if ($result === false )
+ $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
+ }
+ if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
+ if ($result != array() && is_array($result) && !is_array($result[0]))
+ $errors[] = $result; # A single array representing an error
+ else if (is_array($result) && is_array($result[0]))
+ $errors = array_merge( $errors, $result ); # A nested array representing multiple errors
+ else if ($result != '' && $result != null && $result !== true && $result !== false)
+ $errors[] = array($result); # A string representing a message-id
+ else if ($result === false )
+ $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
+ }
+
if( NS_SPECIAL == $this->mNamespace ) {
$errors[] = array('ns-specialprotected');
}
@@ -1135,7 +1164,7 @@ class Title {
# XXX: this might be better using restrictions
# XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
if( $this->isCssJsSubpage()
- && !$user->isAllowed('editinterface')
+ && !$user->isAllowed('editusercssjs')
&& !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) {
$errors[] = array('customcssjsprotected');
}
@@ -1169,52 +1198,146 @@ class Title {
$right = 'protect';
}
if( '' != $right && !$user->isAllowed( $right ) ) {
- $errors[] = array( 'protectedpagetext' );
+ $errors[] = array( 'protectedpagetext', $right );
}
}
- if( $action == 'create' ) {
+ if ($action == 'protect') {
+ if ($this->getUserPermissionsErrors('edit', $user) != array()) {
+ $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect.
+ }
+ }
+
+ if ($action == 'create') {
+ $title_protection = $this->getTitleProtection();
+
+ if (is_array($title_protection)) {
+ extract($title_protection);
+
+ if ($pt_create_perm == 'sysop')
+ $pt_create_perm = 'protect';
+
+ if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) {
+ $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason );
+ }
+ }
+
if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
$errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin');
}
} elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) {
$errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
- } else if ( !$user->isAllowed( $action ) ) {
+ } elseif ( !$user->isAllowed( $action ) ) {
$return = null;
- $groups = array();
+ $groups = array();
global $wgGroupPermissions;
- foreach( $wgGroupPermissions as $key => $value ) {
- if( isset( $value[$action] ) && $value[$action] == true ) {
- $groupName = User::getGroupName( $key );
- $groupPage = User::getGroupPage( $key );
- if( $groupPage ) {
- $skin = $user->getSkin();
- $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]';
- } else {
- $groups[] = $groupName;
- }
- }
- }
- $n = count( $groups );
- $groups = implode( ', ', $groups );
- switch( $n ) {
- case 0:
- case 1:
- case 2:
- $return = array( "badaccess-group$n", $groups );
- break;
- default:
- $return = array( 'badaccess-groups', $groups );
- }
+ foreach( $wgGroupPermissions as $key => $value ) {
+ if( isset( $value[$action] ) && $value[$action] == true ) {
+ $groupName = User::getGroupName( $key );
+ $groupPage = User::getGroupPage( $key );
+ if( $groupPage ) {
+ $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]';
+ } else {
+ $groups[] = $groupName;
+ }
+ }
+ }
+ $n = count( $groups );
+ $groups = implode( ', ', $groups );
+ switch( $n ) {
+ case 0:
+ case 1:
+ case 2:
+ $return = array( "badaccess-group$n", $groups );
+ break;
+ default:
+ $return = array( 'badaccess-groups', $groups );
+ }
$errors[] = $return;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $errors;
}
/**
+ * Is this title subject to title protection?
+ * @return mixed An associative array representing any existent title
+ * protection, or false if there's none.
+ */
+ private function getTitleProtection() {
+ // Can't protect pages in special namespaces
+ if ( $this->getNamespace() < 0 ) {
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'protected_titles', '*',
+ array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) );
+
+ if ($row = $dbr->fetchRow( $res )) {
+ return $row;
+ } else {
+ return false;
+ }
+ }
+
+ public function updateTitleProtection( $create_perm, $reason, $expiry ) {
+ global $wgGroupPermissions,$wgUser,$wgContLang;
+
+ if ($create_perm == implode(',',$this->getRestrictions('create'))
+ && $expiry == $this->mRestrictionsExpiry) {
+ // No change
+ return true;
+ }
+
+ list ($namespace, $title) = array( $this->getNamespace(), $this->getDBkey() );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $encodedExpiry = Block::encodeExpiry($expiry, $dbw );
+
+ $expiry_description = '';
+ if ( $encodedExpiry != 'infinity' ) {
+ $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
+ }
+
+ # Update protection table
+ if ($create_perm != '' ) {
+ $dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')),
+ array( 'pt_namespace' => $namespace, 'pt_title' => $title
+ , 'pt_create_perm' => $create_perm
+ , 'pt_timestamp' => Block::encodeExpiry(wfTimestampNow(), $dbw)
+ , 'pt_expiry' => $encodedExpiry
+ , 'pt_user' => $wgUser->getId(), 'pt_reason' => $reason ), __METHOD__ );
+ } else {
+ $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
+ 'pt_title' => $title ), __METHOD__ );
+ }
+ # Update the protection log
+ $log = new LogPage( 'protect' );
+
+ if( $create_perm ) {
+ $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) );
+ } else {
+ $log->addEntry( 'unprotect', $this, $reason );
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove any title protection (due to page existing
+ */
+ public function deleteTitleProtection() {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->delete( 'protected_titles',
+ array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ );
+ }
+
+ /**
* Can $wgUser edit this page?
* @return boolean
* @deprecated use userCan('edit')
@@ -1258,8 +1381,12 @@ class Title {
* @todo fold these checks into userCan()
*/
public function userCanRead() {
- global $wgUser;
-
+ global $wgUser, $wgGroupPermissions;
+
+ # Shortcut for public wikis, allows skipping quite a bit of code path
+ if ($wgGroupPermissions['*']['read'])
+ return true;
+
$result = null;
wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
if ( $result !== null ) {
@@ -1278,19 +1405,26 @@ class Title {
if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
return true;
}
+
+ /**
+ * Bail out if there isn't whitelist
+ */
+ if( !is_array($wgWhitelistRead) ) {
+ return false;
+ }
/**
* Check for explicit whitelisting
*/
$name = $this->getPrefixedText();
- if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead, true ) )
+ if( in_array( $name, $wgWhitelistRead, true ) )
return true;
/**
* Old settings might have the title prefixed with
* a colon for main-namespace pages
*/
- if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) {
+ if( $this->getNamespace() == NS_MAIN ) {
if( in_array( ':' . $name, $wgWhitelistRead ) )
return true;
}
@@ -1300,8 +1434,13 @@ class Title {
* and check again
*/
if( $this->getNamespace() == NS_SPECIAL ) {
- $name = $this->getText();
+ $name = $this->getDBkey();
list( $name, /* $subpage */) = SpecialPage::resolveAliasWithSubpage( $name );
+ if ( $name === false ) {
+ # Invalid special page, but we show standard login required message
+ return false;
+ }
+
$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
if( in_array( $pure, $wgWhitelistRead, true ) )
return true;
@@ -1394,7 +1533,7 @@ class Title {
*/
public function userCanEditCssJsSubpage() {
global $wgUser;
- return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
+ return ( $wgUser->isAllowed('editusercssjs') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
}
/**
@@ -1579,12 +1718,32 @@ class Title {
public function loadRestrictions( $oldFashionedRestrictions = NULL ) {
if( !$this->mRestrictionsLoaded ) {
- $dbr = wfGetDB( DB_SLAVE );
+ if ($this->exists()) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $res = $dbr->select( 'page_restrictions', '*',
+ array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
+
+ $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions );
+ } else {
+ $title_protection = $this->getTitleProtection();
+
+ if (is_array($title_protection)) {
+ extract($title_protection);
- $res = $dbr->select( 'page_restrictions', '*',
- array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
+ $now = wfTimestampNow();
+ $expiry = Block::decodeExpiry($pt_expiry);
- $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions );
+ if (!$expiry || $expiry > $now) {
+ // Apply the restrictions
+ $this->mRestrictionsExpiry = $expiry;
+ $this->mRestrictions['create'] = explode(',', trim($pt_create_perm) );
+ } else { // Get rid of the old restrictions
+ Title::purgeExpiredRestrictions();
+ }
+ }
+ $this->mRestrictionsLoaded = true;
+ }
}
}
@@ -1596,6 +1755,10 @@ class Title {
$dbw->delete( 'page_restrictions',
array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
__METHOD__ );
+
+ $dbw->delete( 'protected_titles',
+ array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+ __METHOD__ );
}
/**
@@ -1605,16 +1768,12 @@ class Title {
* @return array the array of groups allowed to edit this article
*/
public function getRestrictions( $action ) {
- if( $this->exists() ) {
- if( !$this->mRestrictionsLoaded ) {
- $this->loadRestrictions();
- }
- return isset( $this->mRestrictions[$action] )
- ? $this->mRestrictions[$action]
- : array();
- } else {
- return array();
+ if( !$this->mRestrictionsLoaded ) {
+ $this->loadRestrictions();
}
+ return isset( $this->mRestrictions[$action] )
+ ? $this->mRestrictions[$action]
+ : array();
}
/**
@@ -1753,8 +1912,18 @@ class Title {
# Initialisation
static $rxTc = false;
if( !$rxTc ) {
- # % is needed as well
- $rxTc = '/[^' . Title::legalChars() . ']|%[0-9A-Fa-f]{2}/S';
+ # Matching titles will be held as illegal.
+ $rxTc = '/' .
+ # Any character not allowed is forbidden...
+ '[^' . Title::legalChars() . ']' .
+ # URL percent encoding sequences interfere with the ability
+ # to round-trip titles -- you can't link to them consistently.
+ '|%[0-9A-Fa-f]{2}' .
+ # XML/HTML character references produce similar issues.
+ '|&[A-Za-z0-9\x80-\xff]+;' .
+ '|&#[0-9]+;' .
+ '|&#x[0-9A-Fa-f]+;' .
+ '/S';
}
$this->mInterwiki = $this->mFragment = '';
@@ -1868,7 +2037,9 @@ class Title {
strpos( $dbkey, './' ) === 0 ||
strpos( $dbkey, '../' ) === 0 ||
strpos( $dbkey, '/./' ) !== false ||
- strpos( $dbkey, '/../' ) !== false ) )
+ strpos( $dbkey, '/../' ) !== false ||
+ substr( $dbkey, -2 ) == '/.' ||
+ substr( $dbkey, -3 ) == '/..' ) )
{
return false;
}
@@ -1993,7 +2164,7 @@ class Title {
array(
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->getNamespace(),
- "{$prefix}_title" => $this->getDbKey() ),
+ "{$prefix}_title" => $this->getDBkey() ),
'Title::getLinksTo',
$options );
@@ -2027,10 +2198,16 @@ class Title {
/**
* Get an array of Title objects referring to non-existent articles linked from this page
*
+ * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
* @param string $options may be FOR UPDATE
* @return array the Title objects
*/
public function getBrokenLinksFrom( $options = '' ) {
+ if ( $this->getArticleId() == 0 ) {
+ # All links from article ID 0 are false positives
+ return array();
+ }
+
if ( $options ) {
$db = wfGetDB( DB_MASTER );
} else {
@@ -2137,10 +2314,20 @@ class Title {
return 'badarticleerror';
}
- if ( $auth && (
- !$this->userCan( 'edit' ) || !$nt->userCan( 'edit' ) ||
- !$this->userCan( 'move' ) || !$nt->userCan( 'move' ) ) ) {
- return 'protectedpage';
+ if ( $auth ) {
+ global $wgUser;
+ $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser),
+ $this->getUserPermissionsErrors('edit', $wgUser),
+ $nt->getUserPermissionsErrors('move', $wgUser),
+ $nt->getUserPermissionsErrors('edit', $wgUser));
+ if($errors !== array())
+ return $errors[0][0];
+ }
+
+ global $wgUser;
+ $err = null;
+ if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err ) ) ) {
+ return 'hookaborted';
}
# The move is allowed only if (1) the target doesn't exist, or
@@ -2151,6 +2338,11 @@ class Title {
if ( ! $this->isValidMoveTarget( $nt ) ) {
return 'articleexists';
}
+ } else {
+ $tp = $nt->getTitleProtection();
+ if ( $tp and !$wgUser->isAllowed( $tp['pt_create_perm'] ) ) {
+ return 'cantmove-titleprotected';
+ }
}
return true;
}
@@ -2160,9 +2352,12 @@ class Title {
* @param Title &$nt the new title
* @param bool $auth indicates whether $wgUser's permissions
* should be checked
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
+ * Ignored if the user doesn't have the suppressredirect right.
* @return mixed true on success, message name on failure
*/
- public function moveTo( &$nt, $auth = true, $reason = '' ) {
+ public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
$err = $this->isValidMoveOperation( $nt, $auth );
if( is_string( $err ) ) {
return $err;
@@ -2170,21 +2365,34 @@ class Title {
$pageid = $this->getArticleID();
if( $nt->exists() ) {
- $this->moveOverExistingRedirect( $nt, $reason );
- $pageCountChange = 0;
+ $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
+ $pageCountChange = ($createRedirect ? 0 : -1);
} else { # Target didn't exist, do normal move.
- $this->moveToNewTitle( $nt, $reason );
- $pageCountChange = 1;
+ $this->moveToNewTitle( $nt, $reason, $createRedirect );
+ $pageCountChange = ($createRedirect ? 1 : 0);
}
$redirid = $this->getArticleID();
- # Fixing category links (those without piped 'alternate' names) to be sorted under the new title
+ // Category memberships include a sort key which may be customized.
+ // If it's left as the default (the page title), we need to update
+ // the sort key to match the new title.
+ //
+ // Be careful to avoid resetting cl_timestamp, which may disturb
+ // time-based lists on some sites.
+ //
+ // Warning -- if the sort key is *explicitly* set to the old title,
+ // we can't actually distinguish it from a default here, and it'll
+ // be set to the new title even though it really shouldn't.
+ // It'll get corrected on the next edit, but resetting cl_timestamp.
$dbw = wfGetDB( DB_MASTER );
- $categorylinks = $dbw->tableName( 'categorylinks' );
- $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) .
- " WHERE cl_from=" . $dbw->addQuotes( $pageid ) .
- " AND cl_sortkey=" . $dbw->addQuotes( $this->getPrefixedText() );
- $dbw->query( $sql, 'SpecialMovepage::doSubmit' );
+ $dbw->update( 'categorylinks',
+ array(
+ 'cl_sortkey' => $nt->getPrefixedText(),
+ 'cl_timestamp=cl_timestamp' ),
+ array(
+ 'cl_from' => $pageid,
+ 'cl_sortkey' => $this->getPrefixedText() ),
+ __METHOD__ );
# Update watchlists
@@ -2221,6 +2429,14 @@ class Title {
}
if( $u )
$u->doUpdate();
+ # Update message cache for interface messages
+ if( $nt->getNamespace() == NS_MEDIAWIKI ) {
+ global $wgMessageCache;
+ $oldarticle = new Article( $this );
+ $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() );
+ $newarticle = new Article( $nt );
+ $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() );
+ }
global $wgUser;
wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
@@ -2233,9 +2449,12 @@ class Title {
*
* @param Title &$nt the page to move to, which should currently
* be a redirect
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to leave a redirect at the old title.
+ * Ignored if the user doesn't have the suppressredirect right
*/
- private function moveOverExistingRedirect( &$nt, $reason = '' ) {
- global $wgUseSquid;
+ private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
+ global $wgUseSquid, $wgUser;
$fname = 'Title::moveOverExistingRedirect';
$comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() );
@@ -2247,13 +2466,25 @@ class Title {
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
$dbw = wfGetDB( DB_MASTER );
- $linkCache =& LinkCache::singleton();
# Delete the old redirect. We don't save it to history since
# by definition if we've got here it's rather uninteresting.
# We have to remove it so that the next step doesn't trigger
# a conflict on the unique namespace+title index...
$dbw->delete( 'page', array( 'page_id' => $newid ), $fname );
+ if ( !$dbw->cascadingDeletes() ) {
+ $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
+ global $wgUseTrackbacks;
+ if ($wgUseTrackbacks)
+ $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ );
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
+ $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
+ }
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
@@ -2270,35 +2501,39 @@ class Title {
/* WHERE */ array( 'page_id' => $oldid ),
$fname
);
- $linkCache->clearLink( $nt->getPrefixedDBkey() );
+ $nt->resetArticleID( $oldid );
# Recreate the redirect, this time in the other direction.
- $mwRedir = MagicWord::get( 'redirect' );
- $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
- $redirectArticle = new Article( $this );
- $newid = $redirectArticle->insertOn( $dbw );
- $redirectRevision = new Revision( array(
- 'page' => $newid,
- 'comment' => $comment,
- 'text' => $redirectText ) );
- $redirectRevision->insertOn( $dbw );
- $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
- $linkCache->clearLink( $this->getPrefixedDBkey() );
-
+ if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
+ {
+ $mwRedir = MagicWord::get( 'redirect' );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $this );
+ $newid = $redirectArticle->insertOn( $dbw );
+ $redirectRevision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ # Now, we record the link from the redirect to the new title.
+ # It should have no other outgoing links...
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname );
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $newid,
+ 'pl_namespace' => $nt->getNamespace(),
+ 'pl_title' => $nt->getDBkey() ),
+ $fname );
+ } else {
+ $this->resetArticleID( 0 );
+ }
+
# Log the move
$log = new LogPage( 'move' );
$log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
- # Now, we record the link from the redirect to the new title.
- # It should have no other outgoing links...
- $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname );
- $dbw->insert( 'pagelinks',
- array(
- 'pl_from' => $newid,
- 'pl_namespace' => $nt->getNamespace(),
- 'pl_title' => $nt->getDbKey() ),
- $fname );
-
# Purge squid
if ( $wgUseSquid ) {
$urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() );
@@ -2310,9 +2545,12 @@ class Title {
/**
* Move page to non-existing title.
* @param Title &$nt the new Title
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to create a redirect from the old title to the new title
+ * Ignored if the user doesn't have the suppressredirect right
*/
- private function moveToNewTitle( &$nt, $reason = '' ) {
- global $wgUseSquid;
+ private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
+ global $wgUseSquid, $wgUser;
$fname = 'MovePageForm::moveToNewTitle';
$comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
if ( $reason ) {
@@ -2323,13 +2561,12 @@ class Title {
$oldid = $this->getArticleID();
$dbw = wfGetDB( DB_MASTER );
$now = $dbw->timestamp();
- $linkCache =& LinkCache::singleton();
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
$nullRevId = $nullRevision->insertOn( $dbw );
- # Rename cur entry
+ # Rename page entry
$dbw->update( 'page',
/* SET */ array(
'page_touched' => $now,
@@ -2340,21 +2577,32 @@ class Title {
/* WHERE */ array( 'page_id' => $oldid ),
$fname
);
+ $nt->resetArticleID( $oldid );
- $linkCache->clearLink( $nt->getPrefixedDBkey() );
-
- # Insert redirect
- $mwRedir = MagicWord::get( 'redirect' );
- $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
- $redirectArticle = new Article( $this );
- $newid = $redirectArticle->insertOn( $dbw );
- $redirectRevision = new Revision( array(
- 'page' => $newid,
- 'comment' => $comment,
- 'text' => $redirectText ) );
- $redirectRevision->insertOn( $dbw );
- $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
- $linkCache->clearLink( $this->getPrefixedDBkey() );
+ if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
+ {
+ # Insert redirect
+ $mwRedir = MagicWord::get( 'redirect' );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $this );
+ $newid = $redirectArticle->insertOn( $dbw );
+ $redirectRevision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ # Record the just-created redirect's linking to the page
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $newid,
+ 'pl_namespace' => $nt->getNamespace(),
+ 'pl_title' => $nt->getDBkey() ),
+ $fname );
+ } else {
+ $this->resetArticleID( 0 );
+ }
# Log the move
$log = new LogPage( 'move' );
@@ -2363,14 +2611,6 @@ class Title {
# Purge caches as per article creation
Article::onArticleCreate( $nt );
- # Record the just-created redirect's linking to the page
- $dbw->insert( 'pagelinks',
- array(
- 'pl_from' => $newid,
- 'pl_namespace' => $nt->getNamespace(),
- 'pl_title' => $nt->getDBkey() ),
- $fname );
-
# Purge old title from squid
# The new title, and links to the new title, are purged in Article::onArticleCreate()
$this->purgeSquid();
@@ -2469,7 +2709,7 @@ class Title {
$data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
$dbr->freeResult ( $res ) ;
} else {
- $data = '';
+ $data = array();
}
return $data;
}
@@ -2562,7 +2802,7 @@ class Title {
// Note: === is necessary for proper matching of number-like titles.
return $this->getInterwiki() === $title->getInterwiki()
&& $this->getNamespace() == $title->getNamespace()
- && $this->getDbkey() === $title->getDbkey();
+ && $this->getDBkey() === $title->getDBkey();
}
/**
@@ -2589,9 +2829,15 @@ class Title {
* @return bool
*/
public function isAlwaysKnown() {
+ // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
+ // the full l10n of that language to be loaded. That takes much memory and
+ // isn't needed. So we strip the language part away.
+ // Also, extension messages which are not loaded, are shown as red, because
+ // we don't call MessageCache::loadAllMessages.
+ list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
return $this->isExternal()
|| ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' )
- || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $this->mDbkeyform ) );
+ || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) );
}
/**
@@ -2730,5 +2976,3 @@ class Title {
}
}
-
-
diff --git a/includes/User.php b/includes/User.php
index 51b0b2ec..8e3c776a 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -34,8 +34,8 @@ class PasswordError extends MWException {
class User {
/**
- * A list of default user toggles, i.e. boolean user preferences that are
- * displayed by Special:Preferences as checkboxes. This list can be
+ * A list of default user toggles, i.e. boolean user preferences that are
+ * displayed by Special:Preferences as checkboxes. This list can be
* extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
*/
static public $mToggles = array(
@@ -80,7 +80,7 @@ class User {
/**
* List of member variables which are saved to the shared cache (memcached).
- * Any operation which changes the corresponding database fields must
+ * Any operation which changes the corresponding database fields must
* call a cache-clearing function.
*/
static $mCacheVars = array(
@@ -107,8 +107,8 @@ class User {
/**
* The cache variable declarations
*/
- var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
- $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
+ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
+ $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
$mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
/**
@@ -133,7 +133,7 @@ class User {
var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
$mBlockreason, $mBlock, $mEffectiveGroups;
- /**
+ /**
* Lightweight constructor for anonymous user
* Use the User::newFrom* factory functions for other kinds of users
*/
@@ -188,7 +188,7 @@ class User {
if ( $this->mId == 0 ) {
$this->loadDefaults();
return false;
- }
+ }
# Try cache
$key = wfMemcKey( 'user', 'id', $this->mId );
@@ -197,7 +197,7 @@ class User {
# Object is expired, load from DB
$data = false;
}
-
+
if ( !$data ) {
wfDebug( "Cache miss for user {$this->mId}\n" );
# Load from DB
@@ -206,13 +206,7 @@ class User {
return false;
}
- # Save to cache
- $data = array();
- foreach ( self::$mCacheVars as $name ) {
- $data[$name] = $this->$name;
- }
- $data['mVersion'] = MW_USER_VERSION;
- $wgMemc->set( $key, $data );
+ $this->saveToCache();
} else {
wfDebug( "Got user {$this->mId} from cache\n" );
# Restore from cache
@@ -224,19 +218,38 @@ class User {
}
/**
+ * Save user data to the shared cache
+ */
+ function saveToCache() {
+ $this->load();
+ if ( $this->isAnon() ) {
+ // Anonymous users are uncached
+ return;
+ }
+ $data = array();
+ foreach ( self::$mCacheVars as $name ) {
+ $data[$name] = $this->$name;
+ }
+ $data['mVersion'] = MW_USER_VERSION;
+ $key = wfMemcKey( 'user', 'id', $this->mId );
+ global $wgMemc;
+ $wgMemc->set( $key, $data );
+ }
+
+ /**
* Static factory method for creation from username.
*
* This is slightly less efficient than newFromId(), so use newFromId() if
- * you have both an ID and a name handy.
+ * you have both an ID and a name handy.
*
* @param string $name Username, validated by Title:newFromText()
- * @param mixed $validate Validate username. Takes the same parameters as
- * User::getCanonicalName(), except that true is accepted as an alias
+ * @param mixed $validate Validate username. Takes the same parameters as
+ * User::getCanonicalName(), except that true is accepted as an alias
* for 'valid', for BC.
- *
- * @return User object, or null if the username is invalid. If the username
+ *
+ * @return User object, or null if the username is invalid. If the username
* is not present in the database, the result will be a user object with
- * a name, zero user ID and default settings.
+ * a name, zero user ID and default settings.
* @static
*/
static function newFromName( $name, $validate = 'valid' ) {
@@ -285,7 +298,7 @@ class User {
return null;
}
}
-
+
/**
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
@@ -348,9 +361,9 @@ class User {
*
* This function exists for username validation, in order to reject
* usernames which are similar in form to IP addresses. Strings such
- * as 300.300.300.300 will return true because it looks like an IP
+ * as 300.300.300.300 will return true because it looks like an IP
* address, despite not being strictly valid.
- *
+ *
* We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
* address because the usemod software would "cloak" anonymous IP
* addresses like this, if we allowed accounts like this to be created
@@ -374,8 +387,8 @@ class User {
* Check if $name is an IPv6 IP.
*/
static function isIPv6($name) {
- /*
- * if it has any non-valid characters, it can't be a valid IPv6
+ /*
+ * if it has any non-valid characters, it can't be a valid IPv6
* address.
*/
if (preg_match("/[^:a-fA-F0-9]/", $name))
@@ -420,7 +433,7 @@ class User {
|| $parsed->getNamespace()
|| strcmp( $name, $parsed->getPrefixedText() ) )
return false;
-
+
// Check an additional blacklist of troublemaker characters.
// Should these be merged into the title char list?
$unicodeBlacklist = '/[' .
@@ -434,10 +447,10 @@ class User {
if( preg_match( $unicodeBlacklist, $name ) ) {
return false;
}
-
+
return true;
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from user login and new account registrations, but may be used
@@ -454,11 +467,11 @@ class User {
return
// Must be a valid username, obviously ;)
self::isValidUserName( $name ) &&
-
+
// Certain names may be reserved for batch processes.
!in_array( $name, $wgReservedUsernames );
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from new account registrations, but may be used internally
@@ -475,7 +488,7 @@ class User {
static function isCreatableName( $name ) {
return
self::isUsableName( $name ) &&
-
+
// Registration-time character blacklisting...
strpos( $name, '@' ) === false;
}
@@ -494,7 +507,7 @@ class User {
return $result;
if( $result === false )
return false;
-
+
// Password needs to be long enough, and can't be the same as the username
return strlen( $password ) >= $wgMinimalPasswordLength
&& $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
@@ -513,11 +526,16 @@ class User {
* @return bool
*/
public static function isValidEmailAddr( $addr ) {
+ $result = null;
+ if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
+ return $result;
+ }
+
return strpos( $addr, '@' ) !== false;
}
/**
- * Given unvalidated user input, return a canonical username, or false if
+ * Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
* @param string $name
* @param mixed $validate Type of validation to use:
@@ -576,7 +594,7 @@ class User {
* Count the number of edits of a user
*
* It should not be static and some day should be merged as proper member function / deprecated -- domas
- *
+ *
* @param int $uid The user ID to check
* @return int
* @static
@@ -592,7 +610,7 @@ class User {
);
if( $field === null ) { // it has not been initialized. do so.
- $dbw = wfGetDb( DB_MASTER );
+ $dbw = wfGetDB( DB_MASTER );
$count = $dbr->selectField(
'revision', 'count(*)',
array( 'rev_user' => $uid ),
@@ -633,7 +651,7 @@ class User {
}
/**
- * Set cached properties to default. Note: this no longer clears
+ * Set cached properties to default. Note: this no longer clears
* uncached lazy-initialised properties. The constructor does that instead.
*
* @private
@@ -666,7 +684,7 @@ class User {
wfProfileOut( __METHOD__ );
}
-
+
/**
* Initialise php session
* @deprecated use wfSetupSession()
@@ -713,7 +731,7 @@ class User {
# Not a valid ID, loadFromId has switched the object to anon for us
return false;
}
-
+
if ( isset( $_SESSION['wsToken'] ) ) {
$passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
$from = 'session';
@@ -737,11 +755,11 @@ class User {
return false;
}
}
-
+
/**
* Load user and user_group data from the database
* $this->mId must be set, this is how the user is identified.
- *
+ *
* @return true if the user exists, false if the user is anonymous
* @private
*/
@@ -773,7 +791,7 @@ class User {
$this->mEmailToken = $s->user_email_token;
$this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
$this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
- $this->mEditCount = $s->user_editcount;
+ $this->mEditCount = $s->user_editcount;
$this->getEditCount(); // revalidation for nulls
# Load group data
@@ -795,9 +813,9 @@ class User {
}
/**
- * Clear various cached data stored in this object.
- * @param string $reloadFrom Reload user and user_groups table data from a
- * given source. May be "name", "id", "defaults", "session" or false for
+ * Clear various cached data stored in this object.
+ * @param string $reloadFrom Reload user and user_groups table data from a
+ * given source. May be "name", "id", "defaults", "session" or false for
* no reload.
*/
function clearInstanceCache( $reloadFrom = false ) {
@@ -891,7 +909,7 @@ class User {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__.": checking...\n" );
- $this->mBlockedby = 0;
+ $this->mBlockedby = 0;
$this->mHideName = 0;
$ip = wfGetIP();
@@ -1004,7 +1022,7 @@ class User {
return $result;
}
- global $wgRateLimits, $wgRateLimitsExcludedGroups;
+ global $wgRateLimits;
if( !isset( $wgRateLimits[$action] ) ) {
return false;
}
@@ -1158,12 +1176,12 @@ class User {
}
/**
- * Set the user name.
+ * Set the user name.
*
- * This does not reload fields from the database according to the given
+ * This does not reload fields from the database according to the given
* name. Rather, it is used to create a temporary "nonexistent user" for
- * later addition to the database. It can also be used to set the IP
- * address for an anonymous user to something other than the current
+ * later addition to the database. It can also be used to set the IP
+ * address for an anonymous user to something other than the current
* remote IP.
*
* User::newFromName() has rougly the same function, when the named user
@@ -1196,11 +1214,13 @@ class User {
global $wgMemc;
$key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
$newtalk = $wgMemc->get( $key );
- if( $newtalk != "" ) {
+ if( strval( $newtalk ) !== '' ) {
$this->mNewtalk = (bool)$newtalk;
} else {
- $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
- $wgMemc->set( $key, (int)$this->mNewtalk, time() + 1800 );
+ // Since we are caching this, make sure it is up to date by getting it
+ // from the master
+ $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
+ $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
}
} else {
$this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
@@ -1225,20 +1245,24 @@ class User {
return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
}
-
+
/**
- * Perform a user_newtalk check on current slaves; if the memcached data
- * is funky we don't want newtalk state to get stuck on save, as that's
- * damn annoying.
- *
+ * Perform a user_newtalk check, uncached.
+ * Use getNewtalk for a cached check.
+ *
* @param string $field
* @param mixed $id
+ * @param bool $fromMaster True to fetch from the master, false for a slave
* @return bool
* @private
*/
- function checkNewtalk( $field, $id ) {
- $dbr = wfGetDB( DB_SLAVE );
- $ok = $dbr->selectField( 'user_newtalk', $field,
+ function checkNewtalk( $field, $id, $fromMaster = false ) {
+ if ( $fromMaster ) {
+ $db = wfGetDB( DB_MASTER );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ }
+ $ok = $db->selectField( 'user_newtalk', $field,
array( $field => $id ), __METHOD__ );
return $ok !== false;
}
@@ -1250,17 +1274,18 @@ class User {
* @private
*/
function updateNewtalk( $field, $id ) {
- if( $this->checkNewtalk( $field, $id ) ) {
- wfDebug( __METHOD__." already set ($field, $id), ignoring\n" );
- return false;
- }
$dbw = wfGetDB( DB_MASTER );
$dbw->insert( 'user_newtalk',
array( $field => $id ),
__METHOD__,
'IGNORE' );
- wfDebug( __METHOD__.": set on ($field, $id)\n" );
- return true;
+ if ( $dbw->affectedRows() ) {
+ wfDebug( __METHOD__.": set on ($field, $id)\n" );
+ return true;
+ } else {
+ wfDebug( __METHOD__." already set ($field, $id)\n" );
+ return false;
+ }
}
/**
@@ -1270,16 +1295,17 @@ class User {
* @private
*/
function deleteNewtalk( $field, $id ) {
- if( !$this->checkNewtalk( $field, $id ) ) {
- wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" );
- return false;
- }
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'user_newtalk',
array( $field => $id ),
__METHOD__ );
- wfDebug( __METHOD__.": killed on ($field, $id)\n" );
- return true;
+ if ( $dbw->affectedRows() ) {
+ wfDebug( __METHOD__.": killed on ($field, $id)\n" );
+ return true;
+ } else {
+ wfDebug( __METHOD__.": already gone ($field, $id)\n" );
+ return false;
+ }
}
/**
@@ -1301,6 +1327,7 @@ class User {
$field = 'user_id';
$id = $this->getId();
}
+ global $wgMemc;
if( $val ) {
$changed = $this->updateNewtalk( $field, $id );
@@ -1308,24 +1335,17 @@ class User {
$changed = $this->deleteNewtalk( $field, $id );
}
- if( $changed ) {
- if( $this->isAnon() ) {
- // Anons have a separate memcached space, since
- // user records aren't kept for them.
- global $wgMemc;
- $key = wfMemcKey( 'newtalk', 'ip', $val );
- $wgMemc->set( $key, $val ? 1 : 0 );
- } else {
- if( $val ) {
- // Make sure the user page is watched, so a notification
- // will be sent out if enabled.
- $this->addWatch( $this->getTalkPage() );
- }
- }
+ if( $this->isAnon() ) {
+ // Anons have a separate memcached space, since
+ // user records aren't kept for them.
+ $key = wfMemcKey( 'newtalk', 'ip', $id );
+ $wgMemc->set( $key, $val ? 1 : 0, 1800 );
+ }
+ if ( $changed ) {
$this->invalidateCache();
}
}
-
+
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
@@ -1334,7 +1354,7 @@ class User {
global $wgClockSkewFudge;
return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
}
-
+
/**
* Clear user data from memcached.
* Use after applying fun updates to the database; caller's
@@ -1358,13 +1378,13 @@ class User {
$this->load();
if( $this->mId ) {
$this->mTouched = self::newTouchedTimestamp();
-
+
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'user',
array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
array( 'user_id' => $this->mId ),
__METHOD__ );
-
+
$this->clearSharedCache();
}
}
@@ -1402,12 +1422,12 @@ class User {
*/
function setPassword( $str ) {
global $wgAuth;
-
+
if( $str !== null ) {
if( !$wgAuth->allowPasswordChange() ) {
throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
}
-
+
if( !$this->isValidPassword( $str ) ) {
global $wgMinimalPasswordLength;
throw new PasswordError( wfMsg( 'passwordtooshort',
@@ -1418,7 +1438,7 @@ class User {
if( !$wgAuth->setPassword( $this, $str ) ) {
throw new PasswordError( wfMsg( 'externaldberror' ) );
}
-
+
$this->setInternalPassword( $str );
return true;
@@ -1433,7 +1453,7 @@ class User {
function setInternalPassword( $str ) {
$this->load();
$this->setToken();
-
+
if( $str === null ) {
// Save an invalid hash...
$this->mPassword = '';
@@ -1495,7 +1515,7 @@ class User {
$expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
return time() < $expiry;
}
-
+
function getEmail() {
$this->load();
return $this->mEmail;
@@ -1544,7 +1564,7 @@ class User {
}
/**
- * Get the user's date preference, including some important migration for
+ * Get the user's date preference, including some important migration for
* old user rows.
*/
function getDatePreference() {
@@ -1567,7 +1587,7 @@ class User {
function getBoolOption( $oname ) {
return (bool)$this->getOption( $oname );
}
-
+
/**
* Get an option as an integer value from the source string.
* @param string $oname The option to check
@@ -1619,8 +1639,8 @@ class User {
/**
* Get the list of implicit group memberships this user has.
- * This includes all explicit groups, plus 'user' if logged in
- * and '*' for all accounts.
+ * This includes all explicit groups, plus 'user' if logged in,
+ * '*' for all accounts and autopromoted groups
* @param boolean $recache Don't use the cache
* @return array of strings
*/
@@ -1632,43 +1652,32 @@ class User {
if( $this->mId ) {
$this->mEffectiveGroups[] = 'user';
- global $wgAutoConfirmAge, $wgAutoConfirmCount;
+ $this->mEffectiveGroups = array_unique( array_merge(
+ $this->mEffectiveGroups,
+ Autopromote::getAutopromoteGroups( $this )
+ ) );
- $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
- if( $accountAge >= $wgAutoConfirmAge && $this->getEditCount() >= $wgAutoConfirmCount ) {
- $this->mEffectiveGroups[] = 'autoconfirmed';
- }
- # Implicit group for users whose email addresses are confirmed
- global $wgEmailAuthentication;
- if( self::isValidEmailAddr( $this->mEmail ) ) {
- if( $wgEmailAuthentication ) {
- if( $this->mEmailAuthenticated )
- $this->mEffectiveGroups[] = 'emailconfirmed';
- } else {
- $this->mEffectiveGroups[] = 'emailconfirmed';
- }
- }
# Hook for additional groups
wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
}
}
return $this->mEffectiveGroups;
}
-
+
/* Return the edit count for the user. This is where User::edits should have been */
function getEditCount() {
if ($this->mId) {
if ( !isset( $this->mEditCount ) ) {
/* Populate the count, if it has not been populated yet */
$this->mEditCount = User::edits($this->mId);
- }
+ }
return $this->mEditCount;
} else {
/* nil */
return null;
}
}
-
+
/**
* Add the user to the given group.
* This takes immediate effect.
@@ -1722,10 +1731,6 @@ class User {
* @return bool
*/
function isLoggedIn() {
- if( $this->mId === null and $this->mName !== null ) {
- // Special-case optimization
- return !self::isIP( $this->mName );
- }
return $this->getID() != 0;
}
@@ -1893,7 +1898,7 @@ class User {
'wl_notificationtimestamp' => NULL
), array( /* WHERE */
'wl_user' => $currentUser
- ), 'UserMailer::clearAll'
+ ), __METHOD__
);
# we also need to clear here the "you have new message" notification for the own user_talk page
@@ -1953,10 +1958,21 @@ class User {
}
/**
- * Logout user
- * Clears the cookies and session, resets the instance cache
+ * Logout user.
*/
function logout() {
+ global $wgUser;
+ if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
+ $this->doLogout();
+ wfRunHooks( 'UserLogoutComplete', array(&$wgUser) );
+ }
+ }
+
+ /**
+ * Really logout user
+ * Clears the cookies and session, resets the instance cache
+ */
+ function doLogout() {
global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
$this->clearInstanceCache( 'defaults' );
@@ -1977,7 +1993,7 @@ class User {
$this->load();
if ( wfReadOnly() ) { return; }
if ( 0 == $this->mId ) { return; }
-
+
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
@@ -2002,11 +2018,11 @@ class User {
/**
- * Checks if a user with the given name exists, returns the ID
+ * Checks if a user with the given name exists, returns the ID.
*/
function idForName() {
$s = trim( $this->getName() );
- if ( 0 == strcmp( '', $s ) ) return 0;
+ if ( $s === '' ) return 0;
$dbr = wfGetDB( DB_SLAVE );
$id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
@@ -2066,7 +2082,7 @@ class User {
}
return $newUser;
}
-
+
/**
* Add an existing user object to the database
*/
@@ -2252,6 +2268,9 @@ class User {
} elseif( $wgAuth->strict() ) {
/* Auth plugin doesn't allow local authentication */
return false;
+ } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
+ /* Auth plugin doesn't allow local authentication for this user name */
+ return false;
}
$ep = $this->encryptPassword( $password );
if ( 0 == strcmp( $ep, $this->mPassword ) ) {
@@ -2266,7 +2285,7 @@ class User {
}
return false;
}
-
+
/**
* Check if the given clear-text password matches the temporary password
* sent by e-mail for password reset operations.
@@ -2366,25 +2385,18 @@ class User {
*
* @param string $subject
* @param string $body
- * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
+ * @param string $from Optional from address; default $wgPasswordSender will be used otherwise.
* @return mixed True on success, a WikiError object on failure.
*/
- function sendMail( $subject, $body, $from = null ) {
+ function sendMail( $subject, $body, $from = null, $replyto = null ) {
if( is_null( $from ) ) {
global $wgPasswordSender;
$from = $wgPasswordSender;
}
- require_once( 'UserMailer.php' );
$to = new MailAddress( $this );
$sender = new MailAddress( $from );
- $error = userMailer( $to, $sender, $subject, $body );
-
- if( $error == '' ) {
- return true;
- } else {
- return new WikiError( $error );
- }
+ return UserMailer::send( $to, $sender, $subject, $body, $replyto );
}
/**
@@ -2441,7 +2453,9 @@ class User {
* @return bool
*/
function canSendEmail() {
- return $this->isEmailConfirmed();
+ $canSend = $this->isEmailConfirmed();
+ wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
+ return $canSend;
}
/**
@@ -2450,7 +2464,7 @@ class User {
* @return bool
*/
function canReceiveEmail() {
- return $this->canSendEmail() && !$this->getOption( 'disablemail' );
+ return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
}
/**
@@ -2479,7 +2493,7 @@ class User {
return $confirmed;
}
}
-
+
/**
* Return true if there is an outstanding request for e-mail confirmation.
* @return bool
@@ -2491,7 +2505,7 @@ class User {
$this->mEmailToken &&
$this->mEmailTokenExpires > wfTimestamp();
}
-
+
/**
* Get the timestamp of account creation, or false for
* non-existent/anonymous user accounts
@@ -2573,11 +2587,9 @@ class User {
* @return array
*/
public static function getImplicitGroups() {
- static $groups = null;
- if( !is_array( $groups ) ) {
- $groups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' );
- wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );
- }
+ global $wgImplicitGroups;
+ $groups = $wgImplicitGroups;
+ wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
return $groups;
}
@@ -2639,7 +2651,7 @@ class User {
return $text;
}
}
-
+
/**
* Increment the user's edit-count field.
* Will have no effect for anonymous users.
@@ -2651,7 +2663,7 @@ class User {
array( 'user_editcount=user_editcount+1' ),
array( 'user_id' => $this->getId() ),
__METHOD__ );
-
+
// Lazy initialization check...
if( $dbw->affectedRows() == 0 ) {
// Pull from a slave to be less cruel to servers
@@ -2661,7 +2673,7 @@ class User {
'COUNT(rev_user)',
array( 'rev_user' => $this->getId() ),
__METHOD__ );
-
+
// Now here's a goddamn hack...
if( $dbr !== $dbw ) {
// If we actually have a slave server, the count is
@@ -2673,7 +2685,7 @@ class User {
// count we just read includes the revision that was
// just added in the working transaction.
}
-
+
$dbw->update( 'user',
array( 'user_editcount' => $count ),
array( 'user_id' => $this->getId() ),
@@ -2686,3 +2698,4 @@ class User {
}
+
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index 835dd310..d043a6b5 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -1,9 +1,5 @@
<?php
/**
- * UserMailer.php
- * Copyright (C) 2004 Thomas Gries <mail@tgries.de>
- * http://www.mediawiki.org/
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -21,16 +17,10 @@
*
* @author <brion@pobox.com>
* @author <mail@tgries.de>
+ * @author Tim Starling
*
*/
-/**
- * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name
- */
-function wfRFC822Phrase( $phrase ) {
- $phrase = strtr( $phrase, array( "\r" => '', "\n" => '', '"' => '' ) );
- return '"' . $phrase . '"';
-}
/**
* Stores a single person's name and email address.
@@ -62,7 +52,7 @@ class MailAddress {
# so don't bother generating them
if( $this->name != '' && !wfIsWindows() ) {
$quoted = wfQuotedPrintable( $this->name );
- if( strpos( $quoted, '.' ) !== false ) {
+ if( strpos( $quoted, '.' ) !== false || strpos( $quoted, ',' ) !== false ) {
$quoted = '"' . $quoted . '"';
}
return "$quoted <{$this->address}>";
@@ -70,155 +60,178 @@ class MailAddress {
return $this->address;
}
}
-}
-function send_mail($mailer, $dest, $headers, $body)
-{
- $mailResult =& $mailer->send($dest, $headers, $body);
-
- # Based on the result return an error string,
- if ($mailResult === true) {
- return '';
- } elseif (is_object($mailResult)) {
- wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
- return $mailResult->getMessage();
- } else {
- wfDebug( "PEAR::Mail failed, unknown error result\n" );
- return 'Mail object return unknown error.';
+ function __toString() {
+ return $this->toString();
}
}
+
/**
- * This function will perform a direct (authenticated) login to
- * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
- * array of parameters. It requires PEAR:Mail to do that.
- * Otherwise it just uses the standard PHP 'mail' function.
- *
- * @param $to MailAddress: recipient's email
- * @param $from MailAddress: sender's email
- * @param $subject String: email's subject.
- * @param $body String: email's text.
- * @param $replyto String: optional reply-to email (default: null).
+ * Collection of static functions for sending mail
*/
-function userMailer( $to, $from, $subject, $body, $replyto=null ) {
- global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal;
- global $wgEnotifMaxRecips;
+class UserMailer {
+ /**
+ * Send mail using a PEAR mailer
+ */
+ protected static function sendWithPear($mailer, $dest, $headers, $body)
+ {
+ $mailResult = $mailer->send($dest, $headers, $body);
+
+ # Based on the result return an error string,
+ if( PEAR::isError( $mailResult ) ) {
+ wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
+ return new WikiError( $mailResult->getMessage() );
+ } else {
+ return true;
+ }
+ }
- if (is_array( $wgSMTP )) {
- require_once( 'Mail.php' );
+ /**
+ * This function will perform a direct (authenticated) login to
+ * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
+ * array of parameters. It requires PEAR:Mail to do that.
+ * Otherwise it just uses the standard PHP 'mail' function.
+ *
+ * @param $to MailAddress: recipient's email
+ * @param $from MailAddress: sender's email
+ * @param $subject String: email's subject.
+ * @param $body String: email's text.
+ * @param $replyto String: optional reply-to email (default: null).
+ * @return mixed True on success, a WikiError object on failure.
+ */
+ static function send( $to, $from, $subject, $body, $replyto=null ) {
+ global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal;
+ global $wgEnotifMaxRecips;
- $msgid = str_replace(" ", "_", microtime());
- if (function_exists('posix_getpid'))
- $msgid .= '.' . posix_getpid();
+ if ( is_array( $to ) ) {
+ wfDebug( __METHOD__.': sending mail to ' . implode( ',', $to ) . "\n" );
+ } else {
+ wfDebug( __METHOD__.': sending mail to ' . implode( ',', array( $to->toString() ) ) . "\n" );
+ }
- if (is_array($to)) {
- $dest = array();
- foreach ($to as $u)
- $dest[] = $u->address;
- } else
- $dest = $to->address;
+ if (is_array( $wgSMTP )) {
+ require_once( 'Mail.php' );
- $headers['From'] = $from->toString();
+ $msgid = str_replace(" ", "_", microtime());
+ if (function_exists('posix_getpid'))
+ $msgid .= '.' . posix_getpid();
- if ($wgEnotifImpersonal)
- $headers['To'] = 'undisclosed-recipients:;';
- else
- $headers['To'] = $to->toString();
+ if (is_array($to)) {
+ $dest = array();
+ foreach ($to as $u)
+ $dest[] = $u->address;
+ } else
+ $dest = $to->address;
- if ( $replyto ) {
- $headers['Reply-To'] = $replyto->toString();
- }
- $headers['Subject'] = wfQuotedPrintable( $subject );
- $headers['Date'] = date( 'r' );
- $headers['MIME-Version'] = '1.0';
- $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding;
- $headers['Content-transfer-encoding'] = '8bit';
- $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME
- $headers['X-Mailer'] = 'MediaWiki mailer';
-
- // Create the mail object using the Mail::factory method
- $mail_object =& Mail::factory('smtp', $wgSMTP);
- if( PEAR::isError( $mail_object ) ) {
- wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" );
- return $mail_object->getMessage();
- }
+ $headers['From'] = $from->toString();
+
+ if ($wgEnotifImpersonal)
+ $headers['To'] = 'undisclosed-recipients:;';
+ else
+ $headers['To'] = $to->toString();
+
+ if ( $replyto ) {
+ $headers['Reply-To'] = $replyto->toString();
+ }
+ $headers['Subject'] = wfQuotedPrintable( $subject );
+ $headers['Date'] = date( 'r' );
+ $headers['MIME-Version'] = '1.0';
+ $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding;
+ $headers['Content-transfer-encoding'] = '8bit';
+ $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME
+ $headers['X-Mailer'] = 'MediaWiki mailer';
+
+ // Create the mail object using the Mail::factory method
+ $mail_object =& Mail::factory('smtp', $wgSMTP);
+ if( PEAR::isError( $mail_object ) ) {
+ wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" );
+ return new WikiError( $mail_object->getMessage() );
+ }
- wfDebug( "Sending mail via PEAR::Mail to $dest\n" );
- if (is_array($dest)) {
- $chunks = array_chunk($dest, $wgEnotifMaxRecips);
+ wfDebug( "Sending mail via PEAR::Mail to $dest\n" );
+ $chunks = array_chunk( (array)$dest, $wgEnotifMaxRecips );
foreach ($chunks as $chunk) {
- $e = send_mail($mail_object, $chunk, $headers, $body);
- if ($e != '')
+ $e = self::sendWithPear($mail_object, $chunk, $headers, $body);
+ if( WikiError::isError( $e ) )
return $e;
}
- } else
- return $mail_object->send($dest, $headers, $body);
-
- } else {
- # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently
- # (fifth parameter of the PHP mail function, see some lines below)
-
- # Line endings need to be different on Unix and Windows due to
- # the bug described at http://trac.wordpress.org/ticket/2603
- if ( wfIsWindows() ) {
- $body = str_replace( "\n", "\r\n", $body );
- $endl = "\r\n";
- } else {
- $endl = "\n";
- }
- $headers =
- "MIME-Version: 1.0$endl" .
- "Content-type: text/plain; charset={$wgOutputEncoding}$endl" .
- "Content-Transfer-Encoding: 8bit$endl" .
- "X-Mailer: MediaWiki mailer$endl".
- 'From: ' . $from->toString();
- if ($replyto) {
- $headers .= "{$endl}Reply-To: " . $replyto->toString();
- }
-
- $wgErrorString = '';
- set_error_handler( 'mailErrorHandler' );
- wfDebug( "Sending mail via internal mail() function\n" );
+ } else {
+ # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently
+ # (fifth parameter of the PHP mail function, see some lines below)
+
+ # Line endings need to be different on Unix and Windows due to
+ # the bug described at http://trac.wordpress.org/ticket/2603
+ if ( wfIsWindows() ) {
+ $body = str_replace( "\n", "\r\n", $body );
+ $endl = "\r\n";
+ } else {
+ $endl = "\n";
+ }
+ $headers =
+ "MIME-Version: 1.0$endl" .
+ "Content-type: text/plain; charset={$wgOutputEncoding}$endl" .
+ "Content-Transfer-Encoding: 8bit$endl" .
+ "X-Mailer: MediaWiki mailer$endl".
+ 'From: ' . $from->toString();
+ if ($replyto) {
+ $headers .= "{$endl}Reply-To: " . $replyto->toString();
+ }
- if (function_exists('mail'))
- if (is_array($to))
- foreach ($to as $recip)
- $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers );
- else
- $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers );
- else
- $wgErrorString = 'PHP is not configured to send mail';
+ $wgErrorString = '';
+ $html_errors = ini_get( 'html_errors' );
+ ini_set( 'html_errors', '0' );
+ set_error_handler( array( 'UserMailer', 'errorHandler' ) );
+ wfDebug( "Sending mail via internal mail() function\n" );
+ if (function_exists('mail')) {
+ if (is_array($to)) {
+ foreach ($to as $recip) {
+ $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers );
+ }
+ } else {
+ $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers );
+ }
+ } else {
+ $wgErrorString = 'PHP is not configured to send mail';
+ }
- restore_error_handler();
+ restore_error_handler();
+ ini_set( 'html_errors', $html_errors );
- if ( $wgErrorString ) {
- wfDebug( "Error sending mail: $wgErrorString\n" );
- return $wgErrorString;
- } elseif (! $sent) {
- //mail function only tells if there's an error
- wfDebug( "Error sending mail\n" );
- return 'mailer error';
- } else {
- return '';
+ if ( $wgErrorString ) {
+ wfDebug( "Error sending mail: $wgErrorString\n" );
+ return new WikiError( $wgErrorString );
+ } elseif (! $sent) {
+ //mail function only tells if there's an error
+ wfDebug( "Error sending mail\n" );
+ return new WikiError( 'mailer error' );
+ } else {
+ return true;
+ }
}
}
-}
-
+ /**
+ * Get the mail error message in global $wgErrorString
+ *
+ * @param $code Integer: error number
+ * @param $string String: error message
+ */
+ static function errorHandler( $code, $string ) {
+ global $wgErrorString;
+ $wgErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string );
+ }
-/**
- * Get the mail error message in global $wgErrorString
- *
- * @param $code Integer: error number
- * @param $string String: error message
- */
-function mailErrorHandler( $code, $string ) {
- global $wgErrorString;
- $wgErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string );
+ /**
+ * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name
+ */
+ static function rfc822Phrase( $phrase ) {
+ $phrase = strtr( $phrase, array( "\r" => '', "\n" => '', '"' => '' ) );
+ return '"' . $phrase . '"';
+ }
}
-
/**
* This module processes the email notifications when the current page is
* changed. It looks up the table watchlist to find out which users are watching
@@ -245,10 +258,24 @@ class EmailNotification {
*/
var $to, $subject, $body, $replyto, $from;
var $user, $title, $timestamp, $summary, $minorEdit, $oldid;
+ var $mailTargets = array();
/**@}}*/
- function notifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid = false) {
+ /**
+ * Send emails corresponding to the user $editor editing the page $title.
+ * Also updates wl_notificationtimestamp.
+ *
+ * May be deferred via the job queue.
+ *
+ * @param $editor User object
+ * @param $title Title object
+ * @param $timestamp
+ * @param $summary
+ * @param $minorEdit
+ * @param $oldid (default: false)
+ */
+ function notifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid = false) {
global $wgEnotifUseJobQ;
if( $title->getNamespace() < 0 )
@@ -269,23 +296,27 @@ class EmailNotification {
}
- /**
- * @todo document
+ /*
+ * Immediate version of notifyOnPageChange().
+ *
+ * Send emails corresponding to the user $editor editing the page $title.
+ * Also updates wl_notificationtimestamp.
+ *
+ * @param $editor User object
* @param $title Title object
* @param $timestamp
* @param $summary
* @param $minorEdit
* @param $oldid (default: false)
*/
- function actuallyNotifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid=false) {
+ function actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid=false) {
# we use $wgEmergencyContact as sender's address
global $wgEnotifWatchlist;
global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker;
global $wgEnotifImpersonal;
- $fname = 'UserMailer::notifyOnPageChange';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# The following code is only run, if several conditions are met:
# 1. EmailNotification for pages (other than user_talk pages) must be enabled
@@ -295,37 +326,40 @@ class EmailNotification {
$enotifusertalkpage = ($isUserTalkPage && $wgEnotifUserTalk);
$enotifwatchlistpage = $wgEnotifWatchlist;
- $this->title =& $title;
+ $this->title = $title;
$this->timestamp = $timestamp;
$this->summary = $summary;
$this->minorEdit = $minorEdit;
$this->oldid = $oldid;
$this->composeCommonMailtext($editor);
- $impersonals = array();
+ $userTalkId = false;
if ( (!$minorEdit || $wgEnotifMinorEdits) ) {
- if( $wgEnotifWatchlist ) {
- // Send updates to watchers other than the current editor
- $userCondition = 'wl_user <> ' . intval( $editor->getId() );
- } elseif( $wgEnotifUserTalk && $title->getNamespace() == NS_USER_TALK ) {
+ if ( $wgEnotifUserTalk && $isUserTalkPage ) {
$targetUser = User::newFromName( $title->getText() );
- if( is_null( $targetUser ) ) {
- wfDebug( "$fname: user-talk-only mode; no such user\n" );
- $userCondition = false;
- } elseif( $targetUser->getId() == $editor->getId() ) {
- wfDebug( "$fname: user-talk-only mode; editor is target user\n" );
- $userCondition = false;
+ if ( !$targetUser || $targetUser->isAnon() ) {
+ wfDebug( __METHOD__.": user talk page edited, but user does not exist\n" );
+ } elseif ( $targetUser->getId() == $editor->getId() ) {
+ wfDebug( __METHOD__.": user edited their own talk page, no notification sent\n" );
+ } elseif( $targetUser->getOption( 'enotifusertalkpages' ) ) {
+ wfDebug( __METHOD__.": sending talk page update notification\n" );
+ $this->compose( $targetUser );
+ $userTalkId = $targetUser->getId();
} else {
- // Don't notify anyone other than the owner of the talk page
- $userCondition = 'wl_user = ' . intval( $targetUser->getId() );
+ wfDebug( __METHOD__.": talk page owner doesn't want notifications\n" );
}
- } else {
- // Notifications disabled
- $userCondition = false;
}
- if( $userCondition ) {
- $dbr = wfGetDB( DB_MASTER );
+
+
+ if ( $wgEnotifWatchlist ) {
+ // Send updates to watchers other than the current editor
+ $userCondition = 'wl_user <> ' . intval( $editor->getId() );
+ if ( $userTalkId !== false ) {
+ // Already sent an email to this person
+ $userCondition .= ' AND wl_user <> ' . intval( $userTalkId );
+ }
+ $dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'watchlist', array( 'wl_user' ),
array(
@@ -333,67 +367,44 @@ class EmailNotification {
'wl_namespace' => $title->getNamespace(),
$userCondition,
'wl_notificationtimestamp IS NULL',
- ), $fname );
-
- # if anyone is watching ... set up the email message text which is
- # common for all receipients ...
- if ( $dbr->numRows( $res ) > 0 ) {
-
- $watchingUser = new User();
-
- # ... now do for all watching users ... if the options fit
- for ($i = 1; $i <= $dbr->numRows( $res ); $i++) {
-
- $wuser = $dbr->fetchObject( $res );
- $watchingUser->setID($wuser->wl_user);
-
- if ( ( $enotifwatchlistpage && $watchingUser->getOption('enotifwatchlistpages') ) ||
- ( $enotifusertalkpage
- && $watchingUser->getOption('enotifusertalkpages')
- && $title->equals( $watchingUser->getTalkPage() ) )
- && (!$minorEdit || ($wgEnotifMinorEdits && $watchingUser->getOption('enotifminoredits') ) )
- && ($watchingUser->isEmailConfirmed() ) ) {
- # ... adjust remaining text and page edit time placeholders
- # which needs to be personalized for each user
- if ($wgEnotifImpersonal)
- $impersonals[] = $watchingUser;
- else
- $this->composeAndSendPersonalisedMail( $watchingUser );
-
- } # if the watching user has an email address in the preferences
+ ), __METHOD__ );
+
+ foreach ( $res as $row ) {
+ $watchingUser = User::newFromId( $row->wl_user );
+ if ( $watchingUser->getOption( 'enotifwatchlistpages' ) &&
+ ( !$minorEdit || $watchingUser->getOption('enotifminoredits') ) &&
+ $watchingUser->isEmailConfirmed() )
+ {
+ $this->compose( $watchingUser );
}
}
- } # if anyone is watching
- } # if $wgEnotifWatchlist = true
+ }
+ }
global $wgUsersNotifedOnAllChanges;
foreach ( $wgUsersNotifedOnAllChanges as $name ) {
$user = User::newFromName( $name );
- if ($wgEnotifImpersonal)
- $impersonals[] = $user;
- else
- $this->composeAndSendPersonalisedMail( $user );
+ $this->compose( $user );
}
- $this->composeAndSendImpersonalMail($impersonals);
+ $this->sendMails();
if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) {
# mark the changed watch-listed page with a timestamp, so that the page is
# listed with an "updated since your last visit" icon in the watch list, ...
$dbw = wfGetDB( DB_MASTER );
- $success = $dbw->update( 'watchlist',
+ $dbw->update( 'watchlist',
array( /* SET */
'wl_notificationtimestamp' => $dbw->timestamp($timestamp)
), array( /* WHERE */
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
'wl_notificationtimestamp IS NULL'
- ), 'UserMailer::NotifyOnChange'
+ ), __METHOD__
);
- # FIXME what do we do on failure ?
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
} # function NotifyOnChange
/**
@@ -495,6 +506,31 @@ class EmailNotification {
}
/**
+ * Compose a mail to a given user and either queue it for sending, or send it now,
+ * depending on settings.
+ *
+ * Call sendMails() to send any mails that were queued.
+ */
+ function compose( $user ) {
+ global $wgEnotifImpersonal;
+ if ( $wgEnotifImpersonal ) {
+ $this->mailTargets[] = new MailAddress( $user );
+ } else {
+ $this->sendPersonalised( $user );
+ }
+ }
+
+ /**
+ * Send any queued mails
+ */
+ function sendMails() {
+ global $wgEnotifImpersonal;
+ if ( $wgEnotifImpersonal ) {
+ $this->sendImpersonal( $this->mailTargets );
+ }
+ }
+
+ /**
* Does the per-user customizations to a notification e-mail (name,
* timestamp in proper timezone, etc) and sends it out.
* Returns true if the mail was sent successfully.
@@ -504,7 +540,7 @@ class EmailNotification {
* @return bool
* @private
*/
- function composeAndSendPersonalisedMail( $watchingUser ) {
+ function sendPersonalised( $watchingUser ) {
global $wgLang;
// From the PHP manual:
// Note: The to parameter cannot be an address in the form of "Something <someone@example.com>".
@@ -521,23 +557,19 @@ class EmailNotification {
$wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ),
$body);
- return userMailer($to, $this->from, $this->subject, $body, $this->replyto);
+ return UserMailer::send($to, $this->from, $this->subject, $body, $this->replyto);
}
/**
- * Same as composeAndSendPersonalisedMail but does impersonal mail
- * suitable for bulk mailing. Takes an array of users.
+ * Same as sendPersonalised but does impersonal mail suitable for bulk
+ * mailing. Takes an array of MailAddress objects.
*/
- function composeAndSendImpersonalMail($users) {
+ function sendImpersonal( $addresses ) {
global $wgLang;
- if (empty($users))
+ if (empty($addresses))
return;
- $to = array();
- foreach ($users as $user)
- $to[] = new MailAddress($user);
-
$body = str_replace(
array( '$WATCHINGUSERNAME',
'$PAGEEDITDATE'),
@@ -545,8 +577,17 @@ class EmailNotification {
$wgLang->timeanddate($this->timestamp, true, false, false)),
$this->body);
- return userMailer($to, $this->from, $this->subject, $body, $this->replyto);
+ return UserMailer::send($addresses, $this->from, $this->subject, $body, $this->replyto);
}
} # end of class EmailNotification
+/**
+ * Backwards compatibility functions
+ */
+function wfRFC822Phrase( $s ) {
+ return UserMailer::rfc822Phrase( $s );
+}
+function userMailer( $to, $from, $subject, $body, $replyto=null ) {
+ return UserMailer::send( $to, $from, $subject, $body, $replyto );
+}
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
new file mode 100644
index 00000000..de0e770c
--- /dev/null
+++ b/includes/UserRightsProxy.php
@@ -0,0 +1,161 @@
+<?php
+
+
+/**
+ * Cut-down copy of User interface for local-interwiki-database
+ * user rights manipulation.
+ */
+class UserRightsProxy {
+ private function __construct( $db, $database, $name, $id ) {
+ $this->db = $db;
+ $this->database = $database;
+ $this->name = $name;
+ $this->id = intval( $id );
+ }
+
+ /**
+ * Confirm the selected database name is a valid local interwiki database name.
+ * @return bool
+ */
+ public static function validDatabase( $database ) {
+ global $wgLocalDatabases;
+ return in_array( $database, $wgLocalDatabases );
+ }
+
+ public static function whoIs( $database, $id ) {
+ $user = self::newFromId( $database, $id );
+ if( $user ) {
+ return $user->name;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Factory function; get a remote user entry by ID number.
+ * @return UserRightsProxy or null if doesn't exist
+ */
+ public static function newFromId( $database, $id ) {
+ return self::newFromLookup( $database, 'user_id', intval( $id ) );
+ }
+
+ public static function newFromName( $database, $name ) {
+ return self::newFromLookup( $database, 'user_name', $name );
+ }
+
+ private static function newFromLookup( $database, $field, $value ) {
+ $db = self::getDB( $database );
+ if( $db ) {
+ $row = $db->selectRow( 'user',
+ array( 'user_id', 'user_name' ),
+ array( $field => $value ),
+ __METHOD__ );
+ if( $row !== false ) {
+ return new UserRightsProxy( $db, $database,
+ $row->user_name,
+ intval( $row->user_id ) );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Open a database connection to work on for the requested user.
+ * This may be a new connection to another database for remote users.
+ * @param string $database
+ * @return Database or null if invalid selection
+ */
+ private static function getDB( $database ) {
+ global $wgLocalDatabases, $wgDBname;
+ if( self::validDatabase( $database ) ) {
+ if( $database == $wgDBname ) {
+ // Hmm... this shouldn't happen though. :)
+ return wfGetDB( DB_MASTER );
+ } else {
+ global $wgDBuser, $wgDBpassword;
+ $server = self::getMaster( $database );
+ return new Database( $server, $wgDBuser, $wgDBpassword, $database );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the master server to connect to for the requested database.
+ */
+ private static function getMaster( $database ) {
+ global $wgDBserver, $wgAlternateMaster;
+ if( isset( $wgAlternateMaster[$database] ) ) {
+ return $wgAlternateMaster[$database];
+ }
+ return $wgDBserver;
+ }
+
+ public function getId() {
+ return $this->id;
+ }
+
+ public function isAnon() {
+ return $this->getId() == 0;
+ }
+
+ public function getName() {
+ return $this->name . '@' . $this->database;
+ }
+
+ public function getUserPage() {
+ return Title::makeTitle( NS_USER, $this->getName() );
+ }
+
+ // Replaces getUserGroups()
+ function getGroups() {
+ $res = $this->db->select( 'user_groups',
+ array( 'ug_group' ),
+ array( 'ug_user' => $this->id ),
+ __METHOD__ );
+ $groups = array();
+ while( $row = $this->db->fetchObject( $res ) ) {
+ $groups[] = $row->ug_group;
+ }
+ return $groups;
+ }
+
+ // replaces addUserGroup
+ function addGroup( $group ) {
+ $this->db->insert( 'user_groups',
+ array(
+ 'ug_user' => $this->id,
+ 'ug_group' => $group,
+ ),
+ __METHOD__,
+ array( 'IGNORE' ) );
+ }
+
+ // replaces removeUserGroup
+ function removeGroup( $group ) {
+ $this->db->delete( 'user_groups',
+ array(
+ 'ug_user' => $this->id,
+ 'ug_group' => $group,
+ ),
+ __METHOD__ );
+ }
+
+ // replaces touchUser
+ function invalidateCache() {
+ $this->db->update( 'user',
+ array( 'user_touched' => $this->db->timestamp() ),
+ array( 'user_id' => $this->id ),
+ __METHOD__ );
+
+ global $wgMemc;
+ if ( function_exists( 'wfForeignMemcKey' ) ) {
+ $key = wfForeignMemcKey( $this->database, false, 'user', 'id', $this->id );
+ } else {
+ $key = "$this->database:user:id:" . $this->id;
+ }
+ $wgMemc->delete( $key );
+ }
+}
+
+?> \ No newline at end of file
diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php
index e03225a3..7e37dca7 100644
--- a/includes/WatchlistEditor.php
+++ b/includes/WatchlistEditor.php
@@ -32,15 +32,8 @@ class WatchlistEditor {
}
switch( $mode ) {
case self::EDIT_CLEAR:
- $output->setPageTitle( wfMsg( 'watchlistedit-clear-title' ) );
- if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
- $this->clearWatchlist( $user );
- $user->invalidateCache();
- $output->addHtml( wfMsgExt( 'watchlistedit-clear-done', 'parse' ) );
- } else {
- $this->showClearForm( $output, $user );
- }
- break;
+ // The "Clear" link scared people too much.
+ // Pass on to the raw editor, from which it's very easy to clear.
case self::EDIT_RAW:
$output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) );
if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
@@ -333,27 +326,6 @@ class WatchlistEditor {
}
}
}
-
- /**
- * Show a confirmation form for users wishing to clear their watchlist
- *
- * @param OutputPage $output
- * @param User $user
- */
- private function showClearForm( $output, $user ) {
- global $wgUser;
- if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
- $self = SpecialPage::getTitleFor( 'Watchlist' );
- $form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $self->getLocalUrl( 'action=clear' ) ) );
- $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
- $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-clear-legend' ) . '</legend>';
- $form .= wfMsgExt( 'watchlistedit-clear-confirm', 'parse' );
- $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-clear-submit' ) ) . '</p>';
- $form .= '</fieldset></form>';
- $output->addHtml( $form );
- }
- }
/**
* Show the standard watchlist editing form
@@ -481,11 +453,9 @@ class WatchlistEditor {
*/
public static function buildTools( $skin ) {
$tools = array();
- $self = SpecialPage::getTitleFor( 'Watchlist' );
- $modes = array( 'view' => '', 'edit' => 'edit', 'raw' => 'raw', 'clear' => 'clear' );
- foreach( $modes as $mode => $action ) {
- $action = $action ? "action={$action}" : '';
- $tools[] = $skin->makeKnownLinkObj( $self, wfMsgHtml( "watchlisttools-{$mode}" ), $action );
+ $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' );
+ foreach( $modes as $mode => $subpage ) {
+ $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) );
}
return implode( ' | ', $tools );
}
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index aa9885f0..944be3c9 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -42,8 +42,17 @@ if ( !function_exists( '__autoload' ) ) {
*
*/
class WebRequest {
+ var $data = array();
+
function __construct() {
+ /// @fixme This preemptive de-quoting can interfere with other web libraries
+ /// and increases our memory footprint. It would be cleaner to do on
+ /// demand; but currently we have no wrapper for $_SERVER etc.
$this->checkMagicQuotes();
+
+ // POST overrides GET data
+ // We don't use $_REQUEST here to avoid interference from cookies...
+ $this->data = wfArrayMerge( $_GET, $_POST );
}
/**
@@ -70,11 +79,22 @@ class WebRequest {
if( $a ) {
$path = $a['path'];
+ global $wgScript;
+ if( $path == $wgScript ) {
+ // Script inside a rewrite path?
+ // Abort to keep from breaking...
+ return;
+ }
+ // Raw PATH_INFO style
+ $matches = $this->extractTitle( $path, "$wgScript/$1" );
+
global $wgArticlePath;
- $matches = $this->extractTitle( $path, $wgArticlePath );
+ if( !$matches && $wgArticlePath ) {
+ $matches = $this->extractTitle( $path, $wgArticlePath );
+ }
global $wgActionPaths;
- if( !$matches && $wgActionPaths) {
+ if( !$matches && $wgActionPaths ) {
$matches = $this->extractTitle( $path, $wgActionPaths, 'action' );
}
@@ -99,7 +119,7 @@ class WebRequest {
$matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
}
foreach( $matches as $key => $val) {
- $_GET[$key] = $_REQUEST[$key] = $val;
+ $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
}
}
}
@@ -225,7 +245,7 @@ class WebRequest {
* @return string
*/
function getVal( $name, $default = NULL ) {
- $val = $this->getGPCVal( $_REQUEST, $name, $default );
+ $val = $this->getGPCVal( $this->data, $name, $default );
if( is_array( $val ) ) {
$val = $default;
}
@@ -246,7 +266,7 @@ class WebRequest {
* @return array
*/
function getArray( $name, $default = NULL ) {
- $val = $this->getGPCVal( $_REQUEST, $name, $default );
+ $val = $this->getGPCVal( $this->data, $name, $default );
if( is_null( $val ) ) {
return null;
} else {
@@ -351,7 +371,7 @@ class WebRequest {
function getValues() {
$names = func_get_args();
if ( count( $names ) == 0 ) {
- $names = array_keys( $_REQUEST );
+ $names = array_keys( $this->data );
}
$retVal = array();
@@ -576,9 +596,13 @@ class WebRequest {
*
*/
class FauxRequest extends WebRequest {
- var $data = null;
var $wasPosted = false;
+ /**
+ * @param array $data Array of *non*-urlencoded key => value pairs, the
+ * fake GET/POST values
+ * @param bool $wasPosted Whether to treat the data as POST
+ */
function FauxRequest( $data, $wasPosted = false ) {
if( is_array( $data ) ) {
$this->data = $data;
@@ -588,13 +612,9 @@ class FauxRequest extends WebRequest {
$this->wasPosted = $wasPosted;
}
- function getVal( $name, $default = NULL ) {
- return $this->getGPCVal( $this->data, $name, $default );
- }
-
function getText( $name, $default = '' ) {
# Override; don't recode since we're using internal data
- return $this->getVal( $name, $default );
+ return (string)$this->getVal( $name, $default );
}
function getValues() {
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 72a6a61d..e0a57445 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -57,18 +57,10 @@ class MediaWiki {
}
function checkMaxLag( $maxLag ) {
- global $wgLoadBalancer, $wgShowHostnames;
+ global $wgLoadBalancer;
list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
if ( $lag > $maxLag ) {
- header( 'HTTP/1.1 503 Service Unavailable' );
- header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
- header( 'X-Database-Lag: ' . intval( $lag ) );
- header( 'Content-Type: text/plain' );
- if( $wgShowHostnames ) {
- echo "Waiting for $host: $lag seconds lagged\n";
- } else {
- echo "Waiting for a database server: $lag seconds lagged\n";
- }
+ wfMaxlagError( $host, $lag, $maxLag );
return false;
} else {
return true;
@@ -228,6 +220,10 @@ class MediaWiki {
switch( $title->getNamespace() ) {
case NS_IMAGE:
+ $file = wfFindFile( $title );
+ if( $file && $file->getRedirected() ) {
+ return new Article( $title );
+ }
return new ImagePage( $title );
case NS_CATEGORY:
return new CategoryPage( $title );
@@ -252,8 +248,9 @@ class MediaWiki {
$article = $this->articleFromTitle( $title );
// Namespace might change when using redirects
- if( $action == 'view' && !$request->getVal( 'oldid' ) &&
- $request->getVal( 'redirect' ) != 'no' ) {
+ if( ( $action == 'view' || $action == 'render' ) && !$request->getVal( 'oldid' ) &&
+ $request->getVal( 'redirect' ) != 'no' &&
+ !( $wgTitle->getNamespace() == NS_IMAGE && wfFindFile( $wgTitle->getText() ) ) ) {
$dbr = wfGetDB(DB_SLAVE);
$article->loadPageData($article->pageDataFromTitle($dbr, $title));
@@ -297,7 +294,7 @@ class MediaWiki {
$this->doJobs();
$loadBalancer->saveMasterPos();
# Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
- $loadBalancer->commitAll();
+ $loadBalancer->commitMasterChanges();
$output->output();
wfProfileOut( 'MediaWiki::finalCleanup' );
}
@@ -309,6 +306,12 @@ class MediaWiki {
*/
function doUpdates ( &$updates ) {
wfProfileIn( 'MediaWiki::doUpdates' );
+ /* No need to get master connections in case of empty updates array */
+ if (!$updates) {
+ wfProfileOut('MediaWiki::doUpdates');
+ return;
+ }
+
$dbw = wfGetDB( DB_MASTER );
foreach( $updates as $up ) {
$up->doUpdate();
@@ -360,7 +363,6 @@ class MediaWiki {
*/
function restInPeace ( &$loadBalancer ) {
wfLogProfilingData();
- $loadBalancer->closeAll();
wfDebug( "Request ended normally\n" );
}
@@ -371,6 +373,11 @@ class MediaWiki {
wfProfileIn( 'MediaWiki::performAction' );
+ if ( !wfRunHooks('MediaWikiPerformAction', array($output, $article, $title, $user, $request)) ) {
+ wfProfileOut( 'MediaWiki::performAction' );
+ return;
+ }
+
$action = $this->getVal('Action');
if( in_array( $action, $this->getVal('DisabledActions',array()) ) ) {
/* No such action; this will switch to the default case */
diff --git a/includes/WikiError.php b/includes/WikiError.php
index efb645bb..b155f9bf 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -101,12 +101,12 @@ class WikiXmlError extends WikiError {
/** @return string */
function getMessage() {
- return sprintf( '%s at line %d, col %d (byte %d%s): %s',
+ // '$1 at line $2, col $3 (byte $4): $5',
+ return wfMsgHtml( 'xml-error-string',
$this->mMessage,
$this->mLine,
$this->mColumn,
- $this->mByte,
- $this->mContext,
+ $this->mByte . $this->mContext,
xml_error_string( $this->mXmlError ) );
}
@@ -120,5 +120,3 @@ class WikiXmlError extends WikiError {
}
}
}
-
-
diff --git a/includes/Xml.php b/includes/Xml.php
index fe4bb0cd..6689a4a4 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -104,6 +104,14 @@ class Xml {
$namespaces = $wgContLang->getFormattedNamespaces();
$options = array();
+ // Godawful hack... we'll be frequently passed selected namespaces
+ // as strings since PHP is such a shithole.
+ // But we also don't want blanks and nulls and "all"s matching 0,
+ // so let's convert *just* string ints to clean ints.
+ if( preg_match( '/^\d+$/', $selected ) ) {
+ $selected = intval( $selected );
+ }
+
if( !is_null( $all ) )
$namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces;
foreach( $namespaces as $index => $name ) {
@@ -301,7 +309,7 @@ class Xml {
'type' => 'hidden',
'value' => $value ) + $attribs );
}
-
+
/**
* Convenience function to build an HTML drop-down list item.
* @param $text String: text for this item
@@ -322,6 +330,63 @@ class Xml {
}
/**
+ * Build a drop-down box from a textual list.
+ *
+ * @param mixed $name Name and id for the drop-down
+ * @param mixed $class CSS classes for the drop-down
+ * @param mixed $other Text for the "Other reasons" option
+ * @param mixed $list Correctly formatted text to be used to generate the options
+ * @param mixed $selected Option which should be pre-selected
+ * @return string
+ */
+ public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = Null ) {
+ $options = '';
+ $optgroup = false;
+
+ $options = self::option( $other, 'other', $selected === 'other' );
+
+ foreach ( explode( "\n", $list ) as $option) {
+ $value = trim( $option );
+ if ( $value == '' ) {
+ continue;
+ } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
+ // A new group is starting ...
+ $value = trim( substr( $value, 1 ) );
+ if( $optgroup ) $options .= self::closeElement('optgroup');
+ $options .= self::openElement( 'optgroup', array( 'label' => $value ) );
+ $optgroup = true;
+ } elseif ( substr( $value, 0, 2) == '**' ) {
+ // groupmember
+ $value = trim( substr( $value, 2 ) );
+ $options .= self::option( $value, $value, $selected === $value );
+ } else {
+ // groupless reason list
+ if( $optgroup ) $options .= self::closeElement('optgroup');
+ $options .= self::option( $value, $value, $selected === $value );
+ $optgroup = false;
+ }
+ }
+ if( $optgroup ) $options .= self::closeElement('optgroup');
+
+ $attribs = array();
+ if( $name ) {
+ $attribs['id'] = $name;
+ $attribs['name'] = $name;
+ }
+ if( $class ) {
+ $attribs['class'] = $class;
+ }
+ if( $tabindex ) {
+ $attribs['tabindex'] = $tabindex;
+ }
+ return Xml::openElement( 'select', $attribs )
+ . "\n"
+ . $options
+ . "\n"
+ . Xml::closeElement( 'select' );
+ }
+
+ /**
* Returns an escaped string suitable for inclusion in a string literal
* for JavaScript source code.
* Illegal control characters are assumed not to be present.
diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php
new file mode 100644
index 00000000..639d1f85
--- /dev/null
+++ b/includes/XmlTypeCheck.php
@@ -0,0 +1,93 @@
+<?php
+
+class XmlTypeCheck {
+ /**
+ * Will be set to true or false to indicate whether the file is
+ * well-formed XML. Note that this doesn't check schema validity.
+ */
+ public $wellFormed = false;
+
+ /**
+ * Name of the document's root element, including any namespace
+ * as an expanded URL.
+ */
+ public $rootElement = '';
+
+ private $softNamespaces;
+ private $namespaces = array();
+
+ /**
+ * @param $file string filename
+ * @param $softNamespaces bool
+ * If set to true, use of undeclared XML namespaces will be ignored.
+ * This matches the behavior of rsvg, but more compliant consumers
+ * such as Firefox will reject such files.
+ * Leave off for the default, stricter checks.
+ */
+ function __construct( $file, $softNamespaces=false ) {
+ $this->softNamespaces = $softNamespaces;
+ $this->run( $file );
+ }
+
+ private function run( $fname ) {
+ if( $this->softNamespaces ) {
+ $parser = xml_parser_create( 'UTF-8' );
+ } else {
+ $parser = xml_parser_create_ns( 'UTF-8' );
+ }
+
+ // case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ xml_set_element_handler( $parser, array( $this, 'elementOpen' ), false );
+
+ $file = fopen( $fname, "rb" );
+ do {
+ $chunk = fread( $file, 32768 );
+ $ret = xml_parse( $parser, $chunk, feof( $file ) );
+ if( $ret == 0 ) {
+ // XML isn't well-formed!
+ fclose( $file );
+ xml_parser_free( $parser );
+ return;
+ }
+ } while( !feof( $file ) );
+
+ $this->wellFormed = true;
+
+ fclose( $file );
+ xml_parser_free( $parser );
+ }
+
+ private function elementOpen( $parser, $name, $attribs ) {
+ if( $this->softNamespaces ) {
+ // Check namespaces manually, so expat doesn't throw
+ // errors on use of undeclared namespaces.
+ foreach( $attribs as $attrib => $val ) {
+ if( $attrib == 'xmlns' ) {
+ $this->namespaces[''] = $val;
+ } elseif( substr( $attrib, 0, strlen( 'xmlns:' ) ) == 'xmlns:' ) {
+ $this->namespaces[substr( $attrib, strlen( 'xmlns:' ) )] = $val;
+ }
+ }
+
+ if( strpos( $name, ':' ) === false ) {
+ $ns = '';
+ $subname = $name;
+ } else {
+ list( $ns, $subname ) = explode( ':', $name, 2 );
+ }
+
+ if( isset( $this->namespaces[$ns] ) ) {
+ $name = $this->namespaces[$ns] . ':' . $subname;
+ } else {
+ // Technically this is invalid for XML with Namespaces.
+ // But..... we'll just let it slide in soft mode.
+ }
+ }
+
+ // We only need the first open element
+ $this->rootElement = $name;
+ xml_set_element_handler( $parser, false, false );
+ }
+}
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 1b9d884b..62bebb4e 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -1,13 +1,12 @@
<?php
/**
- * Simplified/Traditional Chinese conversion tables
+ * Simplified / Traditional Chinese conversion tables
*
* Automatically generated using code and data in includes/zhtable/
* Do not modify directly!
- *
-*/
+ */
-$zh2TW=array(
+$zh2Hant = array(
"画"=>"畫",
"板"=>"板",
"表"=>"表",
@@ -185,8 +184,10 @@ $zh2TW=array(
"鳄"=>"鱷",
"鸡"=>"雞",
"鹚"=>"鶿",
-"䌶"=>"䊷",
-"䜥"=>"𧩙",
+"荡"=>"盪",
+"锤"=>"錘",
+"㟆"=>"㠏",
+"㛟"=>"𡞵",
"专"=>"專",
"业"=>"業",
"丛"=>"叢",
@@ -263,6 +264,7 @@ $zh2TW=array(
"傧"=>"儐",
"储"=>"儲",
"傩"=>"儺",
+"㑩"=>"儸",
"兑"=>"兌",
"兖"=>"兗",
"兰"=>"蘭",
@@ -309,6 +311,8 @@ $zh2TW=array(
"剑"=>"劍",
"剥"=>"剝",
"剧"=>"劇",
+"㓥"=>"劏",
+"㔉"=>"劚",
"劝"=>"勸",
"办"=>"辦",
"务"=>"務",
@@ -409,6 +413,7 @@ $zh2TW=array(
"嘘"=>"噓",
"嘤"=>"嚶",
"嘱"=>"囑",
+"㖊"=>"噚",
"噜"=>"嚕",
"嚣"=>"囂",
"园"=>"園",
@@ -558,7 +563,6 @@ $zh2TW=array(
"帻"=>"幘",
"帼"=>"幗",
"幂"=>"冪",
-"幺"=>"么",
"庄"=>"莊",
"庆"=>"慶",
"庐"=>"廬",
@@ -824,6 +828,7 @@ $zh2TW=array(
"殚"=>"殫",
"殡"=>"殯",
"㱮"=>"殨",
+"㱩"=>"殰",
"殴"=>"毆",
"毁"=>"毀",
"毂"=>"轂",
@@ -926,6 +931,7 @@ $zh2TW=array(
"澜"=>"瀾",
"濑"=>"瀨",
"濒"=>"瀕",
+"㲿"=>"瀇",
"灏"=>"灝",
"灭"=>"滅",
"灯"=>"燈",
@@ -956,7 +962,9 @@ $zh2TW=array(
"焕"=>"煥",
"焖"=>"燜",
"焘"=>"燾",
+"㶽"=>"煱",
"煴"=>"熅",
+"㶶"=>"燶",
"爱"=>"愛",
"爷"=>"爺",
"牍"=>"牘",
@@ -987,6 +995,7 @@ $zh2TW=array(
"猬"=>"蝟",
"献"=>"獻",
"獭"=>"獺",
+"㺍"=>"獱",
"玑"=>"璣",
"玚"=>"瑒",
"玛"=>"瑪",
@@ -1101,6 +1110,7 @@ $zh2TW=array(
"秾"=>"穠",
"稆"=>"穭",
"税"=>"稅",
+"䅉"=>"稏",
"稣"=>"穌",
"稳"=>"穩",
"穑"=>"穡",
@@ -1157,8 +1167,11 @@ $zh2TW=array(
"䌷"=>"紬",
"䌹"=>"絅",
"絷"=>"縶",
+"䌼"=>"綐",
+"䌽"=>"綵",
"䌸"=>"縳",
"䍁"=>"繸",
+"䍀"=>"繿",
"纟"=>"糹",
"纠"=>"糾",
"纡"=>"紆",
@@ -1387,7 +1400,6 @@ $zh2TW=array(
"荞"=>"蕎",
"荟"=>"薈",
"荠"=>"薺",
-"荡"=>"蕩",
"荣"=>"榮",
"荤"=>"葷",
"荥"=>"滎",
@@ -1434,6 +1446,7 @@ $zh2TW=array(
"蕰"=>"薀",
"蕲"=>"蘄",
"薮"=>"藪",
+"䓕"=>"薳",
"藓"=>"蘚",
"蘖"=>"櫱",
"虏"=>"虜",
@@ -1467,6 +1480,7 @@ $zh2TW=array(
"蝾"=>"蠑",
"螀"=>"螿",
"螨"=>"蟎",
+"䗖"=>"螮",
"蟏"=>"蠨",
"衅"=>"釁",
"衔"=>"銜",
@@ -1488,6 +1502,7 @@ $zh2TW=array(
"裥"=>"襇",
"褛"=>"褸",
"褴"=>"襤",
+"䙓"=>"襬",
"见"=>"見",
"观"=>"觀",
"觃"=>"覎",
@@ -1512,6 +1527,7 @@ $zh2TW=array(
"䜣"=>"訢",
"誉"=>"譽",
"誊"=>"謄",
+"䜧"=>"譅",
"讠"=>"訁",
"计"=>"計",
"订"=>"訂",
@@ -1660,6 +1676,8 @@ $zh2TW=array(
"谵"=>"譫",
"谶"=>"讖",
"豮"=>"豶",
+"䝙"=>"貙",
+"䞐"=>"賰",
"贝"=>"貝",
"贞"=>"貞",
"负"=>"負",
@@ -1859,6 +1877,7 @@ $zh2TW=array(
"鉴"=>"鑒",
"銮"=>"鑾",
"錾"=>"鏨",
+"𨱏"=>"鎝",
"钅"=>"釒",
"钆"=>"釓",
"钇"=>"釔",
@@ -2013,7 +2032,6 @@ $zh2TW=array(
"锡"=>"錫",
"锢"=>"錮",
"锣"=>"鑼",
-"锤"=>"錘",
"锥"=>"錐",
"锦"=>"錦",
"锧"=>"鑕",
@@ -2174,6 +2192,7 @@ $zh2TW=array(
"靓"=>"靚",
"静"=>"靜",
"靥"=>"靨",
+"䩄"=>"靦",
"鞑"=>"韃",
"鞒"=>"鞽",
"鞯"=>"韉",
@@ -2298,6 +2317,7 @@ $zh2TW=array(
"馓"=>"饊",
"馔"=>"饌",
"馕"=>"饢",
+"䯄"=>"騧",
"马"=>"馬",
"驭"=>"馭",
"驮"=>"馱",
@@ -2471,6 +2491,7 @@ $zh2TW=array(
"䴗"=>"鶪",
"䴘"=>"鷈",
"䴙"=>"鷿",
+"㶉"=>"鸂",
"鸟"=>"鳥",
"鸠"=>"鳩",
"鸢"=>"鳶",
@@ -2557,7 +2578,6 @@ $zh2TW=array(
"鹾"=>"鹺",
"麦"=>"麥",
"麸"=>"麩",
-"麽"=>"麼",
"黄"=>"黃",
"黉"=>"黌",
"黡"=>"黶",
@@ -2590,7 +2610,6 @@ $zh2TW=array(
"龟"=>"龜",
"BIG-" => "BIG-",
-".PRG" => ".PRG",
"一伙" => "一伙",
"一并" => "一併",
"一准" => "一准",
@@ -2615,51 +2634,47 @@ $zh2TW=array(
"下面" => "下麵",
"不准" => "不准",
"不吊" => "不吊",
-"不干" => "不幹",
-"不舍" => "不捨",
+"不知就里" => "不知就裡",
"不知所云" => "不知所云",
-"不识台举" => "不識檯舉",
"不锈钢" => "不鏽鋼",
"丑剧" => "丑劇",
"丑旦" => "丑旦",
"丑角" => "丑角",
-"世界杯" => "世界盃",
"并存着" => "並存著",
"中岳" => "中嶽",
-"中台路" => "中臺路",
"中台医专" => "中臺醫專",
"丰南" => "丰南",
"丰台" => "丰台",
"丰姿" => "丰姿",
-"丰神俊朗" => "丰神俊朗",
"丰采" => "丰采",
"丰韵" => "丰韻",
"主干" => "主幹",
+"么么唱唱" => "么么唱唱",
+"么儿" => "么兒",
+"么喝" => "么喝",
+"么妹" => "么妹",
+"么弟" => "么弟",
+"么爷" => "么爺",
"九世之雠" => "九世之讎",
"九只" => "九隻",
"干丝" => "乾絲",
"干着急" => "乾著急",
-"干面" => "乾麵",
"乱发" => "亂髮",
"云云" => "云云",
-"云何" => "云何",
"云尔" => "云爾",
"五岳" => "五嶽",
"五斗柜" => "五斗櫃",
"五斗橱" => "五斗櫥",
-"五斗米" => "五斗米",
"五谷" => "五穀",
"五行生克" => "五行生剋",
"五只" => "五隻",
"五出" => "五齣",
-"井里" => "井裡",
"交卷" => "交卷",
"人云亦云" => "人云亦云",
"人物志" => "人物誌",
"什锦面" => "什錦麵",
"什么" => "什麼",
"仆倒" => "仆倒",
-"仇雠" => "仇讎",
"介系词" => "介係詞",
"介系词" => "介繫詞",
"仿制" => "仿製",
@@ -2677,7 +2692,6 @@ $zh2TW=array(
"布岗" => "佈崗",
"布施" => "佈施",
"布景" => "佈景",
-"布有" => "佈有",
"布满" => "佈滿",
"布线" => "佈線",
"布置" => "佈置",
@@ -2710,6 +2724,7 @@ $zh2TW=array(
"并进" => "併進",
"来复" => "來複",
"供制" => "供製",
+"依依不舍" => "依依不捨",
"侵并" => "侵併",
"便辟" => "便辟",
"系数" => "係數",
@@ -2720,9 +2735,7 @@ $zh2TW=array(
"修胡刀" => "修鬍刀",
"俯冲" => "俯衝",
"个里" => "個裡",
-"倒绷孩儿" => "倒繃孩兒",
"借着" => "借著",
-"偃仆" => "偃仆",
"假发" => "假髮",
"停制" => "停製",
"偷鸡不着" => "偷雞不著",
@@ -2732,6 +2745,7 @@ $zh2TW=array(
"传布" => "傳佈",
"债台高筑" => "債臺高築",
"傻里傻气" => "傻裡傻氣",
+"倾家荡产" => "傾家蕩產",
"倾复" => "傾複",
"倾复" => "傾覆",
"僱佣" => "僱佣",
@@ -2741,7 +2755,6 @@ $zh2TW=array(
"尽先" => "儘先",
"尽其所有" => "儘其所有",
"尽力" => "儘力",
-"尽可能" => "儘可能",
"尽快" => "儘快",
"尽早" => "儘早",
"尽是" => "儘是",
@@ -2763,29 +2776,21 @@ $zh2TW=array(
"公干" => "公幹",
"公斗" => "公斗",
"公历" => "公曆",
-"公里" => "公裡",
-"六谷" => "六穀",
"六只" => "六隻",
"六出" => "六齣",
"兼并" => "兼併",
-"册卷" => "冊卷",
"冤雠" => "冤讎",
"准予" => "准予",
"准假" => "准假",
-"准定" => "准定",
"准将" => "准將",
-"准尉" => "准尉",
-"准此" => "准此",
"准考证" => "准考證",
"准许" => "准許",
"几几" => "几几",
-"几杖" => "几杖",
"几案" => "几案",
-"几筵" => "几筵",
"几丝" => "几絲",
"凹洞里" => "凹洞裡",
"出征" => "出征",
-"函复" => "函覆",
+"出锤" => "出鎚",
"刀削面" => "刀削麵",
"刁斗" => "刁斗",
"分布" => "分佈",
@@ -2826,39 +2831,30 @@ $zh2TW=array(
"剃须" => "剃鬚",
"削发" => "削髮",
"克制" => "剋制",
-"克扣" => "剋扣",
-"克日" => "剋日",
"克星" => "剋星",
"克服" => "剋服",
-"克期" => "剋期",
"克死" => "剋死",
"克薄" => "剋薄",
-"前仆后仰" => "前仆後仰",
"前仆后继" => "前仆後繼",
"前台" => "前臺",
"前车之复" => "前車之覆",
"刚才" => "剛纔",
-"剥制" => "剝製",
"剪发" => "剪髮",
"割舍" => "割捨",
-"创获" => "創穫",
"创制" => "創製",
"加里宁" => "加裡寧",
+"动荡" => "動蕩",
"劳力士表" => "勞力士錶",
"包准" => "包准",
"包谷" => "包穀",
-"匏系" => "匏繫",
-"北岳" => "北嶽",
"北斗" => "北斗",
"北回" => "北迴",
"匡复" => "匡複",
"匪干" => "匪幹",
"十卷" => "十卷",
-"十干" => "十干",
"十台" => "十臺",
"十只" => "十隻",
"十出" => "十齣",
-"千百只" => "千百隻",
"千丝万缕" => "千絲萬縷",
"千回百折" => "千迴百折",
"千回百转" => "千迴百轉",
@@ -2868,7 +2864,6 @@ $zh2TW=array(
"半只" => "半隻",
"南岳" => "南嶽",
"南征" => "南征",
-"南斗" => "南斗",
"南台" => "南臺",
"南回" => "南迴",
"卡里" => "卡裡",
@@ -2888,14 +2883,12 @@ $zh2TW=array(
"卷筒" => "卷筒",
"卷纬" => "卷緯",
"卷绕" => "卷繞",
-"卷舌" => "卷舌",
"卷装" => "卷裝",
"卷轴" => "卷軸",
"卷云" => "卷雲",
"卷领" => "卷領",
"卷发" => "卷髮",
"卷须" => "卷鬚",
-"厚朴" => "厚朴",
"参与" => "參与",
"参与者" => "參与者",
"参合" => "參合",
@@ -2913,7 +2906,6 @@ $zh2TW=array(
"反复" => "反覆",
"取舍" => "取捨",
"口里" => "口裡",
-"古柯咸" => "古柯鹹",
"只准" => "只准",
"只冲" => "只衝",
"叮当" => "叮噹",
@@ -2970,23 +2962,18 @@ $zh2TW=array(
"吊钟" => "吊鐘",
"同伙" => "同伙",
"名表" => "名錶",
-"後冠" => "后冠",
-"後北街" => "后北街",
-"後土" => "后土",
-"後妃" => "后妃",
-"後安路" => "后安路",
-"後平路" => "后平路",
-"後座" => "后座",
-"後稷" => "后稷",
-"後羿" => "后羿",
-"後街" => "后街",
-"後里" => "后里",
+"后冠" => "后冠",
+"后土" => "后土",
+"后妃" => "后妃",
+"后座" => "后座",
+"后稷" => "后稷",
+"后羿" => "后羿",
+"后里" => "后里",
"向着" => "向著",
"吞并" => "吞併",
"吹发" => "吹髮",
-"吕後" => "呂后",
-"呆里呆气" => "呆裡呆氣",
-"呈准" => "呈准",
+"吕后" => "呂后",
+"獃里獃气" => "呆裡呆氣",
"周而复始" => "周而複始",
"呼吁" => "呼籲",
"和面" => "和麵",
@@ -2994,10 +2981,8 @@ $zh2TW=array(
"哭脏" => "哭髒",
"问卷" => "問卷",
"喝采" => "喝采",
-"乔岳" => "喬嶽",
"单干" => "單干",
"单只" => "單隻",
-"嘴里" => "嘴裏",
"嘴里" => "嘴裡",
"恶心" => "噁心",
"当啷" => "噹啷",
@@ -3010,13 +2995,12 @@ $zh2TW=array(
"向迩" => "嚮邇",
"严丝合缝" => "嚴絲合縫",
"严复" => "嚴複",
-"囉苏" => "囉囌",
"四舍五入" => "四捨五入",
"四只" => "四隻",
"四出" => "四齣",
-"回历新年" => "回曆新年",
"回丝" => "回絲",
"回着" => "回著",
+"回荡" => "回蕩",
"回复" => "回覆",
"回采" => "回采",
"圈子里" => "圈子裡",
@@ -3024,17 +3008,15 @@ $zh2TW=array(
"国历" => "國曆",
"国雠" => "國讎",
"园里" => "園裡",
-"圆台" => "圓臺",
"图里" => "圖裡",
"土里" => "土裡",
"土制" => "土製",
"地志" => "地誌",
"坍台" => "坍臺",
"坑里" => "坑裡",
+"坦荡" => "坦蕩",
"垂发" => "垂髮",
"垮台" => "垮臺",
-"埃及豔後" => "埃及豔后",
-"埃荣冲" => "埃榮衝",
"埋布" => "埋佈",
"城里" => "城裡",
"基干" => "基幹",
@@ -3046,7 +3028,6 @@ $zh2TW=array(
"墨斗" => "墨斗",
"墨索里尼" => "墨索裡尼",
"垦复" => "墾複",
-"压卷" => "壓卷",
"垄断价格" => "壟斷價格",
"垄断资产" => "壟斷資產",
"垄断集团" => "壟斷集團",
@@ -3065,20 +3046,19 @@ $zh2TW=array(
"大卷" => "大卷",
"大干" => "大干",
"大干" => "大幹",
-"大辟" => "大辟",
+"大锤" => "大鎚",
"大只" => "大隻",
-"天後" => "天后",
+"天后" => "天后",
"天干" => "天干",
"天文台" => "天文臺",
"天翻地复" => "天翻地覆",
-"太後" => "太后",
+"太后" => "太后",
"奏折" => "奏摺",
"女丑" => "女丑",
"女佣" => "女佣",
"好家夥" => "好傢夥",
"好戏连台" => "好戲連臺",
-"好困" => "好睏",
-"如饥似渴" => "如饑似渴",
+"如法泡制" => "如法泡製",
"妆台" => "妝臺",
"姜太公" => "姜太公",
"姜子牙" => "姜子牙",
@@ -3088,26 +3068,18 @@ $zh2TW=array(
"存折" => "存摺",
"孟姜女" => "孟姜女",
"宇宙志" => "宇宙誌",
-"宋皇台道" => "宋皇臺道",
"定准" => "定准",
"定制" => "定製",
"宣布" => "宣佈",
"宫里" => "宮裡",
"家伙" => "家伙",
-"家里" => "家裏",
"家里" => "家裡",
"密布" => "密佈",
-"密致" => "密緻",
"寇雠" => "寇讎",
-"富台街" => "富臺街",
-"寓禁于征" => "寓禁於征",
"实干" => "實幹",
"写字台" => "寫字檯",
"写字台" => "寫字臺",
"宽松" => "寬鬆",
-"宝卷" => "寶卷",
-"宝里宝气" => "寶裡寶氣",
-"封後" => "封后",
"封面里" => "封面裡",
"射干" => "射干",
"对表" => "對錶",
@@ -3115,7 +3087,6 @@ $zh2TW=array(
"小伙" => "小伙",
"小只" => "小隻",
"少吊" => "少吊",
-"就里" => "就裡",
"尺布斗粟" => "尺布斗粟",
"尼克松" => "尼克鬆",
"尼采" => "尼采",
@@ -3125,17 +3096,11 @@ $zh2TW=array(
"屋子里" => "屋子裡",
"屋里" => "屋裡",
"展布" => "展佈",
-"展卷" => "展卷",
"屡仆屡起" => "屢仆屢起",
"屯里" => "屯裡",
"山岳" => "山嶽",
-"山斗" => "山斗",
"山里" => "山裡",
-"山重水复" => "山重水複",
-"岱岳" => "岱嶽",
"峰回" => "峰迴",
-"岳岳" => "嶽嶽",
-"巅复" => "巔覆",
"巡回" => "巡迴",
"巧干" => "巧幹",
"巴尔干" => "巴爾幹",
@@ -3149,55 +3114,37 @@ $zh2TW=array(
"席卷" => "席卷",
"带团参加" => "帶團參加",
"带发修行" => "帶髮修行",
-"干世" => "干世",
"干休" => "干休",
"干系" => "干係",
-"干冒" => "干冒",
"干卿何事" => "干卿何事",
-"干卿底事" => "干卿底事",
-"干城" => "干城",
"干将" => "干將",
-"干德道" => "干德道",
"干戈" => "干戈",
"干挠" => "干撓",
"干扰" => "干擾",
"干支" => "干支",
"干政" => "干政",
"干时" => "干時",
-"干没" => "干沒",
"干涉" => "干涉",
"干犯" => "干犯",
-"干禄" => "干祿",
"干与" => "干與",
"干着急" => "干著急",
-"干诺道中" => "干諾道中",
-"干诺道西" => "干諾道西",
-"干谒" => "干謁",
-"干证" => "干證",
-"干誉" => "干譽",
"干贝" => "干貝",
-"干连" => "干連",
-"干云蔽日" => "干雲蔽日",
"干预" => "干預",
"平台" => "平臺",
"年历" => "年曆",
"年里" => "年裡",
"干上" => "幹上",
"干下去" => "幹下去",
-"干不了" => "幹不了",
-"干不成" => "幹不成",
"干了" => "幹了",
"干事" => "幹事",
"干些" => "幹些",
"干个" => "幹個",
"干劲" => "幹勁",
"干员" => "幹員",
-"干啥" => "幹啥",
"干吗" => "幹嗎",
"干嘛" => "幹嘛",
"干坏事" => "幹壞事",
"干完" => "幹完",
-"干将" => "幹將",
"干得" => "幹得",
"干性油" => "幹性油",
"干才" => "幹才",
@@ -3206,15 +3153,11 @@ $zh2TW=array(
"干活" => "幹活",
"干流" => "幹流",
"干球温度" => "幹球溫度",
-"干略" => "幹略",
"干线" => "幹線",
"干练" => "幹練",
"干警" => "幹警",
"干起来" => "幹起來",
"干路" => "幹路",
-"干办" => "幹辦",
-"干这一行" => "幹這一行",
-"干这种事" => "幹這種事",
"干道" => "幹道",
"干部" => "幹部",
"干么" => "幹麼",
@@ -3222,43 +3165,34 @@ $zh2TW=array(
"几只" => "幾隻",
"几出" => "幾齣",
"底里" => "底裡",
-"店里" => "店裡",
"康采恩" => "康采恩",
"庙里" => "廟裡",
"建台" => "建臺",
"弄脏" => "弄髒",
"弔卷" => "弔卷",
"弘历" => "弘曆",
-"强干弱枝" => "強幹弱枝",
"别扭" => "彆扭",
"别拗" => "彆拗",
"别气" => "彆氣",
"别脚" => "彆腳",
"别着" => "彆著",
"弹子台" => "彈子檯",
-"弹珠台" => "彈珠檯",
"弹药" => "彈葯",
-"汇刊" => "彙刊",
"汇报" => "彙報",
"汇整" => "彙整",
-"汇算" => "彙算",
"汇编" => "彙編",
"汇总" => "彙總",
"汇纂" => "彙纂",
"汇辑" => "彙輯",
"汇集" => "彙集",
"形单影只" => "形單影隻",
-"影後" => "影后",
+"影后" => "影后",
"往里" => "往裡",
"往复" => "往複",
"征伐" => "征伐",
"征兵" => "征兵",
-"征利" => "征利",
"征尘" => "征塵",
"征夫" => "征夫",
-"征属" => "征屬",
-"征帆" => "征帆",
-"征戌" => "征戌",
"征战" => "征戰",
"征收" => "征收",
"征服" => "征服",
@@ -3274,18 +3208,18 @@ $zh2TW=array(
"复辟" => "復辟",
"德干高原" => "德干高原",
"心愿" => "心愿",
-"心里" => "心裏",
+"心荡神驰" => "心蕩神馳",
"心里" => "心裡",
"忙里" => "忙裡",
"快干" => "快幹",
"快冲" => "快衝",
"怎么" => "怎麼",
"怎么着" => "怎麼著",
+"怒发冲冠" => "怒髮衝冠",
"急冲而下" => "急衝而下",
"怪里怪气" => "怪裡怪氣",
"恩准" => "恩准",
"情有所钟" => "情有所鍾",
-"情有独钟" => "情有獨鍾",
"意面" => "意麵",
"慌里慌张" => "慌裡慌張",
"慰借" => "慰藉",
@@ -3297,7 +3231,7 @@ $zh2TW=array(
"怀里" => "懷裡",
"怀表" => "懷錶",
"悬吊" => "懸吊",
-"悬心吊胆" => "懸心吊膽",
+"恋恋不舍" => "戀戀不捨",
"戏台" => "戲臺",
"戴表" => "戴錶",
"戽斗" => "戽斗",
@@ -3305,7 +3239,6 @@ $zh2TW=array(
"手不释卷" => "手不釋卷",
"手卷" => "手卷",
"手折" => "手摺",
-"手里" => "手裏",
"手里" => "手裡",
"手表" => "手錶",
"手松" => "手鬆",
@@ -3331,6 +3264,7 @@ $zh2TW=array(
"拖吊" => "拖吊",
"拗别" => "拗彆",
"拮据" => "拮据",
+"振荡" => "振蕩",
"捍御" => "捍禦",
"舍不得" => "捨不得",
"舍出" => "捨出",
@@ -3353,18 +3287,18 @@ $zh2TW=array(
"舍近求远" => "捨近求遠",
"捲发" => "捲髮",
"捵面" => "捵麵",
+"扫荡" => "掃蕩",
"掌柜" => "掌柜",
"排骨面" => "排骨麵",
"挂帘" => "掛帘",
"挂面" => "掛麵",
"接着说" => "接著說",
-"掩卷" => "掩卷",
"提心吊胆" => "提心吊膽",
"插图卷" => "插圖卷",
"换吊" => "換吊",
"换只" => "換隻",
"换发" => "換髮",
-"握发" => "握髮",
+"摇荡" => "搖蕩",
"搭伙" => "搭伙",
"折合" => "摺合",
"折奏" => "摺奏",
@@ -3388,50 +3322,41 @@ $zh2TW=array(
"担负着" => "擔負著",
"据云" => "據云",
"擢发难数" => "擢髮難數",
-"拟准" => "擬准",
"摆布" => "擺佈",
"摄制" => "攝製",
"支干" => "支幹",
"收获" => "收穫",
"改制" => "改製",
"攻克" => "攻剋",
+"放荡" => "放蕩",
"放松" => "放鬆",
-"故布疑阵" => "故佈疑陣",
"叙说着" => "敘說著",
"散伙" => "散伙",
"散布" => "散佈",
+"散荡" => "散蕩",
"散发" => "散髮",
"整只" => "整隻",
"整出" => "整齣",
-"敌忾同雠" => "敵愾同讎",
-"文借" => "文藉",
"文采" => "文采",
-"斗亚兰路" => "斗亞蘭路",
"斗六" => "斗六",
"斗南" => "斗南",
"斗大" => "斗大",
"斗子" => "斗子",
"斗室" => "斗室",
-"斗宿" => "斗宿",
"斗方" => "斗方",
"斗栱" => "斗栱",
"斗笠" => "斗笠",
-"斗筲" => "斗筲",
"斗箕" => "斗箕",
"斗篷" => "斗篷",
"斗胆" => "斗膽",
-"斗蓬" => "斗蓬",
"斗转参横" => "斗轉參橫",
"斗量" => "斗量",
"斗门" => "斗門",
"料斗" => "料斗",
-"斤斗" => "斤斗",
"斯里兰卡" => "斯裡蘭卡",
"新历" => "新曆",
"断头台" => "斷頭臺",
-"断发文身" => "斷髮文身",
"方才" => "方纔",
-"方志" => "方誌",
"施舍" => "施捨",
"旋绕着" => "旋繞著",
"旋回" => "旋迴",
@@ -3450,15 +3375,14 @@ $zh2TW=array(
"星辰表" => "星辰錶",
"春假里" => "春假裡",
"春天里" => "春天裡",
+"晃荡" => "晃蕩",
"景致" => "景緻",
"暗地里" => "暗地裡",
"暗沟里" => "暗溝裡",
"暗里" => "暗裡",
-"暴敛横征" => "暴斂橫征",
"历数" => "曆數",
"历书" => "曆書",
"历法" => "曆法",
-"历象" => "曆象",
"书卷" => "書卷",
"会干" => "會幹",
"会里" => "會裡",
@@ -3469,22 +3393,16 @@ $zh2TW=array(
"本台" => "本臺",
"朴子" => "朴子",
"朴实" => "朴實",
-"朴忠" => "朴忠",
-"朴直" => "朴直",
"朴硝" => "朴硝",
"朴素" => "朴素",
-"朴茂" => "朴茂",
"朴资茅斯" => "朴資茅斯",
-"朴钝" => "朴鈍",
-"材干" => "材幹",
"村里" => "村裡",
-"杜老志道" => "杜老誌道",
"束发" => "束髮",
-"杯面" => "杯麵",
"东岳" => "東嶽",
"东征" => "東征",
"松赞干布" => "松贊干布",
"板着脸" => "板著臉",
+"板荡" => "板蕩",
"枕借" => "枕藉",
"林宏岳" => "林宏嶽",
"枝干" => "枝幹",
@@ -3494,18 +3412,14 @@ $zh2TW=array(
"柜上" => "柜上",
"柜台" => "柜台",
"柜子" => "柜子",
-"柜柳" => "柜柳",
"查卷" => "查卷",
"查号台" => "查號臺",
"校雠学" => "校讎學",
"核准" => "核准",
"核复" => "核覆",
"格里" => "格裡",
-"案准" => "案准",
"案卷" => "案卷",
"条干" => "條幹",
-"梯冲" => "梯衝",
-"械系" => "械繫",
"棉卷" => "棉卷",
"棉制" => "棉製",
"植发" => "植髮",
@@ -3528,12 +3442,10 @@ $zh2TW=array(
"柜台" => "櫃臺",
"栏干" => "欄干",
"欺蒙" => "欺矇",
-"歌後" => "歌后",
-"歌台舞榭" => "歌臺舞榭",
+"歌后" => "歌后",
"欧几里得" => "歐幾裡得",
"正当着" => "正當著",
-"此仆彼起" => "此仆彼起",
-"武後" => "武后",
+"武后" => "武后",
"武松" => "武鬆",
"归并" => "歸併",
"死里求生" => "死裡求生",
@@ -3541,23 +3453,20 @@ $zh2TW=array(
"残卷" => "殘卷",
"杀虫药" => "殺虫藥",
"壳里" => "殼裡",
-"母後" => "母后",
+"母后" => "母后",
"每只" => "每隻",
"比干" => "比干",
"毛卷" => "毛卷",
-"毛坏" => "毛坏",
"毛发" => "毛髮",
"毫发" => "毫髮",
-"气冲斗牛" => "氣沖斗牛",
"气冲牛斗" => "氣沖牛斗",
"气象台" => "氣象臺",
+"氯霉素" => "氯黴素",
"水斗" => "水斗",
"水里" => "水裡",
"水表" => "水錶",
"永历" => "永曆",
-"永志不忘" => "永誌不忘",
"污蔑" => "汙衊",
-"江干" => "江干",
"池里" => "池裡",
"污蔑" => "污衊",
"沈着" => "沈著",
@@ -3565,47 +3474,47 @@ $zh2TW=array(
"没精打采" => "沒精打采",
"冲着" => "沖著",
"沙里淘金" => "沙裡淘金",
-"河岳" => "河嶽",
"河里" => "河裡",
"油面" => "油麵",
-"泡制" => "泡製",
"泡面" => "泡麵",
"泰斗" => "泰斗",
-"洗发" => "洗髮",
+"洗手不干" => "洗手不幹",
+"洗发精" => "洗髮精",
"派团参加" => "派團參加",
+"流荡" => "流蕩",
+"浩荡" => "浩蕩",
"浪琴表" => "浪琴錶",
-"浮吊" => "浮吊",
+"浪荡" => "浪蕩",
+"浮荡" => "浮蕩",
"海里" => "海裡",
"涂着" => "涂著",
"液晶表" => "液晶錶",
"凉面" => "涼麵",
"淡朱" => "淡硃",
-"渊淳岳峙" => "淵淳嶽峙",
-"渠冲" => "渠衝",
+"淫荡" => "淫蕩",
"测验卷" => "測驗卷",
"港制" => "港製",
+"游荡" => "游蕩",
"凑合着" => "湊合著",
"湖里" => "湖裡",
"汤团" => "湯糰",
"汤面" => "湯麵",
-"温郁" => "溫郁",
"卤制" => "滷製",
"卤面" => "滷麵",
"满布" => "滿佈",
+"漂荡" => "漂蕩",
"漏斗" => "漏斗",
"演奏台" => "演奏臺",
-"潜意识里" => "潛意識裡",
"潭里" => "潭裡",
+"激荡" => "激蕩",
"浓郁" => "濃郁",
"浓发" => "濃髮",
"湿地松" => "濕地鬆",
"蒙蒙" => "濛濛",
"蒙雾" => "濛霧",
-"蒙鸿" => "濛鴻",
"瀛台" => "瀛臺",
"弥漫" => "瀰漫",
"弥漫着" => "瀰漫著",
-"漓江" => "灕江",
"火并" => "火併",
"灰蒙" => "灰濛",
"炒面" => "炒麵",
@@ -3634,7 +3543,7 @@ $zh2TW=array(
"烫面" => "燙麵",
"烛台" => "燭臺",
"炉台" => "爐臺",
-"墙里" => "牆裡",
+"爽荡" => "爽蕩",
"片言只语" => "片言隻語",
"牛肉面" => "牛肉麵",
"牛只" => "牛隻",
@@ -3648,8 +3557,8 @@ $zh2TW=array(
"奖杯" => "獎盃",
"获准" => "獲准",
"率团参加" => "率團參加",
-"王侯後" => "王侯后",
-"王後" => "王后",
+"王侯后" => "王侯后",
+"王后" => "王后",
"班里" => "班裡",
"理发" => "理髮",
"瑶台" => "瑤臺",
@@ -3660,14 +3569,12 @@ $zh2TW=array(
"生发" => "生髮",
"田里" => "田裡",
"由馀" => "由余",
-"由表及里" => "由表及裡",
"男佣" => "男佣",
"男用表" => "男用錶",
"留发" => "留髮",
"畚斗" => "畚斗",
"当着" => "當著",
"疏松" => "疏鬆",
-"疑系" => "疑係",
"疲困" => "疲睏",
"病症" => "病癥",
"症候" => "癥候",
@@ -3675,19 +3582,16 @@ $zh2TW=array(
"症结" => "癥結",
"登台" => "登臺",
"发布" => "發佈",
-"发蒙" => "發矇",
"发着" => "發著",
"发面" => "發麵",
"发霉" => "發黴",
"白卷" => "白卷",
"白干儿" => "白干兒",
-"白里透红" => "白裡透紅",
"白发" => "白髮",
"白面" => "白麵",
-"百谷" => "百穀",
"百里" => "百裡",
"百只" => "百隻",
-"皇後" => "皇后",
+"皇后" => "皇后",
"皇历" => "皇曆",
"皓发" => "皓髮",
"皮里阳秋" => "皮裏陽秋",
@@ -3708,14 +3612,12 @@ $zh2TW=array(
"眼眶里" => "眼眶裡",
"眼里" => "眼裡",
"困乏" => "睏乏",
-"困倦" => "睏倦",
"睡着了" => "睡著了",
"了如" => "瞭如",
"了望" => "瞭望",
"了然" => "瞭然",
"了若指掌" => "瞭若指掌",
"了解" => "瞭解",
-"瞳蒙" => "瞳矇",
"蒙住" => "矇住",
"蒙昧无知" => "矇昧無知",
"蒙混" => "矇混",
@@ -3724,9 +3626,7 @@ $zh2TW=array(
"蒙蔽" => "矇蔽",
"蒙骗" => "矇騙",
"短发" => "短髮",
-"矮几" => "矮几",
"石英表" => "石英錶",
-"石莼" => "石蓴",
"研制" => "研製",
"砰当" => "砰噹",
"砲台" => "砲臺",
@@ -3736,7 +3636,6 @@ $zh2TW=array(
"朱笔" => "硃筆",
"朱红色" => "硃紅色",
"朱色" => "硃色",
-"朱谕" => "硃諭",
"硬干" => "硬幹",
"砚台" => "硯臺",
"碑志" => "碑誌",
@@ -3749,9 +3648,7 @@ $zh2TW=array(
"御寇" => "禦寇",
"御寒" => "禦寒",
"御敌" => "禦敵",
-"礼义干橹" => "禮義干櫓",
"秃发" => "禿髮",
-"秀斗" => "秀斗",
"秀发" => "秀髮",
"私下里" => "私下裡",
"秋天里" => "秋天裡",
@@ -3761,17 +3658,13 @@ $zh2TW=array(
"禀复" => "稟覆",
"稻谷" => "稻穀",
"稽征" => "稽征",
-"谷人" => "穀人",
-"谷保家商" => "穀保家商",
"谷仓" => "穀倉",
"谷场" => "穀場",
"谷子" => "穀子",
-"谷梁" => "穀梁",
"谷壳" => "穀殼",
"谷物" => "穀物",
"谷皮" => "穀皮",
"谷神" => "穀神",
-"谷谷" => "穀穀",
"谷粒" => "穀粒",
"谷舱" => "穀艙",
"谷苗" => "穀苗",
@@ -3780,19 +3673,16 @@ $zh2TW=array(
"谷道" => "穀道",
"谷雨" => "穀雨",
"谷类" => "穀類",
-"谷风" => "穀風",
"积极参与" => "積极參与",
"积极参加" => "積极參加",
-"积谷防饥" => "積穀防饑",
-"空蒙" => "空濛",
+"空荡" => "空蕩",
"窗帘" => "窗帘",
"窗明几净" => "窗明几淨",
"窗台" => "窗檯",
"窗台" => "窗臺",
"窝里" => "窩裡",
"窝阔台" => "窩闊臺",
-"穷发" => "窮髮",
-"站台" => "站臺",
+"穷追不舍" => "窮追不捨",
"笆斗" => "笆斗",
"笑里藏刀" => "笑裡藏刀",
"第一卷" => "第一卷",
@@ -3802,7 +3692,6 @@ $zh2TW=array(
"答复" => "答覆",
"筵几" => "筵几",
"箕斗" => "箕斗",
-"算历" => "算曆",
"签着" => "簽著",
"吁求" => "籲求",
"吁请" => "籲請",
@@ -3817,14 +3706,12 @@ $zh2TW=array(
"糊里糊涂" => "糊裡糊塗",
"团子" => "糰子",
"系着" => "系著",
-"系里" => "系裡",
"纪历" => "紀曆",
-"红绳系足" => "紅繩繫足",
"红发" => "紅髮",
+"红霉素" => "紅黴素",
"纡回" => "紆迴",
"纳采" => "納采",
"素食面" => "素食麵",
-"素发" => "素髮",
"素面" => "素麵",
"紫微斗数" => "紫微斗數",
"细致" => "細緻",
@@ -3841,11 +3728,13 @@ $zh2TW=array(
"丝虫" => "絲蟲",
"綑吊" => "綑吊",
"经卷" => "經卷",
+"绿霉素" => "綠黴素",
"维系" => "維繫",
"绾发" => "綰髮",
"网里" => "網裡",
"紧绷" => "緊繃",
"紧绷着" => "緊繃著",
+"紧追不舍" => "緊追不捨",
"编制" => "編製",
"编发" => "編髮",
"缓冲" => "緩衝",
@@ -3856,7 +3745,6 @@ $zh2TW=array(
"缝里" => "縫裡",
"缝制" => "縫製",
"纤夫" => "縴夫",
-"纤手" => "縴手",
"繁复" => "繁複",
"绷住" => "繃住",
"绷子" => "繃子",
@@ -3869,30 +3757,24 @@ $zh2TW=array(
"绷开" => "繃開",
"绘制" => "繪製",
"系上" => "繫上",
-"系世" => "繫世",
"系到" => "繫到",
"系囚" => "繫囚",
"系心" => "繫心",
"系念" => "繫念",
"系怀" => "繫懷",
-"系恋" => "繫戀",
"系数" => "繫數",
"系于" => "繫於",
"系系" => "繫系",
-"系结" => "繫結",
"系紧" => "繫緊",
"系绳" => "繫繩",
-"系累" => "繫纍",
"系着" => "繫著",
"系辞" => "繫辭",
-"系风捕影" => "繫風捕影",
"缴卷" => "繳卷",
"累囚" => "纍囚",
"累累" => "纍纍",
"坛子" => "罈子",
"坛坛罐罐" => "罈罈罐罐",
"骂着" => "罵著",
-"羁系" => "羈繫",
"美制" => "美製",
"美发" => "美髮",
"翻来复去" => "翻來覆去",
@@ -3909,10 +3791,7 @@ $zh2TW=array(
"肉丝面" => "肉絲麵",
"肉羹面" => "肉羹麵",
"肉松" => "肉鬆",
-"肚里" => "肚裏",
-"肚里" => "肚裡",
"肢体" => "肢体",
-"胃里" => "胃裡",
"背向着" => "背向著",
"背地里" => "背地裡",
"胡里胡涂" => "胡裡胡塗",
@@ -3925,13 +3804,11 @@ $zh2TW=array(
"脑子里" => "腦子裡",
"腰里" => "腰裡",
"胶卷" => "膠卷",
-"膨松" => "膨鬆",
"自制" => "自製",
"自觉自愿" => "自覺自愿",
"台上" => "臺上",
"台下" => "臺下",
"台中" => "臺中",
-"台儿庄" => "臺兒莊",
"台北" => "臺北",
"台南" => "臺南",
"台地" => "臺地",
@@ -3942,8 +3819,6 @@ $zh2TW=array(
"台东" => "臺東",
"台柱" => "臺柱",
"台榭" => "臺榭",
-"台机路" => "臺機路",
-"台步" => "臺步",
"台汽" => "臺汽",
"台海" => "臺海",
"台澎金马" => "臺澎金馬",
@@ -3955,7 +3830,6 @@ $zh2TW=array(
"台糖" => "臺糖",
"台肥" => "臺肥",
"台航" => "臺航",
-"台西" => "臺西",
"台视" => "臺視",
"台词" => "臺詞",
"台车" => "臺車",
@@ -3968,7 +3842,6 @@ $zh2TW=array(
"兴高采烈" => "興高采烈",
"旧历" => "舊曆",
"舒卷" => "舒卷",
-"舞榭歌台" => "舞榭歌臺",
"舞台" => "舞臺",
"航海历" => "航海曆",
"船只" => "船隻",
@@ -3979,7 +3852,6 @@ $zh2TW=array(
"花采" => "花采",
"苑里" => "苑裡",
"若干" => "若干",
-"若干" => "若幹",
"苦干" => "苦幹",
"苦里" => "苦裏",
"苦卤" => "苦鹵",
@@ -3991,6 +3863,7 @@ $zh2TW=array(
"草丛里" => "草叢裡",
"庄里" => "莊裡",
"茎干" => "莖幹",
+"莽荡" => "莽蕩",
"菌丝体" => "菌絲体",
"菌丝体" => "菌絲體",
"华里" => "華裡",
@@ -4010,10 +3883,20 @@ $zh2TW=array(
"蓬发" => "蓬髮",
"蓬松" => "蓬鬆",
"莲台" => "蓮臺",
+"荡来荡去" => "蕩來蕩去",
+"荡女" => "蕩女",
+"荡妇" => "蕩婦",
+"荡寇" => "蕩寇",
+"荡平" => "蕩平",
+"荡涤" => "蕩滌",
+"荡漾" => "蕩漾",
+"荡然" => "蕩然",
+"荡舟" => "蕩舟",
+"荡船" => "蕩船",
+"荡荡" => "蕩蕩",
"薑丝" => "薑絲",
"薙发" => "薙髮",
"借以" => "藉以",
-"借助" => "藉助",
"借口" => "藉口",
"借故" => "藉故",
"借机" => "藉機",
@@ -4032,9 +3915,7 @@ $zh2TW=array(
"萝卜" => "蘿蔔",
"虎须" => "虎鬚",
"号志" => "號誌",
-"蜂後" => "蜂后",
-"蜜里调油" => "蜜裡調油",
-"蠁干" => "蠁幹",
+"蜂后" => "蜂后",
"蛮干" => "蠻幹",
"行事历" => "行事曆",
"胡同" => "衚衕",
@@ -4042,7 +3923,6 @@ $zh2TW=array(
"冲下" => "衝下",
"冲来" => "衝來",
"冲倒" => "衝倒",
-"冲冠" => "衝冠",
"冲出" => "衝出",
"冲到" => "衝到",
"冲刺" => "衝刺",
@@ -4075,7 +3955,6 @@ $zh2TW=array(
"冲过" => "衝過",
"冲锋" => "衝鋒",
"表里" => "表裡",
-"袋里" => "袋裡",
"袖里" => "袖裡",
"被里" => "被裡",
"被复" => "被複",
@@ -4177,7 +4056,6 @@ $zh2TW=array(
"复苏" => "複蘇",
"复制" => "複製",
"复诊" => "複診",
-"复评" => "複評",
"复词" => "複詞",
"复试" => "複試",
"复课" => "複課",
@@ -4187,7 +4065,6 @@ $zh2TW=array(
"复述" => "複述",
"复选" => "複選",
"复钱" => "複錢",
-"复阅" => "複閱",
"复杂" => "複雜",
"复电" => "複電",
"复音" => "複音",
@@ -4202,13 +4079,10 @@ $zh2TW=array(
"复亡" => "覆亡",
"复住" => "覆住",
"复信" => "覆信",
-"复冒" => "覆冒",
-"复判" => "覆判",
"复命" => "覆命",
"复在" => "覆在",
"复审" => "覆審",
-"复写" => "覆寫",
-"复巢" => "覆巢",
+"复巢之下" => "覆巢之下",
"复成" => "覆成",
"复败" => "覆敗",
"复文" => "覆文",
@@ -4217,22 +4091,17 @@ $zh2TW=array(
"复水难收" => "覆水難收",
"复没" => "覆沒",
"复灭" => "覆滅",
-"复叠" => "覆疊",
"复盆" => "覆盆",
"复舟" => "覆舟",
"复着" => "覆著",
"复盖" => "覆蓋",
"复盖着" => "覆蓋著",
"复试" => "覆試",
-"复诵" => "覆誦",
"复议" => "覆議",
"复车" => "覆車",
"复载" => "覆載",
"复辙" => "覆轍",
-"复述" => "覆述",
-"复选" => "覆選",
"复电" => "覆電",
-"复鼎金" => "覆鼎金",
"见复" => "見覆",
"亲征" => "親征",
"观众台" => "觀眾臺",
@@ -4244,9 +4113,7 @@ $zh2TW=array(
"订制" => "訂製",
"诉说着" => "訴說著",
"词汇" => "詞彙",
-"词采" => "詞采",
"试卷" => "試卷",
-"试制" => "試製",
"诗卷" => "詩卷",
"话里有话" => "話裡有話",
"志哀" => "誌哀",
@@ -4264,7 +4131,6 @@ $zh2TW=array(
"讲台" => "講臺",
"谢绝参观" => "謝絕參觀",
"护发" => "護髮",
-"雠正" => "讎正",
"雠隙" => "讎隙",
"豆腐干" => "豆腐干",
"竖着" => "豎著",
@@ -4274,22 +4140,20 @@ $zh2TW=array(
"丰采" => "豐采",
"象征着" => "象徵著",
"贵干" => "貴幹",
-"贾後" => "賈后",
+"贾后" => "賈后",
"赈饥" => "賑饑",
-"赐复" => "賜覆",
-"贤後" => "賢后",
+"贤后" => "賢后",
"质朴" => "質朴",
"赌台" => "賭檯",
"购并" => "購併",
-"赤绳系足" => "赤繩繫足",
"赤松" => "赤鬆",
"起吊" => "起吊",
"起复" => "起複",
-"超级杯" => "超級盃",
"赶制" => "趕製",
+"跌荡" => "跌蕩",
"跟斗" => "跟斗",
+"跳荡" => "跳蕩",
"跳表" => "跳錶",
-"蹈借" => "蹈藉",
"踬仆" => "躓仆",
"躯干" => "軀幹",
"车库里" => "車庫裡",
@@ -4299,7 +4163,6 @@ $zh2TW=array(
"轮回" => "輪迴",
"转台" => "轉檯",
"辛丑" => "辛丑",
-"辟易" => "辟易",
"辟邪" => "辟邪",
"办伙" => "辦伙",
"办公台" => "辦公檯",
@@ -4317,10 +4180,9 @@ $zh2TW=array(
"回旋" => "迴旋",
"回流" => "迴流",
"回环" => "迴環",
-"回盪" => "迴盪",
+"回荡" => "迴盪",
"回纹针" => "迴紋針",
"回绕" => "迴繞",
-"回翔" => "迴翔",
"回肠" => "迴腸",
"回荡" => "迴蕩",
"回诵" => "迴誦",
@@ -4328,8 +4190,6 @@ $zh2TW=array(
"回转" => "迴轉",
"回递性" => "迴遞性",
"回避" => "迴避",
-"回銮" => "迴鑾",
-"回音" => "迴音",
"回响" => "迴響",
"回风" => "迴風",
"回首" => "迴首",
@@ -4345,6 +4205,7 @@ $zh2TW=array(
"速食面" => "速食麵",
"连系" => "連繫",
"连台好戏" => "連臺好戲",
+"游荡" => "遊蕩",
"遍布" => "遍佈",
"递回" => "遞迴",
"远征" => "遠征",
@@ -4352,7 +4213,6 @@ $zh2TW=array(
"遮复" => "遮覆",
"还冲" => "還衝",
"邋里邋遢" => "邋裡邋遢",
-"那里" => "那裏",
"那里" => "那裡",
"那只" => "那隻",
"那么" => "那麼",
@@ -4381,22 +4241,18 @@ $zh2TW=array(
"采女" => "采女",
"采声" => "采聲",
"采色" => "采色",
-"采薇" => "采薇",
-"采薪之忧" => "采薪之憂",
-"采兰赠药" => "采蘭贈藥",
"采邑" => "采邑",
-"采采" => "采采",
-"采风" => "采風",
"里程表" => "里程錶",
"重折" => "重摺",
-"重制" => "重製",
"重复" => "重複",
"重复" => "重覆",
+"重锤" => "重鎚",
"野台戏" => "野臺戲",
"金斗" => "金斗",
-"金装玉里" => "金裝玉裡",
"金表" => "金錶",
"金发" => "金髮",
+"金霉素" => "金黴素",
+"钉锤" => "釘鎚",
"银朱" => "銀硃",
"银发" => "銀髮",
"铜制" => "銅製",
@@ -4413,11 +4269,17 @@ $zh2TW=array(
"锅台" => "鍋臺",
"锻鍊出" => "鍛鍊出",
"锻鍊身体" => "鍛鍊身体",
+"锲而不舍" => "鍥而不捨",
+"锤儿" => "鎚兒",
+"锤子" => "鎚子",
+"锤头" => "鎚頭",
+"链霉素" => "鏈黴素",
"镜台" => "鏡臺",
"锈病" => "鏽病",
"锈菌" => "鏽菌",
"锈蚀" => "鏽蝕",
"钟表" => "鐘錶",
+"铁锤" => "鐵鎚",
"铁锈" => "鐵鏽",
"长征" => "長征",
"长发" => "長髮",
@@ -4430,6 +4292,7 @@ $zh2TW=array(
"开诚布公" => "開誠佈公",
"开采" => "開采",
"閒情逸致" => "閒情逸緻",
+"閒荡" => "閒蕩",
"间不容发" => "間不容髮",
"闵采尔" => "閔采爾",
"阅卷" => "閱卷",
@@ -4464,7 +4327,6 @@ $zh2TW=array(
"鸡腿面" => "雞腿麵",
"鸡只" => "雞隻",
"难舍" => "難捨",
-"雨花台" => "雨花臺",
"雪里" => "雪裡",
"云须" => "雲鬚",
"电子表" => "電子錶",
@@ -4473,7 +4335,7 @@ $zh2TW=array(
"电复" => "電覆",
"电视台" => "電視臺",
"电表" => "電錶",
-"雾台" => "霧臺",
+"震荡" => "震蕩",
"雾里" => "霧裡",
"露台" => "露臺",
"灵台" => "靈臺",
@@ -4487,7 +4349,6 @@ $zh2TW=array(
"鞭辟入里" => "鞭辟入裡",
"韩国制" => "韓國製",
"韩制" => "韓製",
-"颂系" => "頌繫",
"预制" => "預製",
"颁布" => "頒佈",
"头里" => "頭裡",
@@ -4511,6 +4372,7 @@ $zh2TW=array(
"刮走" => "颳走",
"刮起" => "颳起",
"刮风" => "颳風",
+"飘荡" => "飄蕩",
"饭团" => "飯糰",
"饼干" => "餅干",
"馄饨面" => "餛飩麵",
@@ -4528,13 +4390,13 @@ $zh2TW=array(
"馥郁" => "馥郁",
"马里" => "馬裡",
"马表" => "馬錶",
+"骀荡" => "駘蕩",
"腾冲" => "騰衝",
"骨子里" => "骨子裡",
"骨干" => "骨幹",
"骨灰坛" => "骨灰罈",
"肮脏" => "骯髒",
"脏乱" => "髒亂",
-"脏了" => "髒了",
"脏兮兮" => "髒兮兮",
"脏字" => "髒字",
"脏得" => "髒得",
@@ -4572,7 +4434,6 @@ $zh2TW=array(
"发针" => "髮針",
"发长" => "髮長",
"发际" => "髮際",
-"发雕" => "髮雕",
"发霜" => "髮霜",
"发髻" => "髮髻",
"发鬓" => "髮鬢",
@@ -4587,7 +4448,6 @@ $zh2TW=array(
"松快" => "鬆快",
"松懈" => "鬆懈",
"松手" => "鬆手",
-"松掉" => "鬆掉",
"松散" => "鬆散",
"松林" => "鬆林",
"松柔" => "鬆柔",
@@ -4624,11 +4484,9 @@ $zh2TW=array(
"闹着玩儿" => "鬧著玩儿",
"闹着玩儿" => "鬧著玩兒",
"郁郁" => "鬱郁",
-"魂牵梦系" => "魂牽夢繫",
"鱼松" => "魚鬆",
"鲸须" => "鯨鬚",
"鲇鱼" => "鯰魚",
-"鸿篇巨制" => "鴻篇巨製",
"鹤发" => "鶴髮",
"卤化" => "鹵化",
"卤味" => "鹵味",
@@ -4664,7 +4522,6 @@ $zh2TW=array(
"面团" => "麵團",
"面店" => "麵店",
"面厂" => "麵廠",
-"面摊" => "麵攤",
"面杖" => "麵杖",
"面条" => "麵條",
"面灰" => "麵灰",
@@ -4678,341 +4535,28 @@ $zh2TW=array(
"面饺" => "麵餃",
"面饼" => "麵餅",
"麻酱面" => "麻醬麵",
-"黄卷" => "黃卷",
"黄历" => "黃曆",
-"黄发" => "黃髮",
+"黄发垂髫" => "黃髮垂髫",
"黑发" => "黑髮",
"黑松" => "黑鬆",
"霉毒" => "黴毒",
-"霉素" => "黴素",
"霉菌" => "黴菌",
"鼓里" => "鼓裡",
"冬冬" => "鼕鼕",
"龙卷" => "龍卷",
"龙须" => "龍鬚",
-"内存"=>"記憶體",
-"默认"=>"預設",
-"缺省"=>"預設",
-"串行"=>"串列",
-"以太网"=>"乙太網",
-"位图"=>"點陣圖",
-"例程"=>"常式",
-"信道"=>"通道",
-"光标"=>"游標",
-"光盘"=>"光碟",
-"光驱"=>"光碟機",
-"全角"=>"全形",
-"共享"=>"共用",
-"兼容"=>"相容",
-"前缀"=>"首碼",
-"后缀"=>"尾碼",
-"加载"=>"載入",
-"半角"=>"半形",
-"变量"=>"變數",
-"噪声"=>"雜訊",
-"因子"=>"因數",
-"在线"=>"線上",
-"脱机"=>"離線",
-"域名"=>"功能變數名稱",
-"声卡"=>"音效卡",
-"字号"=>"字型大小",
-"字库"=>"字型檔",
-"字段"=>"欄位",
-"字符"=>"字元",
-"存盘"=>"存檔",
-"寻址"=>"定址",
-"尾注"=>"章節附註",
-"异步"=>"非同步",
-"总线"=>"匯流排",
-"括号"=>"括弧",
-"接口"=>"介面",
-"控件"=>"控制項",
-"权限"=>"許可權",
-"盘片"=>"碟片",
-"硅片"=>"矽片",
-"硅谷"=>"矽谷",
-"硬盘"=>"硬碟",
-"磁盘"=>"磁碟",
-"磁道"=>"磁軌",
-"程控"=>"程式控制",
-"端口"=>"埠",
-"算子"=>"運算元",
-"算法"=>"演算法",
-"芯片"=>"晶片",
-"芯片"=>"晶元",
-"词组"=>"片語",
-"译码"=>"解碼",
-"软驱"=>"軟碟機",
-"闪存"=>"快閃記憶體",
-"鼠标"=>"滑鼠",
-"进制"=>"進位",
-"交互式"=>"互動式",
-"仿真"=>"模擬",
-"优先级"=>"優先順序",
-"传感"=>"感測",
-"便携式"=>"攜帶型",
-"信息论"=>"資訊理論",
-"循环"=>"迴圈",
-"写保护"=>"防寫",
-"分布式"=>"分散式",
-"分辨率"=>"解析度",
-"程序"=>"程式",
-"服务器"=>"伺服器",
-"等于"=>"等於",
-"局域网"=>"區域網",
-"上载"=>"上傳",
-"计算机"=>"電腦",
-"宏"=>"巨集",
-"扫瞄仪"=>"掃瞄器",
-"宽带"=>"寬頻",
-"窗口"=>"視窗",
-"数据库"=>"資料庫",
-"公历"=>"西曆",
-"奶酪"=>"乳酪",
-"巨商"=>"鉅賈",
-"手电"=>"手電筒",
-"万历"=>"萬曆",
-"永历"=>"永曆",
-"词汇"=>"辭彙",
-"保安"=>"保全",
-"习用"=>"慣用",
-"元音"=>"母音",
-"任意球"=>"自由球",
-"头球"=>"頭槌",
-"入球"=>"進球",
-"粒入球"=>"顆進球",
-"打门"=>"射門",
-"火锅盖帽"=>"蓋火鍋",
-"打印机"=>"印表機",
-"打印機"=>"印表機",
-"字节"=>"位元組",
-"字節"=>"位元組",
-"打印"=>"列印",
-"打印"=>"列印",
-"硬件"=>"硬體",
-"硬件"=>"硬體",
-"二极管"=>"二極體",
-"二極管"=>"二極體",
-"三极管"=>"三極體",
-"三極管"=>"三極體",
-"数码"=>"數位",
-"數碼"=>"數位",
-"软件"=>"軟體",
-"軟件"=>"軟體",
-"网络"=>"網路",
-"網絡"=>"網路",
-"人工智能"=>"人工智慧",
-"航天飞机"=>"太空梭",
-"穿梭機"=>"太空梭",
-"因特网"=>"網際網路",
-"互聯網"=>"網際網路",
-"机器人"=>"機器人",
-"機械人"=>"機器人",
-"移动电话"=>"行動電話",
-"流動電話"=>"行動電話",
-"调制解调器"=>"數據機",
-"調制解調器"=>"數據機",
-"短信"=>"簡訊",
-"短訊"=>"簡訊",
-"乌兹别克斯坦"=>"烏茲別克",
-"乍得"=>"查德",
-"乍得"=>"查德",
-"也门"=>"葉門",
-"也門"=>"葉門",
-"伯利兹"=>"貝里斯",
-"伯利茲"=>"貝里斯",
-"佛得角"=>"維德角",
-"佛得角"=>"維德角",
-"克罗地亚"=>"克羅埃西亞",
-"克羅地亞"=>"克羅埃西亞",
-"冈比亚"=>"甘比亞",
-"岡比亞"=>"甘比亞",
-"几内亚比绍"=>"幾內亞比索",
-"幾內亞比紹"=>"幾內亞比索",
-"列支敦士登"=>"列支敦斯登",
-"列支敦士登"=>"列支敦斯登",
-"利比里亚"=>"賴比瑞亞",
-"利比里亞"=>"賴比瑞亞",
-"加纳"=>"迦納",
-"加納"=>"迦納",
-"加蓬"=>"加彭",
-"加蓬"=>"加彭",
-"博茨瓦纳"=>"波札那",
-"博茨瓦納"=>"波札那",
-"卡塔尔"=>"卡達",
-"卡塔爾"=>"卡達",
-"卢旺达"=>"盧安達",
-"盧旺達"=>"盧安達",
-"危地马拉"=>"瓜地馬拉",
-"危地馬拉"=>"瓜地馬拉",
-"厄瓜多尔"=>"厄瓜多",
-"厄瓜多爾"=>"厄瓜多",
-"厄立特里亚"=>"厄利垂亞",
-"厄立特里亞"=>"厄利垂亞",
-"吉布提"=>"吉布地",
-"吉布堤"=>"吉布地",
-"哈萨克斯坦"=>"哈薩克",
-"哥斯达黎加"=>"哥斯大黎加",
-"哥斯達黎加"=>"哥斯大黎加",
-"图瓦卢"=>"吐瓦魯",
-"圖瓦盧"=>"吐瓦魯",
-"土库曼斯坦"=>"土庫曼",
-"圣卢西亚"=>"聖露西亞",
-"聖盧西亞"=>"聖露西亞",
-"圣基茨和尼维斯"=>"聖克里斯多福及尼維斯",
-"聖吉斯納域斯"=>"聖克里斯多福及尼維斯",
-"圣文森特和格林纳丁斯"=>"聖文森及格瑞那丁",
-"聖文森特和格林納丁斯"=>"聖文森及格瑞那丁",
-"圣马力诺"=>"聖馬利諾",
-"聖馬力諾"=>"聖馬利諾",
-"圭亚那"=>"蓋亞那",
-"圭亞那"=>"蓋亞那",
-"坦桑尼亚"=>"坦尚尼亞",
-"坦桑尼亞"=>"坦尚尼亞",
-"埃塞俄比亚"=>"衣索比亞",
-"埃塞俄比亞"=>"衣索比亞",
-"基里巴斯"=>"吉里巴斯",
-"基里巴斯"=>"吉里巴斯",
-"塔吉克斯坦"=>"塔吉克",
-"塞拉利昂"=>"獅子山",
-"塞拉利昂"=>"獅子山",
-"塞浦路斯"=>"塞普勒斯",
-"塞浦路斯"=>"塞普勒斯",
-"塞舌尔"=>"塞席爾",
-"塞舌爾"=>"塞席爾",
-"多米尼加"=>"多明尼加",
-"多明尼加共和國"=>"多明尼加",
-"多米尼加联邦"=>"多米尼克",
-"多明尼加聯邦"=>"多米尼克",
-"安提瓜和巴布达"=>"安地卡及巴布達",
-"安提瓜和巴布達"=>"安地卡及巴布達",
-"尼日利亚"=>"奈及利亞",
-"尼日利亞"=>"奈及利亞",
-"尼日尔"=>"尼日",
-"尼日爾"=>"尼日",
-"巴巴多斯"=>"巴貝多",
-"巴巴多斯"=>"巴貝多",
-"巴布亚新几内亚"=>"巴布亞紐幾內亞",
-"巴布亞新畿內亞"=>"巴布亞紐幾內亞",
-"布基纳法索"=>"布吉納法索",
-"布基納法索"=>"布吉納法索",
-"布隆迪"=>"蒲隆地",
-"布隆迪"=>"蒲隆地",
-"希腊"=>"希臘",
-"帕劳"=>"帛琉",
-"意大利"=>"義大利",
-"意大利"=>"義大利",
-"所罗门群岛"=>"索羅門群島",
-"所羅門群島"=>"索羅門群島",
-"文莱"=>"汶萊",
-"斯威士兰"=>"史瓦濟蘭",
-"斯威士蘭"=>"史瓦濟蘭",
-"斯洛文尼亚"=>"斯洛維尼亞",
-"斯洛文尼亞"=>"斯洛維尼亞",
-"新西兰"=>"紐西蘭",
-"新西蘭"=>"紐西蘭",
-"朝鲜"=>"北韓",
-"格林纳达"=>"格瑞那達",
-"格林納達"=>"格瑞那達",
-"格鲁吉亚"=>"喬治亞",
-"格魯吉亞"=>"喬治亞",
-"梵蒂冈"=>"教廷",
-"梵蒂岡"=>"教廷",
-"毛里塔尼亚"=>"茅利塔尼亞",
-"毛里塔尼亞"=>"茅利塔尼亞",
-"毛里求斯"=>"模里西斯",
-"毛里裘斯"=>"模里西斯",
-"沙特阿拉伯"=>"沙烏地阿拉伯",
-"沙地阿拉伯"=>"沙烏地阿拉伯",
-"波斯尼亚和黑塞哥维那"=>"波士尼亞赫塞哥維納",
-"波斯尼亞黑塞哥維那"=>"波士尼亞赫塞哥維納",
-"津巴布韦"=>"辛巴威",
-"津巴布韋"=>"辛巴威",
-"洪都拉斯"=>"宏都拉斯",
-"洪都拉斯"=>"宏都拉斯",
-"特立尼达和托巴哥"=>"千里達托貝哥",
-"特立尼達和多巴哥"=>"千里達托貝哥",
-"瑙鲁"=>"諾魯",
-"瑙魯"=>"諾魯",
-"瓦努阿图"=>"萬那杜",
-"瓦努阿圖"=>"萬那杜",
-"溫納圖萬"=>"那杜",
-"科摩罗"=>"葛摩",
-"科摩羅"=>"葛摩",
-"科特迪瓦"=>"象牙海岸",
-"突尼斯"=>"突尼西亞",
-"索马里"=>"索馬利亞",
-"索馬里"=>"索馬利亞",
-"老挝"=>"寮國",
-"老撾"=>"寮國",
-"肯尼亚"=>"肯亞",
-"肯雅"=>"肯亞",
-"苏里南"=>"蘇利南",
-"莫桑比克"=>"莫三比克",
-"莱索托"=>"賴索托",
-"萊索托"=>"賴索托",
-"贝宁"=>"貝南",
-"貝寧"=>"貝南",
-"赞比亚"=>"尚比亞",
-"贊比亞"=>"尚比亞",
-"阿塞拜疆"=>"亞塞拜然",
-"阿塞拜疆"=>"亞塞拜然",
-"阿拉伯联合酋长国"=>"阿拉伯聯合大公國",
-"阿拉伯聯合酋長國"=>"阿拉伯聯合大公國",
-"韩国"=>"南韓",
-"马尔代夫"=>"馬爾地夫",
-"馬爾代夫"=>"馬爾地夫",
-"马耳他"=>"馬爾他",
-"马里"=>"馬利",
-"馬里"=>"馬利",
-"方便面"=>"速食麵",
-"快速面"=>"速食麵",
-"即食麵"=>"速食麵",
-"薯仔"=>"土豆",
-"蹦极跳"=>"笨豬跳",
-"绑紧跳"=>"笨豬跳",
-"冷菜"=>"冷盤",
-"凉菜"=>"冷盤",
-"的士"=>"計程車",
-"出租车"=>"計程車",
-"巴士"=>"公車",
-"公共汽车"=>"公車",
-"台球"=>"撞球",
-"桌球"=>"撞球",
-"雪糕"=>"冰淇淋",
-"卫生"=>"衛生",
-"衞生"=>"衛生",
-"平治"=>"賓士",
-"奔驰"=>"賓士",
-"積架"=>"捷豹",
-"福士"=>"福斯",
-"雪铁龙"=>"雪鐵龍",
-"马自达"=>"馬自達",
-"萬事得"=>"馬自達",
-"布什"=>"布希",
-"布殊"=>"布希",
-"克林顿"=>"柯林頓",
-"克林頓"=>"柯林頓",
-"萨达姆"=>"海珊",
-"薩達姆"=>"海珊",
-"凡高"=>"梵谷",
-"狄安娜"=>"黛安娜",
-"戴安娜"=>"黛安娜",
-"赫拉"=>"希拉",
);
-
-$zh2CN=array(
-"么"=>"么",
+$zh2Hans = array(
"瀋"=>"沈",
"畫"=>"划",
"鍾"=>"钟",
+"靦"=>"腼",
"餘"=>"余",
"鯰"=>"鲇",
"鹼"=>"硷",
-"麼"=>"么",
-"䊷"=>"䌶",
-"𧩙"=>"䜥",
+"㠏"=>"㟆",
+"𡞵"=>"㛟",
"万"=>"万",
"与"=>"与",
"丑"=>"丑",
@@ -5087,6 +4631,7 @@ $zh2CN=array(
"優"=>"优",
"儲"=>"储",
"儷"=>"俪",
+"儸"=>"㑩",
"儺"=>"傩",
"儻"=>"傥",
"儼"=>"俨",
@@ -5125,7 +4670,9 @@ $zh2CN=array(
"劊"=>"刽",
"劌"=>"刿",
"劍"=>"剑",
+"劏"=>"㓥",
"劑"=>"剂",
+"劚"=>"㔉",
"勁"=>"劲",
"動"=>"动",
"務"=>"务",
@@ -5158,11 +4705,11 @@ $zh2CN=array(
"叶"=>"叶",
"吊"=>"吊",
"后"=>"后",
-"后"=>"後",
"吒"=>"咤",
"吳"=>"吴",
"吶"=>"呐",
"呂"=>"吕",
+"呆"=>"獃",
"咼"=>"呙",
"員"=>"员",
"唄"=>"呗",
@@ -5200,6 +4747,7 @@ $zh2CN=array(
"嘽"=>"啴",
"噁"=>"恶",
"噓"=>"嘘",
+"噚"=>"㖊",
"噝"=>"咝",
"噠"=>"哒",
"噥"=>"哝",
@@ -5423,6 +4971,7 @@ $zh2CN=array(
"復"=>"复",
"徵"=>"征",
"徹"=>"彻",
+"志"=>"志",
"恆"=>"恒",
"恥"=>"耻",
"悅"=>"悦",
@@ -5490,7 +5039,6 @@ $zh2CN=array(
"戶"=>"户",
"担"=>"担",
"拋"=>"抛",
-"拾"=>"十",
"挩"=>"捝",
"挾"=>"挟",
"捨"=>"舍",
@@ -5700,6 +5248,7 @@ $zh2CN=array(
"殫"=>"殚",
"殮"=>"殓",
"殯"=>"殡",
+"殰"=>"㱩",
"殲"=>"歼",
"殺"=>"杀",
"殻"=>"壳",
@@ -5803,6 +5352,7 @@ $zh2CN=array(
"濾"=>"滤",
"瀅"=>"滢",
"瀆"=>"渎",
+"瀇"=>"㲿",
"瀉"=>"泻",
"瀋"=>"沈",
"瀏"=>"浏",
@@ -5839,6 +5389,7 @@ $zh2CN=array(
"煥"=>"焕",
"煩"=>"烦",
"煬"=>"炀",
+"煱"=>"㶽",
"熅"=>"煴",
"熒"=>"荧",
"熗"=>"炝",
@@ -5855,6 +5406,7 @@ $zh2CN=array(
"燦"=>"灿",
"燭"=>"烛",
"燴"=>"烩",
+"燶"=>"㶶",
"燼"=>"烬",
"燾"=>"焘",
"爍"=>"烁",
@@ -5877,6 +5429,7 @@ $zh2CN=array(
"猶"=>"犹",
"猻"=>"狲",
"獁"=>"犸",
+"獃"=>"呆",
"獄"=>"狱",
"獅"=>"狮",
"獎"=>"奖",
@@ -5885,6 +5438,7 @@ $zh2CN=array(
"獫"=>"猃",
"獮"=>"狝",
"獰"=>"狞",
+"獱"=>"㺍",
"獲"=>"获",
"獵"=>"猎",
"獷"=>"犷",
@@ -5965,6 +5519,7 @@ $zh2CN=array(
"監"=>"监",
"盤"=>"盘",
"盧"=>"卢",
+"盪"=>"荡",
"眥"=>"眦",
"眾"=>"众",
"睏"=>"困",
@@ -6020,6 +5575,7 @@ $zh2CN=array(
"种"=>"种",
"稅"=>"税",
"稈"=>"秆",
+"稏"=>"䅉",
"稟"=>"禀",
"種"=>"种",
"稱"=>"称",
@@ -6154,6 +5710,7 @@ $zh2CN=array(
"綉"=>"绣",
"綌"=>"绤",
"綏"=>"绥",
+"綐"=>"䌼",
"經"=>"经",
"綜"=>"综",
"綞"=>"缍",
@@ -6169,6 +5726,7 @@ $zh2CN=array(
"網"=>"网",
"綳"=>"绷",
"綴"=>"缀",
+"綵"=>"䌽",
"綸"=>"纶",
"綹"=>"绺",
"綺"=>"绮",
@@ -6254,6 +5812,7 @@ $zh2CN=array(
"繼"=>"继",
"繽"=>"缤",
"繾"=>"缱",
+"繿"=>"䍀",
"纈"=>"缬",
"纊"=>"纩",
"續"=>"续",
@@ -6336,7 +5895,6 @@ $zh2CN=array(
"艱"=>"艰",
"艷"=>"艳",
"芻"=>"刍",
-"苎"=>"苧",
"苧"=>"苎",
"苹"=>"苹",
"范"=>"范",
@@ -6395,6 +5953,7 @@ $zh2CN=array(
"薟"=>"莶",
"薦"=>"荐",
"薩"=>"萨",
+"薳"=>"䓕",
"薴"=>"苧",
"薺"=>"荠",
"藉"=>"借",
@@ -6437,6 +5996,7 @@ $zh2CN=array(
"螄"=>"蛳",
"螞"=>"蚂",
"螢"=>"萤",
+"螮"=>"䗖",
"螻"=>"蝼",
"螿"=>"螀",
"蟄"=>"蛰",
@@ -6487,6 +6047,7 @@ $zh2CN=array(
"襠"=>"裆",
"襤"=>"褴",
"襪"=>"袜",
+"襬"=>"䙓",
"襯"=>"衬",
"襲"=>"袭",
"覆"=>"复",
@@ -6650,6 +6211,7 @@ $zh2CN=array(
"謳"=>"讴",
"謹"=>"谨",
"謾"=>"谩",
+"譅"=>"䜧",
"證"=>"证",
"譎"=>"谲",
"譏"=>"讥",
@@ -6682,6 +6244,7 @@ $zh2CN=array(
"豬"=>"猪",
"豶"=>"豮",
"貓"=>"猫",
+"貙"=>"䝙",
"貝"=>"贝",
"貞"=>"贞",
"貟"=>"贠",
@@ -6735,6 +6298,7 @@ $zh2CN=array(
"賫"=>"赍",
"賬"=>"账",
"賭"=>"赌",
+"賰"=>"䞐",
"賴"=>"赖",
"賵"=>"赗",
"賺"=>"赚",
@@ -7080,7 +6644,9 @@ $zh2CN=array(
"鎔"=>"镕",
"鎖"=>"锁",
"鎘"=>"镉",
+"鎚"=>"锤",
"鎛"=>"镈",
+"鎝"=>"𨱏",
"鎡"=>"镃",
"鎢"=>"钨",
"鎣"=>"蓥",
@@ -7251,7 +6817,6 @@ $zh2CN=array(
"靈"=>"灵",
"靚"=>"靓",
"靜"=>"静",
-"靦"=>"腼",
"靨"=>"靥",
"鞀"=>"鼗",
"鞏"=>"巩",
@@ -7431,6 +6996,7 @@ $zh2CN=array(
"騖"=>"骛",
"騙"=>"骗",
"騤"=>"骙",
+"騧"=>"䯄",
"騫"=>"骞",
"騭"=>"骘",
"騮"=>"骝",
@@ -7671,6 +7237,7 @@ $zh2CN=array(
"鷺"=>"鹭",
"鷽"=>"鸴",
"鷿"=>"䴙",
+"鸂"=>"㶉",
"鸇"=>"鹯",
"鸌"=>"鹱",
"鸏"=>"鹲",
@@ -7740,15 +7307,11 @@ $zh2CN=array(
"名畫" => "名画",
"奇畫" => "奇画",
"如畫" => "如画",
-"么 " => "幺 ",
-"么廝" => "幺厮",
-"么爹" => "幺爹",
"弱鹼" => "弱碱",
"彩畫" => "彩画",
"所畫" => "所画",
"扉畫" => "扉画",
"教畫" => "教画",
-"楊么" => "杨幺",
"水鹼" => "水碱",
"洋鹼" => "洋碱",
"炭畫" => "炭画",
@@ -7800,7 +7363,6 @@ $zh2CN=array(
"策畫" => "策画",
"組畫" => "组画",
"絹畫" => "绢画",
-"老么" => "老幺",
"耐鹼" => "耐碱",
"肉鹼" => "肉碱",
"膠畫" => "胶画",
@@ -7808,12 +7370,10 @@ $zh2CN=array(
"西畫" => "西画",
"貼畫" => "贴画",
"返鹼" => "返碱",
-"那麼" => "那麽",
"鍾鍛" => "锺锻",
"鍛鍾" => "锻锺",
"雕畫" => "雕画",
"鯰 " => "鲶 ",
-"麼 " => "麽 ",
"三聯畫" => "三联画",
"中國畫" => "中国画",
"書畫 " => "书画 ",
@@ -7889,344 +7449,306 @@ $zh2CN=array(
"鍾 " => "锺 ",
"靜物畫" => "静物画",
"餘 " => "馀 ",
-"記憶體"=>"内存",
-"預設"=>"默认",
-"預設"=>"缺省",
-"串列"=>"串行",
-"乙太網"=>"以太网",
-"點陣圖"=>"位图",
-"常式"=>"例程",
-"通道"=>"信道",
-"游標"=>"光标",
-"光碟"=>"光盘",
-"光碟機"=>"光驱",
-"全形"=>"全角",
-"共用"=>"共享",
-"相容"=>"兼容",
-"首碼"=>"前缀",
-"尾碼"=>"后缀",
-"載入"=>"加载",
-"半形"=>"半角",
-"變數"=>"变量",
-"雜訊"=>"噪声",
-"因數"=>"因子",
-"線上"=>"在线",
-"離線"=>"脱机",
-"功能變數名稱"=>"域名",
-"音效卡"=>"声卡",
-"字型大小"=>"字号",
-"字型檔"=>"字库",
-"欄位"=>"字段",
-"字元"=>"字符",
-"存檔"=>"存盘",
-"定址"=>"寻址",
-"章節附註"=>"尾注",
-"非同步"=>"异步",
-"匯流排"=>"总线",
-"括弧"=>"括号",
-"介面"=>"接口",
-"控制項"=>"控件",
-"許可權"=>"权限",
-"碟片"=>"盘片",
-"矽片"=>"硅片",
-"矽谷"=>"硅谷",
-"硬碟"=>"硬盘",
-"磁碟"=>"磁盘",
-"磁軌"=>"磁道",
-"程式控制"=>"程控",
-"埠"=>"端口",
-"運算元"=>"算子",
-"演算法"=>"算法",
-"晶片"=>"芯片",
-"晶元"=>"芯片",
-"片語"=>"词组",
-"解碼"=>"译码",
-"軟碟機"=>"软驱",
-"快閃記憶體"=>"闪存",
-"滑鼠"=>"鼠标",
-"進位"=>"进制",
-"互動式"=>"交互式",
-"模擬"=>"仿真",
-"優先順序"=>"优先级",
-"感測"=>"传感",
-"攜帶型"=>"便携式",
-"資訊理論"=>"信息论",
-"迴圈"=>"循环",
-"防寫"=>"写保护",
-"分散式"=>"分布式",
-"解析度"=>"分辨率",
-"程式"=>"程序",
-"伺服器"=>"服务器",
-"等於"=>"等于",
-"區域網"=>"局域网",
-"上傳"=>"上载",
-"電腦"=>"计算机",
-"巨集"=>"宏",
-"掃瞄器"=>"扫瞄仪",
-"寬頻"=>"宽带",
-"視窗"=>"窗口",
-"資料庫"=>"数据库",
-"西曆"=>"公历",
-"乳酪"=>"奶酪",
-"鉅賈"=>"巨商",
-"手電筒"=>"手电",
-"萬曆"=>"万历",
-"永曆"=>"永历",
-"辭彙"=>"词汇",
-"保全"=>"保安",
-"慣用"=>"习用",
-"母音"=>"元音",
-"自由球"=>"任意球",
-"頭槌"=>"头球",
-"進球"=>"入球",
-"顆進球"=>"粒入球",
-"射門"=>"打门",
-"蓋火鍋"=>"火锅盖帽",
-"印表機"=>"打印机",
-"打印機"=>"打印机",
-"位元組"=>"字节",
-"字節"=>"字节",
-"列印"=>"打印",
-"打印"=>"打印",
-"硬體"=>"硬件",
-"二極體"=>"二极管",
-"二極管"=>"二极管",
-"三極體"=>"三极管",
-"三極管"=>"三极管",
-"數位"=>"数码",
-"數碼"=>"数码",
-"軟體"=>"软件",
-"軟件"=>"软件",
-"網路"=>"网络",
-"網絡"=>"网络",
-"人工智慧"=>"人工智能",
-"太空梭"=>"航天飞机",
-"穿梭機"=>"航天飞机",
-"網際網路"=>"因特网",
-"互聯網"=>"因特网",
-"機械人"=>"机器人",
-"機器人"=>"机器人",
-"行動電話"=>"移动电话",
-"流動電話"=>"移动电话",
-"調制解調器"=>"调制解调器",
-"數據機"=>"调制解调器",
-"短訊"=>"短信",
-"簡訊"=>"短信",
-"烏茲別克"=>"乌兹别克斯坦",
-"查德"=>"乍得",
-"乍得"=>"乍得",
-"也門"=>"",
-"葉門"=>"也门",
-"伯利茲"=>"伯利兹",
-"貝里斯"=>"伯利兹",
-"維德角"=>"佛得角",
-"佛得角"=>"佛得角",
-"克羅地亞"=>"克罗地亚",
-"克羅埃西亞"=>"克罗地亚",
-"岡比亞"=>"冈比亚",
-"甘比亞"=>"冈比亚",
-"幾內亞比紹"=>"几内亚比绍",
-"幾內亞比索"=>"几内亚比绍",
-"列支敦斯登"=>"列支敦士登",
-"列支敦士登"=>"列支敦士登",
-"利比里亞"=>"利比里亚",
-"賴比瑞亞"=>"利比里亚",
-"加納"=>"加纳",
-"迦納"=>"加纳",
-"加彭"=>"加蓬",
-"加蓬"=>"加蓬",
-"博茨瓦納"=>"博茨瓦纳",
-"波札那"=>"博茨瓦纳",
-"卡塔爾"=>"卡塔尔",
-"卡達"=>"卡塔尔",
-"盧旺達"=>"卢旺达",
-"盧安達"=>"卢旺达",
-"危地馬拉"=>"危地马拉",
-"瓜地馬拉"=>"危地马拉",
-"厄瓜多爾"=>"厄瓜多尔",
-"厄瓜多"=>"厄瓜多尔",
-"厄立特里亞"=>"厄立特里亚",
-"厄利垂亞"=>"厄立特里亚",
-"吉布堤"=>"吉布提",
-"吉布地"=>"吉布提",
-"哈薩克"=>"哈萨克斯坦",
-"哥斯達黎加"=>"哥斯达黎加",
-"哥斯大黎加"=>"哥斯达黎加",
-"圖瓦盧"=>"图瓦卢",
-"吐瓦魯"=>"图瓦卢",
-"土庫曼"=>"土库曼斯坦",
-"聖盧西亞"=>"圣卢西亚",
-"聖露西亞"=>"圣卢西亚",
-"聖吉斯納域斯"=>"圣基茨和尼维斯",
-"聖克里斯多福及尼維斯"=>"圣基茨和尼维斯",
-"聖文森特和格林納丁斯"=>"圣文森特和格林纳丁斯",
-"聖文森及格瑞那丁"=>"圣文森特和格林纳丁斯",
-"聖馬力諾"=>"圣马力诺",
-"聖馬利諾"=>"圣马力诺",
-"圭亞那"=>"圭亚那",
-"蓋亞那"=>"圭亚那",
-"坦桑尼亞"=>"坦桑尼亚",
-"坦尚尼亞"=>"坦桑尼亚",
-"埃塞俄比亞"=>"埃塞俄比亚",
-"衣索比亞"=>"埃塞俄比亚",
-"吉里巴斯"=>"基里巴斯",
-"基里巴斯"=>"基里巴斯",
-"塔吉克"=>"塔吉克斯坦",
-"獅子山"=>"塞拉利昂",
-"塞拉利昂"=>"塞拉利昂",
-"塞普勒斯"=>"塞浦路斯",
-"塞浦路斯"=>"塞浦路斯",
-"塞舌爾"=>"塞舌尔",
-"塞席爾"=>"塞舌尔",
-"多明尼加共和國"=>"多米尼加",
-"多明尼加"=>"多米尼加",
-"多明尼加聯邦"=>"多米尼加联邦",
-"多米尼克"=>"多米尼加联邦",
-"安提瓜和巴布達"=>"安提瓜和巴布达",
-"安地卡及巴布達"=>"安提瓜和巴布达",
-"尼日利亞"=>"尼日利亚",
-"奈及利亞"=>"尼日利亚",
-"尼日爾"=>"尼日尔",
-"尼日"=>"尼日尔",
-"巴貝多"=>"巴巴多斯",
-"巴巴多斯"=>"巴巴多斯",
-"巴布亞新畿內亞"=>"巴布亚新几内亚",
-"巴布亞紐幾內亞"=>"巴布亚新几内亚",
-"布基納法索"=>"布基纳法索",
-"布吉納法索"=>"布基纳法索",
-"蒲隆地"=>"布隆迪",
-"布隆迪"=>"布隆迪",
-"希臘"=>"希腊",
-"帛琉"=>"帕劳",
-"義大利"=>"意大利",
-"意大利"=>"意大利",
-"所羅門群島"=>"所罗门群岛",
-"索羅門群島"=>"所罗门群岛",
-"汶萊"=>"文莱",
-"斯威士蘭"=>"斯威士兰",
-"史瓦濟蘭"=>"斯威士兰",
-"斯洛文尼亞"=>"斯洛文尼亚",
-"斯洛維尼亞"=>"斯洛文尼亚",
-"新西蘭"=>"新西兰",
-"紐西蘭"=>"新西兰",
-"北韓"=>"朝鲜",
-"格林納達"=>"格林纳达",
-"格瑞那達"=>"格林纳达",
-"格魯吉亞"=>"格鲁吉亚",
-"喬治亞"=>"格鲁吉亚",
-"梵蒂岡"=>"梵蒂冈",
-"教廷"=>"梵蒂冈",
-"毛里塔尼亞"=>"毛里塔尼亚",
-"茅利塔尼亞"=>"毛里塔尼亚",
-"毛里裘斯"=>"毛里求斯",
-"模里西斯"=>"毛里求斯",
-"沙地阿拉伯"=>"沙特阿拉伯",
-"沙烏地阿拉伯"=>"沙特阿拉伯",
-"波斯尼亞黑塞哥維那"=>"波斯尼亚和黑塞哥维那",
-"波士尼亞赫塞哥維納"=>"波斯尼亚和黑塞哥维那",
-"津巴布韋"=>"津巴布韦",
-"辛巴威"=>"津巴布韦",
-"宏都拉斯"=>"洪都拉斯",
-"洪都拉斯"=>"洪都拉斯",
-"特立尼達和多巴哥"=>"特立尼达和托巴哥",
-"千里達托貝哥"=>"特立尼达和托巴哥",
-"瑙魯"=>"瑙鲁",
-"諾魯"=>"瑙鲁",
-"瓦努阿圖"=>"瓦努阿图",
-"萬那杜"=>"瓦努阿图",
-"溫納圖"=>"瓦努阿图",
-"科摩羅"=>"科摩罗",
-"葛摩"=>"科摩罗",
-"象牙海岸"=>"科特迪瓦",
-"突尼西亞"=>"突尼斯",
-"索馬里"=>"索马里",
-"索馬利亞"=>"索马里",
-"老撾"=>"老挝",
-"寮國"=>"老挝",
-"肯雅"=>"肯尼亚",
-"肯亞"=>"肯尼亚",
-"蘇利南"=>"苏里南",
-"莫三比克"=>"莫桑比克",
-"莫桑比克"=>"莫桑比克",
-"萊索托"=>"莱索托",
-"賴索托"=>"莱索托",
-"貝寧"=>"贝宁",
-"貝南"=>"贝宁",
-"贊比亞"=>"赞比亚",
-"尚比亞"=>"赞比亚",
-"亞塞拜然"=>"阿塞拜疆",
-"阿塞拜疆"=>"阿塞拜疆",
-"阿拉伯聯合酋長國"=>"阿拉伯联合酋长国",
-"阿拉伯聯合大公國"=>"阿拉伯联合酋长国",
-"南韓"=>"韩国",
-"馬爾代夫"=>"马尔代夫",
-"馬爾地夫"=>"马尔代夫",
-"馬爾他"=>"马耳他",
-"馬里"=>"马里",
-"馬利"=>"马里",
-"即食麵"=>"方便面",
-"快速面"=>"方便面",
-"速食麵"=>"方便面",
-"泡麵"=>"方便面",
-"笨豬跳"=>"蹦极跳",
-"绑紧跳"=>"蹦极跳",
-"冷盤  "=>"凉菜",
-"冷菜"=>"凉菜",
-"散钱"=>"零钱",
-"谐星"=>"笑星    ",
-"夜学"=>"夜校",
-"华乐"=>"民乐",
-"中樂"=>"民乐",
-"住屋"=>"住房",
-"屋价"=>"房价",
-"的士"=>"出租车",
-"計程車"=>"出租车",
-"巴士"=>"公共汽车",
-"公車"=>"公共汽车",
-"單車"=>"自行车",
-"節慶"=>"节日",
-"芝士"=>"乾酪",
-"狗隻"=>"犬只",
-"士多啤梨"=>"草莓",
-"忌廉"=>"奶油",
-"桌球"=>"台球",
-"撞球"=>"台球",
-"雪糕"=>"冰淇淋",
-"衞生"=>"卫生",
-"衛生"=>"卫生",
-"賓士"=>"奔驰",
-"平治"=>"奔驰",
-"捷豹"=>"美洲虎",
-"積架"=>"美洲虎",
-"福斯"=>"大众",
-"福士"=>"大众",
-"雪鐵龍"=>"雪铁龙",
-"萬事得"=>"马自达",
-"馬自達"=>"马自达",
-"寶獅"=>"标志",
-"布殊"=>"布什",
-"布希"=>"布什",
-"柯林頓"=>"克林顿",
-"克林頓"=>"克林顿",
-"薩達姆"=>"萨达姆",
-"海珊"=>"萨达姆",
-"梵谷"=>"凡高",
-"大衛碧咸"=>"大卫·贝克汉姆",
-"米高奧雲"=>"迈克尔·欧文",
-"卡佩雅蒂"=>"珍妮弗·卡普里亚蒂",
-"沙芬"=>"马拉特·萨芬",
-"舒麥加"=>"迈克尔·舒马赫",
-"希特拉"=>"希特勒",
-"戴安娜"=>"狄安娜",
-"黛安娜"=>"狄安娜",
-"希拉"=>"赫拉",
);
-$zh2HK=array(
+$zh2TW = array(
+"缺省" => "預設",
+"串行" => "串列",
+"以太网" => "乙太網",
+"位图" => "點陣圖",
+"例程" => "常式",
+"信道" => "通道",
+"光标" => "游標",
+"光盘" => "光碟",
+"光驱" => "光碟機",
+"全角" => "全形",
+"加载" => "載入",
+"半角" => "半形",
+"变量" => "變數",
+"噪声" => "雜訊",
+"脱机" => "離線",
+"声卡" => "音效卡",
+"老字号" => "老字號",
+"字号" => "字型大小",
+"字库" => "字型檔",
+"字段" => "欄位",
+"字符" => "字元",
+"存盘" => "存檔",
+"寻址" => "定址",
+"尾注" => "章節附註",
+"异步" => "非同步",
+"总线" => "匯流排",
+"括号" => "括弧",
+"接口" => "介面",
+"控件" => "控制項",
+"权限" => "許可權",
+"盘片" => "碟片",
+"硅片" => "矽片",
+"硅谷" => "矽谷",
+"硬盘" => "硬碟",
+"磁盘" => "磁碟",
+"磁道" => "磁軌",
+"程控" => "程式控制",
+"端口" => "埠",
+"算子" => "運算元",
+"算法" => "演算法",
+"芯片" => "晶片",
+"芯片" => "晶元",
+"词组" => "片語",
+"译码" => "解碼",
+"软驱" => "軟碟機",
+"快闪存储器" => "快閃記憶體",
+"闪存" => "快閃記憶體",
+"鼠标" => "滑鼠",
+"进制" => "進位",
+"交互式" => "互動式",
+"仿真" => "模擬",
+"优先级" => "優先順序",
+"传感" => "感測",
+"便携式" => "攜帶型",
+"信息论" => "資訊理論",
+"写保护" => "防寫",
+"分布式" => "分散式",
+"分辨率" => "解析度",
+"服务器" => "伺服器",
+"等于" => "等於",
+"局域网" => "區域網",
+"计算机" => "電腦",
+"扫瞄仪" => "掃瞄器",
+"宽带" => "寬頻",
+"数据库" => "資料庫",
+"奶酪" => "乳酪",
+"巨商" => "鉅賈",
+"手电" => "手電筒",
+"万历" => "萬曆",
+"永历" => "永曆",
+"词汇" => "辭彙",
+"习用" => "慣用",
+"元音" => "母音",
+"任意球" => "自由球",
+"头球" => "頭槌",
+"入球" => "進球",
+"粒入球" => "顆進球",
+"打门" => "射門",
+"火锅盖帽" => "蓋火鍋",
+"打印机" => "印表機",
+"打印機" => "印表機",
+"字节" => "位元組",
+"字節" => "位元組",
+"打印" => "列印",
+"打印" => "列印",
+"硬件" => "硬體",
+"硬件" => "硬體",
+"二极管" => "二極體",
+"二極管" => "二極體",
+"三极管" => "三極體",
+"三極管" => "三極體",
+"软件" => "軟體",
+"軟件" => "軟體",
+"网络" => "網路",
+"網絡" => "網路",
+"人工智能" => "人工智慧",
+"航天飞机" => "太空梭",
+"穿梭機" => "太空梭",
+"因特网" => "網際網路",
+"互聯網" => "網際網路",
+"机器人" => "機器人",
+"機械人" => "機器人",
+"移动电话" => "行動電話",
+"流動電話" => "行動電話",
+"调制解调器" => "數據機",
+"調制解調器" => "數據機",
+"短信" => "簡訊",
+"短訊" => "簡訊",
+"乌兹别克斯坦" => "烏茲別克",
+"乍得" => "查德",
+"乍得" => "查德",
+"也门" => "葉門",
+"也門" => "葉門",
+"伯利兹" => "貝里斯",
+"伯利茲" => "貝里斯",
+"佛得角" => "維德角",
+"佛得角" => "維德角",
+"克罗地亚" => "克羅埃西亞",
+"克羅地亞" => "克羅埃西亞",
+"冈比亚" => "甘比亞",
+"岡比亞" => "甘比亞",
+"几内亚比绍" => "幾內亞比索",
+"幾內亞比紹" => "幾內亞比索",
+"列支敦士登" => "列支敦斯登",
+"列支敦士登" => "列支敦斯登",
+"利比里亚" => "賴比瑞亞",
+"利比里亞" => "賴比瑞亞",
+"加纳" => "迦納",
+"加納" => "迦納",
+"加蓬" => "加彭",
+"加蓬" => "加彭",
+"博茨瓦纳" => "波札那",
+"博茨瓦納" => "波札那",
+"卡塔尔" => "卡達",
+"卡塔爾" => "卡達",
+"卢旺达" => "盧安達",
+"盧旺達" => "盧安達",
+"危地马拉" => "瓜地馬拉",
+"危地馬拉" => "瓜地馬拉",
+"厄瓜多尔" => "厄瓜多",
+"厄瓜多爾" => "厄瓜多",
+"厄立特里亚" => "厄利垂亞",
+"厄立特里亞" => "厄利垂亞",
+"吉布提" => "吉布地",
+"吉布堤" => "吉布地",
+"哈萨克斯坦" => "哈薩克",
+"哥斯达黎加" => "哥斯大黎加",
+"哥斯達黎加" => "哥斯大黎加",
+"图瓦卢" => "吐瓦魯",
+"圖瓦盧" => "吐瓦魯",
+"土库曼斯坦" => "土庫曼",
+"圣卢西亚" => "聖露西亞",
+"聖盧西亞" => "聖露西亞",
+"圣基茨和尼维斯" => "聖克里斯多福及尼維斯",
+"聖吉斯納域斯" => "聖克里斯多福及尼維斯",
+"圣文森特和格林纳丁斯" => "聖文森及格瑞那丁",
+"聖文森特和格林納丁斯" => "聖文森及格瑞那丁",
+"圣马力诺" => "聖馬利諾",
+"聖馬力諾" => "聖馬利諾",
+"圭亚那" => "蓋亞那",
+"圭亞那" => "蓋亞那",
+"坦桑尼亚" => "坦尚尼亞",
+"坦桑尼亞" => "坦尚尼亞",
+"埃塞俄比亚" => "衣索比亞",
+"埃塞俄比亞" => "衣索比亞",
+"基里巴斯" => "吉里巴斯",
+"基里巴斯" => "吉里巴斯",
+"塔吉克斯坦" => "塔吉克",
+"塞拉利昂" => "獅子山",
+"塞拉利昂" => "獅子山",
+"塞浦路斯" => "塞普勒斯",
+"塞浦路斯" => "塞普勒斯",
+"塞舌尔" => "塞席爾",
+"塞舌爾" => "塞席爾",
+"多米尼加" => "多明尼加",
+"多明尼加共和國" => "多明尼加",
+"多米尼加联邦" => "多米尼克",
+"多明尼加聯邦" => "多米尼克",
+"安提瓜和巴布达" => "安地卡及巴布達",
+"安提瓜和巴布達" => "安地卡及巴布達",
+"尼日利亚" => "奈及利亞",
+"尼日利亞" => "奈及利亞",
+"尼日尔" => "尼日",
+"尼日爾" => "尼日",
+"巴巴多斯" => "巴貝多",
+"巴巴多斯" => "巴貝多",
+"巴布亚新几内亚" => "巴布亞紐幾內亞",
+"巴布亞新畿內亞" => "巴布亞紐幾內亞",
+"布基纳法索" => "布吉納法索",
+"布基納法索" => "布吉納法索",
+"布隆迪" => "蒲隆地",
+"布隆迪" => "蒲隆地",
+"希腊" => "希臘",
+"帕劳" => "帛琉",
+"意大利" => "義大利",
+"意大利" => "義大利",
+"所罗门群岛" => "索羅門群島",
+"所羅門群島" => "索羅門群島",
+"文莱" => "汶萊",
+"斯威士兰" => "史瓦濟蘭",
+"斯威士蘭" => "史瓦濟蘭",
+"斯洛文尼亚" => "斯洛維尼亞",
+"斯洛文尼亞" => "斯洛維尼亞",
+"新西兰" => "紐西蘭",
+"新西蘭" => "紐西蘭",
+"格林纳达" => "格瑞那達",
+"格林納達" => "格瑞那達",
+"格鲁吉亚" => "喬治亞",
+"格魯吉亞" => "喬治亞",
+"佐治亚" => "喬治亞",
+"佐治亞" => "喬治亞",
+"毛里塔尼亚" => "茅利塔尼亞",
+"毛里塔尼亞" => "茅利塔尼亞",
+"毛里求斯" => "模里西斯",
+"毛里裘斯" => "模里西斯",
+"沙特阿拉伯" => "沙烏地阿拉伯",
+"沙地阿拉伯" => "沙烏地阿拉伯",
+"波斯尼亚和黑塞哥维那" => "波士尼亞赫塞哥維納",
+"波斯尼亞黑塞哥維那" => "波士尼亞赫塞哥維納",
+"津巴布韦" => "辛巴威",
+"津巴布韋" => "辛巴威",
+"洪都拉斯" => "宏都拉斯",
+"洪都拉斯" => "宏都拉斯",
+"特立尼达和托巴哥" => "千里達托貝哥",
+"特立尼達和多巴哥" => "千里達托貝哥",
+"瑙鲁" => "諾魯",
+"瑙魯" => "諾魯",
+"瓦努阿图" => "萬那杜",
+"瓦努阿圖" => "萬那杜",
+"溫納圖萬" => "那杜",
+"科摩罗" => "葛摩",
+"科摩羅" => "葛摩",
+"科特迪瓦" => "象牙海岸",
+"突尼斯" => "突尼西亞",
+"索马里" => "索馬利亞",
+"索馬里" => "索馬利亞",
+"老挝" => "寮國",
+"老撾" => "寮國",
+"肯尼亚" => "肯亞",
+"肯雅" => "肯亞",
+"苏里南" => "蘇利南",
+"莫桑比克" => "莫三比克",
+"莱索托" => "賴索托",
+"萊索托" => "賴索托",
+"贝宁" => "貝南",
+"貝寧" => "貝南",
+"赞比亚" => "尚比亞",
+"贊比亞" => "尚比亞",
+"阿塞拜疆" => "亞塞拜然",
+"阿塞拜疆" => "亞塞拜然",
+"阿拉伯联合酋长国" => "阿拉伯聯合大公國",
+"阿拉伯聯合酋長國" => "阿拉伯聯合大公國",
+"马尔代夫" => "馬爾地夫",
+"馬爾代夫" => "馬爾地夫",
+"马耳他" => "馬爾他",
+"马里共和国" => "馬利共和國",
+"馬里共和國" => "馬利共和國",
+"方便面" => "速食麵",
+"快速面" => "速食麵",
+"即食麵" => "速食麵",
+"薯仔" => "土豆",
+"蹦极跳" => "笨豬跳",
+"绑紧跳" => "笨豬跳",
+"冷菜" => "冷盤",
+"凉菜" => "冷盤",
+"出租车" => "計程車",
+"台球" => "撞球",
+"桌球" => "撞球",
+"雪糕" => "冰淇淋",
+"卫生" => "衛生",
+"衞生" => "衛生",
+"平治" => "賓士",
+"奔驰" => "賓士",
+"積架" => "捷豹",
+"福士" => "福斯",
+"雪铁龙" => "雪鐵龍",
+"马自达" => "馬自達",
+"萬事得" => "馬自達",
+"拿破仑" => "拿破崙",
+"拿破侖" => "拿破崙",
+"布什" => "布希",
+"布殊" => "布希",
+"克林顿" => "柯林頓",
+"克林頓" => "柯林頓",
+"侯赛因" => "海珊",
+"侯賽因" => "海珊",
+"凡高" => "梵谷",
+"狄安娜" => "黛安娜",
+"戴安娜" => "黛安娜",
+"赫拉" => "希拉",
+);
+
+$zh2HK = array(
"打印机" => "打印機",
"印表機" => "打印機",
-"字节" => "字節",
-"位元組" => "字節",
+"字节" => "位元組",
+"字節" => "位元組",
"打印" => "打印",
"列印" => "打印",
"硬件" => "硬件",
@@ -8308,10 +7830,11 @@ $zh2HK=array(
"坦桑尼亚" => "坦桑尼亞",
"坦尚尼亞" => "坦桑尼亞",
"埃塞俄比亚" => "埃塞俄比亞",
+"衣索匹亞" => "埃塞俄比亞",
"衣索比亞" => "埃塞俄比亞",
"基里巴斯" => "基里巴斯",
"吉里巴斯" => "基里巴斯",
-"獅子山" => "塞拉利昂",
+"狮子山" => "獅子山",
"塞普勒斯" => "塞浦路斯",
"塞舌尔" => "塞舌爾",
"塞席爾" => "塞舌爾",
@@ -8344,16 +7867,14 @@ $zh2HK=array(
"紐西蘭" => "新西蘭",
"格林纳达" => "格林納達",
"格瑞那達" => "格林納達",
-"格鲁吉亚" => "格魯吉亞",
-"喬治亞" => "格魯吉亞",
+"格鲁吉亚" => "喬治亞",
+"格魯吉亞" => "喬治亞",
"梵蒂冈" => "梵蒂岡",
-"教廷" => "梵蒂岡",
"毛里塔尼亚" => "毛里塔尼亞",
"茅利塔尼亞" => "毛里塔尼亞",
"毛里求斯" => "毛里裘斯",
"模里西斯" => "毛里裘斯",
-"沙特阿拉伯" => "沙地阿拉伯",
-"沙烏地阿拉伯" => "沙地阿拉伯",
+"沙烏地阿拉伯" => "沙特阿拉伯",
"波斯尼亚和黑塞哥维那" => "波斯尼亞黑塞哥維那",
"波士尼亞赫塞哥維納" => "波斯尼亞黑塞哥維那",
"津巴布韦" => "津巴布韋",
@@ -8388,29 +7909,22 @@ $zh2HK=array(
"阿拉伯聯合大公國" => "阿拉伯聯合酋長國",
"马尔代夫" => "馬爾代夫",
"馬爾地夫" => "馬爾代夫",
-"马里" => "馬里",
-"馬利" => "馬里",
+"馬利共和國" => "馬里共和國",
"方便面" => "即食麵",
"快速面" => "即食麵",
"速食麵" => "即食麵",
"泡麵" => "即食麵",
-"土豆" => "薯仔",
+"土豆" => "馬鈴薯",
"华乐" => "中樂",
"民乐" => "中樂",
-"計程車 " => "的士",
+"計程車" => "的士",
"出租车" => "的士",
"公車" => "巴士",
-"公共汽车" => "巴士",
"自行车" => "單車",
-"节日" => "節慶",
"犬只" => "狗隻",
"台球" => "桌球",
"撞球" => "桌球",
"冰淇淋" => "雪糕",
-"冰淇淋" => "雪糕",
-"卫生" => "衞生",
-"衛生" => "衞生",
-"老人" => "長者",
"賓士" => "平治",
"捷豹" => "積架",
"福斯" => "福士",
@@ -8420,12 +7934,14 @@ $zh2HK=array(
"马自达" => "萬事得",
"馬自達" => "萬事得",
"寶獅" => "標致",
+"拿破崙" => "拿破侖",
"布什" => "布殊",
"布希" => "布殊",
"克林顿" => "克林頓",
"柯林頓" => "克林頓",
"萨达姆" => "薩達姆",
-"海珊" => "薩達姆",
+"海珊" => "侯賽因",
+"侯赛因" => "侯賽因",
"大卫·贝克汉姆" => "大衛碧咸",
"迈克尔·欧文" => "米高奧雲",
"珍妮弗·卡普里亚蒂" => "卡佩雅蒂",
@@ -8436,7 +7952,318 @@ $zh2HK=array(
"黛安娜" => "戴安娜",
);
-$zh2SG=array(
+$zh2CN = array(
+"記憶體" => "内存",
+"預設" => "默认",
+"串列" => "串行",
+"乙太網" => "以太网",
+"點陣圖" => "位图",
+"常式" => "例程",
+"游標" => "光标",
+"光碟" => "光盘",
+"光碟機" => "光驱",
+"全形" => "全角",
+"共用" => "共享",
+"載入" => "加载",
+"半形" => "半角",
+"變數" => "变量",
+"雜訊" => "噪声",
+"因數" => "因子",
+"功能變數名稱" => "域名",
+"音效卡" => "声卡",
+"字型大小" => "字号",
+"字型檔" => "字库",
+"欄位" => "字段",
+"字元" => "字符",
+"存檔" => "存盘",
+"定址" => "寻址",
+"章節附註" => "尾注",
+"非同步" => "异步",
+"匯流排" => "总线",
+"括弧" => "括号",
+"介面" => "接口",
+"控制項" => "控件",
+"許可權" => "权限",
+"碟片" => "盘片",
+"矽片" => "硅片",
+"矽谷" => "硅谷",
+"硬碟" => "硬盘",
+"磁碟" => "磁盘",
+"磁軌" => "磁道",
+"程式控制" => "程控",
+"運算元" => "算子",
+"演算法" => "算法",
+"晶片" => "芯片",
+"晶元" => "芯片",
+"片語" => "词组",
+"軟碟機" => "软驱",
+"快閃記憶體" => "快闪存储器",
+"滑鼠" => "鼠标",
+"進位" => "进制",
+"互動式" => "交互式",
+"優先順序" => "优先级",
+"感測" => "传感",
+"攜帶型" => "便携式",
+"資訊理論" => "信息论",
+"迴圈" => "循环",
+"防寫" => "写保护",
+"分散式" => "分布式",
+"解析度" => "分辨率",
+"伺服器" => "服务器",
+"等於" => "等于",
+"區域網" => "局域网",
+"巨集" => "宏",
+"掃瞄器" => "扫瞄仪",
+"寬頻" => "宽带",
+"資料庫" => "数据库",
+"乳酪" => "奶酪",
+"鉅賈" => "巨商",
+"手電筒" => "手电",
+"萬曆" => "万历",
+"永曆" => "永历",
+"辭彙" => "词汇",
+"母音" => "元音",
+"自由球" => "任意球",
+"頭槌" => "头球",
+"進球" => "入球",
+"顆進球" => "粒入球",
+"射門" => "打门",
+"蓋火鍋" => "火锅盖帽",
+"印表機" => "打印机",
+"打印機" => "打印机",
+"位元組" => "字节",
+"字節" => "字节",
+"列印" => "打印",
+"打印" => "打印",
+"硬體" => "硬件",
+"二極體" => "二极管",
+"二極管" => "二极管",
+"三極體" => "三极管",
+"三極管" => "三极管",
+"數位" => "数码",
+"數碼" => "数码",
+"軟體" => "软件",
+"軟件" => "软件",
+"網路" => "网络",
+"網絡" => "网络",
+"人工智慧" => "人工智能",
+"太空梭" => "航天飞机",
+"穿梭機" => "航天飞机",
+"網際網路" => "因特网",
+"互聯網" => "因特网",
+"機械人" => "机器人",
+"機器人" => "机器人",
+"行動電話" => "移动电话",
+"流動電話" => "移动电话",
+"調制解調器" => "调制解调器",
+"數據機" => "调制解调器",
+"短訊" => "短信",
+"簡訊" => "短信",
+"烏茲別克" => "乌兹别克斯坦",
+"查德" => "乍得",
+"乍得" => "乍得",
+"也門" => "",
+"葉門" => "也门",
+"伯利茲" => "伯利兹",
+"貝里斯" => "伯利兹",
+"維德角" => "佛得角",
+"佛得角" => "佛得角",
+"克羅地亞" => "克罗地亚",
+"克羅埃西亞" => "克罗地亚",
+"岡比亞" => "冈比亚",
+"甘比亞" => "冈比亚",
+"幾內亞比紹" => "几内亚比绍",
+"幾內亞比索" => "几内亚比绍",
+"列支敦斯登" => "列支敦士登",
+"列支敦士登" => "列支敦士登",
+"利比里亞" => "利比里亚",
+"賴比瑞亞" => "利比里亚",
+"加納" => "加纳",
+"迦納" => "加纳",
+"加彭" => "加蓬",
+"加蓬" => "加蓬",
+"博茨瓦納" => "博茨瓦纳",
+"波札那" => "博茨瓦纳",
+"卡塔爾" => "卡塔尔",
+"卡達" => "卡塔尔",
+"盧旺達" => "卢旺达",
+"盧安達" => "卢旺达",
+"危地馬拉" => "危地马拉",
+"瓜地馬拉" => "危地马拉",
+"厄瓜多爾" => "厄瓜多尔",
+"厄瓜多" => "厄瓜多尔",
+"厄立特里亞" => "厄立特里亚",
+"厄利垂亞" => "厄立特里亚",
+"吉布堤" => "吉布提",
+"吉布地" => "吉布提",
+"哈薩克" => "哈萨克斯坦",
+"哥斯達黎加" => "哥斯达黎加",
+"哥斯大黎加" => "哥斯达黎加",
+"圖瓦盧" => "图瓦卢",
+"吐瓦魯" => "图瓦卢",
+"土庫曼" => "土库曼斯坦",
+"聖盧西亞" => "圣卢西亚",
+"聖露西亞" => "圣卢西亚",
+"聖吉斯納域斯" => "圣基茨和尼维斯",
+"聖克里斯多福及尼維斯" => "圣基茨和尼维斯",
+"聖文森特和格林納丁斯" => "圣文森特和格林纳丁斯",
+"聖文森及格瑞那丁" => "圣文森特和格林纳丁斯",
+"聖馬力諾" => "圣马力诺",
+"聖馬利諾" => "圣马力诺",
+"圭亞那" => "圭亚那",
+"蓋亞那" => "圭亚那",
+"坦桑尼亞" => "坦桑尼亚",
+"坦尚尼亞" => "坦桑尼亚",
+"埃塞俄比亞" => "埃塞俄比亚",
+"衣索匹亞" => "埃塞俄比亚",
+"衣索比亞" => "埃塞俄比亚",
+"吉里巴斯" => "基里巴斯",
+"基里巴斯" => "基里巴斯",
+"塔吉克" => "塔吉克斯坦",
+"塞拉利昂" => "塞拉利昂",
+"塞普勒斯" => "塞浦路斯",
+"塞浦路斯" => "塞浦路斯",
+"塞舌爾" => "塞舌尔",
+"塞席爾" => "塞舌尔",
+"多明尼加共和國" => "多米尼加",
+"多明尼加" => "多米尼加",
+"多明尼加聯邦" => "多米尼加联邦",
+"多米尼克" => "多米尼加联邦",
+"安提瓜和巴布達" => "安提瓜和巴布达",
+"安地卡及巴布達" => "安提瓜和巴布达",
+"尼日利亞" => "尼日利亚",
+"奈及利亞" => "尼日利亚",
+"尼日爾" => "尼日尔",
+"尼日" => "尼日尔",
+"巴貝多" => "巴巴多斯",
+"巴巴多斯" => "巴巴多斯",
+"巴布亞新畿內亞" => "巴布亚新几内亚",
+"巴布亞紐幾內亞" => "巴布亚新几内亚",
+"布基納法索" => "布基纳法索",
+"布吉納法索" => "布基纳法索",
+"蒲隆地" => "布隆迪",
+"布隆迪" => "布隆迪",
+"希臘" => "希腊",
+"帛琉" => "帕劳",
+"義大利" => "意大利",
+"意大利" => "意大利",
+"所羅門群島" => "所罗门群岛",
+"索羅門群島" => "所罗门群岛",
+"汶萊" => "文莱",
+"斯威士蘭" => "斯威士兰",
+"史瓦濟蘭" => "斯威士兰",
+"斯洛文尼亞" => "斯洛文尼亚",
+"斯洛維尼亞" => "斯洛文尼亚",
+"新西蘭" => "新西兰",
+"紐西蘭" => "新西兰",
+"格林納達" => "格林纳达",
+"格瑞那達" => "格林纳达",
+"格魯吉亞" => "乔治亚",
+"喬治亞" => "乔治亚",
+"梵蒂岡" => "梵蒂冈",
+"毛里塔尼亞" => "毛里塔尼亚",
+"茅利塔尼亞" => "毛里塔尼亚",
+"毛里裘斯" => "毛里求斯",
+"模里西斯" => "毛里求斯",
+"沙地阿拉伯" => "沙特阿拉伯",
+"沙烏地阿拉伯" => "沙特阿拉伯",
+"波斯尼亞黑塞哥維那" => "波斯尼亚和黑塞哥维那",
+"波士尼亞赫塞哥維納" => "波斯尼亚和黑塞哥维那",
+"津巴布韋" => "津巴布韦",
+"辛巴威" => "津巴布韦",
+"宏都拉斯" => "洪都拉斯",
+"洪都拉斯" => "洪都拉斯",
+"特立尼達和多巴哥" => "特立尼达和托巴哥",
+"千里達托貝哥" => "特立尼达和托巴哥",
+"瑙魯" => "瑙鲁",
+"諾魯" => "瑙鲁",
+"瓦努阿圖" => "瓦努阿图",
+"萬那杜" => "瓦努阿图",
+"溫納圖" => "瓦努阿图",
+"科摩羅" => "科摩罗",
+"葛摩" => "科摩罗",
+"象牙海岸" => "科特迪瓦",
+"突尼西亞" => "突尼斯",
+"索馬里" => "索马里",
+"索馬利亞" => "索马里",
+"老撾" => "老挝",
+"寮國" => "老挝",
+"肯雅" => "肯尼亚",
+"肯亞" => "肯尼亚",
+"蘇利南" => "苏里南",
+"莫三比克" => "莫桑比克",
+"莫桑比克" => "莫桑比克",
+"萊索托" => "莱索托",
+"賴索托" => "莱索托",
+"貝寧" => "贝宁",
+"貝南" => "贝宁",
+"贊比亞" => "赞比亚",
+"尚比亞" => "赞比亚",
+"亞塞拜然" => "阿塞拜疆",
+"阿塞拜疆" => "阿塞拜疆",
+"阿拉伯聯合酋長國" => "阿拉伯联合酋长国",
+"阿拉伯聯合大公國" => "阿拉伯联合酋长国",
+"南韓" => "韩国",
+"馬爾代夫" => "马尔代夫",
+"馬爾地夫" => "马尔代夫",
+"馬爾他" => "马耳他",
+"馬利共和國" => "马里共和国",
+"即食麵" => "方便面",
+"快速面" => "方便面",
+"速食麵" => "方便面",
+"泡麵" => "方便面",
+"笨豬跳" => "蹦极跳",
+"绑紧跳" => "蹦极跳",
+"冷盤  " => "凉菜",
+"冷菜" => "凉菜",
+"散钱" => "零钱",
+"谐星" => "笑星    ",
+"夜学" => "夜校",
+"华乐" => "民乐",
+"中樂" => "民乐",
+"屋价" => "房价",
+"的士" => "出租车",
+"計程車" => "出租车",
+"公車" => "公共汽车",
+"單車" => "自行车",
+"節慶" => "节日",
+"芝士" => "乾酪",
+"狗隻" => "犬只",
+"士多啤梨" => "草莓",
+"忌廉" => "奶油",
+"桌球" => "台球",
+"撞球" => "台球",
+"雪糕" => "冰淇淋",
+"衞生" => "卫生",
+"衛生" => "卫生",
+"賓士" => "奔驰",
+"平治" => "奔驰",
+"積架" => "捷豹",
+"福斯" => "大众",
+"福士" => "大众",
+"雪鐵龍" => "雪铁龙",
+"萬事得" => "马自达",
+"馬自達" => "马自达",
+"寶獅" => "标志",
+"拿破崙" => "拿破仑",
+"布殊" => "布什",
+"布希" => "布什",
+"柯林頓" => "克林顿",
+"克林頓" => "克林顿",
+"薩達姆" => "萨达姆",
+"海珊" => "萨达姆",
+"梵谷" => "凡高",
+"大衛碧咸" => "大卫·贝克汉姆",
+"米高奧雲" => "迈克尔·欧文",
+"卡佩雅蒂" => "珍妮弗·卡普里亚蒂",
+"沙芬" => "马拉特·萨芬",
+"舒麥加" => "迈克尔·舒马赫",
+"希特拉" => "希特勒",
+"黛安娜" => "戴安娜",
+"希拉" => "赫拉",
+);
+
+$zh2SG = array(
"方便面" => "快速面",
"速食麵" => "快速面",
"即食麵" => "快速面",
@@ -8452,4 +8279,4 @@ $zh2SG=array(
"住房" => "住屋",
"房价" => "屋价",
"泡麵" => "快速面",
-);
+); \ No newline at end of file
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index b324c52f..3a7b5099 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -63,12 +63,36 @@ abstract class ApiBase {
$this->mModulePrefix = $modulePrefix;
}
- /**
- * Executes this module
+ /*****************************************************************************
+ * ABSTRACT METHODS *
+ *****************************************************************************/
+
+ /**
+ * Evaluates the parameters, performs the requested query, and sets up the
+ * result. Concrete implementations of ApiBase must override this method to
+ * provide whatever functionality their module offers. Implementations must
+ * not produce any output on their own and are not expected to handle any
+ * errors.
+ *
+ * The execute method will be invoked directly by ApiMain immediately before
+ * the result of the module is output. Aside from the constructor, implementations
+ * should assume that no other methods will be called externally on the module
+ * before the result is processed.
+ *
+ * The result data should be stored in the result object referred to by
+ * "getResult()". Refer to ApiResult.php for details on populating a result
+ * object.
*/
public abstract function execute();
/**
+ * Returns a String that identifies the version of the extending class. Typically
+ * includes the class name, the svn revision, timestamp, and last author. May
+ * be severely incorrect in many implementations!
+ */
+ public abstract function getVersion();
+
+ /**
* Get the name of the module being executed by this instance
*/
public function getModuleName() {
@@ -100,14 +124,16 @@ abstract class ApiBase {
}
/**
- * If this module's $this is the same as $this->mMainModule, its the root, otherwise no
+ * Returns true if this module is the main module ($this === $this->mMainModule),
+ * false otherwise.
*/
public function isMain() {
return $this === $this->mMainModule;
}
/**
- * Get result object
+ * Get the result object. Please refer to the documentation in ApiResult.php
+ * for details on populating and accessing data in a result object.
*/
public function getResult() {
// Main module has getResult() method overriden
@@ -125,7 +151,8 @@ abstract class ApiBase {
}
/**
- * Set warning section for this module. Users should monitor this section to notice any changes in API.
+ * Set warning section for this module. Users should monitor this section to
+ * notice any changes in API.
*/
public function setWarning($warning) {
$msg = array();
@@ -196,6 +223,10 @@ abstract class ApiBase {
return $msg;
}
+ /**
+ * Generates the parameter descriptions for this module, to be displayed in the
+ * module's help.
+ */
public function makeHelpMsgParameters() {
$params = $this->getAllowedParams();
if ($params !== false) {
@@ -208,7 +239,7 @@ abstract class ApiBase {
if (is_array($desc))
$desc = implode($paramPrefix, $desc);
- @ $type = $paramSettings[self :: PARAM_TYPE];
+ $type = $paramSettings[self :: PARAM_TYPE];
if (isset ($type)) {
if (isset ($paramSettings[self :: PARAM_ISMULTI]))
$prompt = 'Values (separate with \'|\'): ';
@@ -303,13 +334,15 @@ abstract class ApiBase {
* Using getAllowedParams(), makes an array of the values provided by the user,
* with key being the name of the variable, and value - validated value from user or default.
* This method can be used to generate local variables using extract().
+ * limit=max will not be parsed if $parseMaxLimit is set to false; use this
+ * when the max limit is not definite, e.g. when getting revisions.
*/
- public function extractRequestParams() {
+ public function extractRequestParams($parseMaxLimit = true) {
$params = $this->getAllowedParams();
$results = array ();
foreach ($params as $paramName => $paramSettings)
- $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings);
+ $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
return $results;
}
@@ -323,6 +356,10 @@ abstract class ApiBase {
return $this->getParameterFromSettings($paramName, $paramSettings);
}
+ /**
+ * Returns an array of the namespaces (by integer id) that exist on the
+ * wiki. Used primarily in help documentation.
+ */
public static function getValidNamespaces() {
static $mValidNamespaces = null;
if (is_null($mValidNamespaces)) {
@@ -339,10 +376,12 @@ abstract class ApiBase {
/**
* Using the settings determine the value for the given parameter
+ *
* @param $paramName String: parameter name
* @param $paramSettings Mixed: default value or an array of settings using PARAM_* constants.
+ * @param $parseMaxLimit Boolean: parse limit when max is given?
*/
- protected function getParameterFromSettings($paramName, $paramSettings) {
+ protected function getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit) {
// Some classes may decide to change parameter names
$encParamName = $this->encodeParamName($paramName);
@@ -410,8 +449,17 @@ abstract class ApiBase {
if ($multi)
ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
$min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0;
- $value = intval($value);
- $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
+ if( $value == 'max' ) {
+ if( $parseMaxLimit ) {
+ $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self :: PARAM_MAX2] : $paramSettings[self :: PARAM_MAX];
+ $this->getResult()->addValue( 'limits', $this->getModuleName(), $value );
+ $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
+ }
+ }
+ else {
+ $value = intval($value);
+ $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
+ }
break;
case 'boolean' :
if ($multi)
@@ -485,7 +533,7 @@ abstract class ApiBase {
// Optimization: do not check user's bot status unless really needed -- skips db query
// assumes $botMax >= $max
if (!is_null($max) && $value > $max) {
- if (!is_null($botMax) && ($this->getMain()->isBot() || $this->getMain()->isSysop())) {
+ if (!is_null($botMax) && $this->getMain()->canApiHighLimits()) {
if ($value > $botMax) {
$this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName);
}
@@ -501,6 +549,90 @@ abstract class ApiBase {
public function dieUsage($description, $errorCode, $httpRespCode = 0) {
throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode);
}
+
+ /**
+ * Array that maps message keys to error messages. $1 and friends are replaced.
+ */
+ public static $messageMap = array(
+ // This one MUST be present, or dieUsageMsg() will recurse infinitely
+ 'unknownerror' => array('code' => 'unknownerror', 'info' => "Unknown error: ``\$1''"),
+ 'unknownerror-nocode' => array('code' => 'unknownerror', 'info' => 'Unknown error'),
+
+ // Messages from Title::getUserPermissionsErrors()
+ 'ns-specialprotected' => array('code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited"),
+ 'protectedinterface' => array('code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages"),
+ 'namespaceprotected' => array('code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the ``\$1'' namespace"),
+ 'customcssjsprotected' => array('code' => 'customcssjsprotected', 'info' => "You're not allowed to edit custom CSS and JavaScript pages"),
+ 'cascadeprotected' => array('code' => 'cascadeprotected', 'info' =>"The page you're trying to edit is protected because it's included in a cascade-protected page"),
+ 'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"),
+ 'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"),
+ 'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message
+ 'badaccess-group1' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Can't use the parameter 'cause it's wikilinked
+ 'badaccess-group2' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
+ 'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
+ 'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"),
+ 'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"),
+ 'nocreatetext' => array('code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages"),
+ 'movenologintext' => array('code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages"),
+ 'movenotallowed' => array('code' => 'cantmove', 'info' => "You don't have permission to move pages"),
+ 'confirmedittext' => array('code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit"),
+ 'blockedtext' => array('code' => 'blocked', 'info' => "You have been blocked from editing"),
+ 'autoblockedtext' => array('code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"),
+
+ // Miscellaneous interface messages
+ 'actionthrottledtext' => array('code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again"),
+ 'alreadyrolled' => array('code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back"),
+ 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"),
+ 'readonlytext' => array('code' => 'readonly', 'info' => "The wiki is currently in read-only mode"),
+ 'sessionfailure' => array('code' => 'badtoken', 'info' => "Invalid token"),
+ 'cannotdelete' => array('code' => 'cantdelete', 'info' => "Couldn't delete ``\$1''. Maybe it was deleted already by someone else"),
+ 'notanarticle' => array('code' => 'missingtitle', 'info' => "The page you requested doesn't exist"),
+ 'selfmove' => array('code' => 'selfmove', 'info' => "Can't move a page to itself"),
+ 'immobile_namespace' => array('code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving"),
+ 'articleexists' => array('code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article"),
+ 'protectedpage' => array('code' => 'protectedpage', 'info' => "You don't have permission to perform this move"),
+ 'hookaborted' => array('code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook"),
+ 'cantmove-titleprotected' => array('code' => 'protectedtitle', 'info' => "The destination article has been protected from creation"),
+ // 'badarticleerror' => shouldn't happen
+ // 'badtitletext' => shouldn't happen
+ 'ip_range_invalid' => array('code' => 'invalidrange', 'info' => "Invalid IP range"),
+ 'range_block_disabled' => array('code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled"),
+ 'nosuchusershort' => array('code' => 'nosuchuser', 'info' => "The user you specified doesn't exist"),
+ 'badipaddress' => array('code' => 'invalidip', 'info' => "Invalid IP address specified"),
+ 'ipb_expiry_invalid' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"),
+ 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"),
+ 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."),
+ 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"),
+
+ // API-specific messages
+ 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"),
+ 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"),
+ 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"),
+ 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"),
+ 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time is in the past"),
+ 'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"),
+ 'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"),
+ 'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"),
+ 'canthide' => array('code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log"),
+ 'cantblock-email' => array('code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki"),
+ 'unblock-notarget' => array('code' => 'notarget', 'info' => "Either the id or the user parameter must be set"),
+ 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can\'t be used together"),
+ 'cantunblock' => array('code' => 'permissiondenied', 'info' => "You don't have permission to unblock users"),
+ 'cannotundelete' => array('code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"),
+ 'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"),
+ );
+
+ /**
+ * Output the error message related to a certain array
+ * @param array $error Element of a getUserPermissionsErrors()
+ */
+ public function dieUsageMsg($error) {
+ $key = array_shift($error);
+ if(isset(self::$messageMap[$key]))
+ $this->dieUsage(wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error), wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error));
+ // If the key isn't present, throw an "unknown error"
+ $this->dieUsageMsg(array('unknownerror', $key));
+ }
/**
* Internal code errors should be reported with this method
@@ -510,6 +642,28 @@ abstract class ApiBase {
}
/**
+ * Indicates if API needs to check maxlag
+ */
+ public function shouldCheckMaxlag() {
+ return true;
+ }
+
+ /**
+ * Indicates if this module requires edit mode
+ */
+ public function isEditMode() {
+ return false;
+ }
+
+ /**
+ * Indicates whether this module must be called with a POST request
+ */
+ public function mustBePosted() {
+ return false;
+ }
+
+
+ /**
* Profiling: total module execution time
*/
private $mTimeIn = 0, $mModuleTime = 0;
@@ -610,10 +764,12 @@ abstract class ApiBase {
print "\n</pre>\n";
}
- public abstract function getVersion();
+ /**
+ * Returns a String that identifies the version of this class.
+ */
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiBase.php 24934 2007-08-20 08:04:12Z nickj $';
- }
+ return __CLASS__ . ': $Id: ApiBase.php 31259 2008-02-25 14:14:55Z catrope $';
+ }
}
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
new file mode 100644
index 00000000..e5c238ae
--- /dev/null
+++ b/includes/api/ApiBlock.php
@@ -0,0 +1,164 @@
+<?php
+
+/*
+ * Created on Sep 4, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+* API module that facilitates the blocking of users. Requires API write mode
+* to be enabled.
+*
+ * @addtogroup API
+ */
+class ApiBlock extends ApiBase {
+
+ /**
+ * Std ctor.
+ */
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ /**
+ * Blocks the user specified in the parameters for the given expiry, with the
+ * given reason, and with all other settings provided in the params. If the block
+ * succeeds, produces a result containing the details of the block and notice
+ * of success. If it fails, the result will specify the nature of the error.
+ */
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ if($params['gettoken'])
+ {
+ $res['blocktoken'] = $wgUser->editToken();
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ return;
+ }
+
+ if(is_null($params['user']))
+ $this->dieUsageMsg(array('missingparam', 'user'));
+ if(is_null($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+ if(!$wgUser->isAllowed('block'))
+ $this->dieUsageMsg(array('cantblock'));
+ if($params['hidename'] && !$wgUser->isAllowed('hideuser'))
+ $this->dieUsageMsg(array('canthide'));
+ if($params['noemail'] && !$wgUser->isAllowed('blockemail'))
+ $this->dieUsageMsg(array('cantblock-email'));
+ if(wfReadOnly())
+ $this->dieUsageMsg(array('readonlytext'));
+
+ $form = new IPBlockForm('');
+ $form->BlockAddress = $params['user'];
+ $form->BlockReason = (is_null($params['reason']) ? '' : $params['reason']);
+ $form->BlockReasonList = 'other';
+ $form->BlockExpiry = ($params['expiry'] == 'never' ? 'infinite' : $params['expiry']);
+ $form->BlockOther = '';
+ $form->BlockAnonOnly = $params['anononly'];
+ $form->BlockCreateAccount = $params['nocreate'];
+ $form->BlockEnableAutoBlock = $params['autoblock'];
+ $form->BlockEmail = $params['noemail'];
+ $form->BlockHideName = $params['hidename'];
+
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $retval = $form->doBlock($userID, $expiry);
+ if(!empty($retval))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg($retval);
+
+ $dbw->commit();
+ $res['user'] = $params['user'];
+ $res['userID'] = $userID;
+ $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : $expiry);
+ $res['reason'] = $params['reason'];
+ if($params['anononly'])
+ $res['anononly'] = '';
+ if($params['nocreate'])
+ $res['nocreate'] = '';
+ if($params['autoblock'])
+ $res['autoblock'] = '';
+ if($params['noemail'])
+ $res['noemail'] = '';
+ if($params['hidename'])
+ $res['hidename'] = '';
+
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'user' => null,
+ 'token' => null,
+ 'gettoken' => false,
+ 'expiry' => 'never',
+ 'reason' => null,
+ 'anononly' => false,
+ 'nocreate' => false,
+ 'autoblock' => false,
+ 'noemail' => false,
+ 'hidename' => false,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'user' => 'Username, IP address or IP range you want to block',
+ 'token' => 'A block token previously obtained through the gettoken parameter',
+ 'gettoken' => 'If set, a block token will be returned, and no other action will be taken',
+ 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.',
+ 'reason' => 'Reason for block (optional)',
+ 'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)',
+ 'nocreate' => 'Prevent account creation',
+ 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from',
+ 'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)',
+ 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Block a user.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike',
+ 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate&autoblock&noemail'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiBlock.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiChangeRights.php b/includes/api/ApiChangeRights.php
new file mode 100644
index 00000000..647a5194
--- /dev/null
+++ b/includes/api/ApiChangeRights.php
@@ -0,0 +1,155 @@
+<?php
+
+/*
+ * Created on Sep 11, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * API module that facilitates the changing of user rights. The API eqivalent of
+ * Special:Userrights. Requires API write mode to be enabled.
+ *
+ * @addtogroup API
+ */
+class ApiChangeRights extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser, $wgRequest;
+ $this->getMain()->requestWriteMode();
+
+ if(wfReadOnly())
+ $this->dieUsage('The wiki is in read-only mode', 'readonly');
+ $params = $this->extractRequestParams();
+
+ $ur = new UserrightsPage($wgRequest);
+ $allowed = $ur->changeableGroups();
+ $res = array();
+
+ $u = $ur->fetchUser_real($params['user']);
+ if(is_array($u))
+ switch($u[0])
+ {
+ case UserrightsPage::FETCHUSER_NO_INTERWIKI:
+ $this->dieUsage("You don't have permission to change users' rights on other wikis", 'nointerwiki');
+ case UserrightsPage::FETCHUSER_NO_DATABASE:
+ $this->dieUsage("Database ``{$u[1]}'' does not exist or is not local", 'nosuchdatabase');
+ case UserrightsPage::FETCHUSER_NO_USER:
+ $this->dieUsage("You specified an empty username, or none at all", 'emptyuser');
+ case UserrightsPage::FETCHUSER_NOSUCH_USERID:
+ $this->dieUsage("There is no user with ID ``{$u[1]}''", 'nosuchuserid');
+ case UserrightsPage::FETCHUSER_NOSUCH_USERNAME:
+ $this->dieUsage("There is no user with username ``{$u[1]}''", 'nosuchusername');
+ default:
+ $this->dieDebug(__METHOD__, "UserrightsPage::fetchUser_real() returned an unknown error ({$u[0]})");
+ }
+
+ $curgroups = $u->getGroups();
+ if($params['listgroups'])
+ {
+ $res['user'] = $u->getName();
+ $res['allowedgroups'] = $allowed;
+ $res['ingroups'] = $curgroups;
+ $this->getResult()->setIndexedTagName($res['ingroups'], 'group');
+ $this->getResult()->setIndexedTagName($res['allowedgroups']['add'], 'group');
+ $this->getResult()->setIndexedTagName($res['allowedgroups']['remove'], 'group');
+ }
+;
+ if($params['gettoken'])
+ {
+ $res['changerightstoken'] = $wgUser->editToken($u->getName());
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ return;
+ }
+
+ if(empty($params['addto']) && empty($params['rmfrom']))
+ $this->dieUsage('At least one of the addto and rmfrom parameters must be set', 'noaddrm');
+ if(is_null($params['token']))
+ $this->dieUsage('The token parameter must be set', 'notoken');
+ if(!$wgUser->matchEditToken($params['token'], $u->getName()))
+ $this->dieUsage('Invalid token', 'badtoken');
+
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $ur->saveUserGroups($u, $params['rmfrom'], $params['addto'], $params['reason']);
+ $dbw->commit();
+ $res['user'] = $u->getName();
+ $res['addedto'] = (array)$params['addto'];
+ $res['removedfrom'] = (array)$params['rmfrom'];
+ $res['reason'] = $params['reason'];
+
+ $this->getResult()->setIndexedTagName($res['addedto'], 'group');
+ $this->getResult()->setIndexedTagName($res['removedfrom'], 'group');
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'user' => null,
+ 'token' => null,
+ 'gettoken' => false,
+ 'listgroups' => false,
+ 'addto' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ 'rmfrom' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ 'reason' => ''
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'user' => 'The user you want to add to or remove from groups.',
+ 'token' => 'A changerights token previously obtained through the gettoken parameter.',
+ 'gettoken' => 'Output a token. Note that the user parameter still has to be set.',
+ 'listgroups' => 'List the groups the user is in, and the ones you can add them to and remove them from.',
+ 'addto' => 'Pipe-separated list of groups to add this user to',
+ 'rmfrom' => 'Pipe-separated list of groups to remove this user from',
+ 'reason' => 'Reason for change (optional)'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Add or remove a user from certain groups.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=changerights&user=Bob&gettoken&listgroups',
+ 'api.php?action=changerights&user=Bob&token=123ABC&addto=sysop&reason=Promoting%20per%20RFA'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiChangeRights.php 28216 2007-12-06 18:33:18Z vasilievvv $';
+ }
+}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
new file mode 100644
index 00000000..cd747e7e
--- /dev/null
+++ b/includes/api/ApiDelete.php
@@ -0,0 +1,155 @@
+<?php
+
+/*
+ * Created on Jun 30, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+
+/**
+ * API module that facilitates deleting pages. The API eqivalent of action=delete.
+ * Requires API write mode to be enabled.
+ *
+ * @addtogroup API
+ */
+class ApiDelete extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ /**
+ * Extracts the title, token, and reason from the request parameters and invokes
+ * the local delete() function with these as arguments. It does not make use of
+ * the delete function specified by Article.php. If the deletion succeeds, the
+ * details of the article deleted and the reason for deletion are added to the
+ * result object.
+ */
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ $titleObj = NULL;
+ if(!isset($params['title']))
+ $this->dieUsageMsg(array('missingparam', 'title'));
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ if(!$titleObj->exists())
+ $this->dieUsageMsg(array('notanarticle'));
+
+ $articleObj = new Article($titleObj);
+ $reason = (isset($params['reason']) ? $params['reason'] : NULL);
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $retval = self::delete($articleObj, $params['token'], $reason);
+
+ if(!empty($retval))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg(current($retval));
+
+ $dbw->commit();
+ $r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason);
+ $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ }
+
+ /**
+ * We have our own delete() function, since Article.php's implementation is split in two phases
+ *
+ * @param Article $article - Article object to work on
+ * @param string $token - Delete token (same as edit token)
+ * @param string $reason - Reason for the deletion. Autogenerated if NULL
+ * @return Title::getUserPermissionsErrors()-like array
+ */
+ public static function delete(&$article, $token, &$reason = NULL)
+ {
+ global $wgUser;
+
+ // Check permissions
+ $errors = $article->mTitle->getUserPermissionsErrors('delete', $wgUser);
+ if(!empty($errors))
+ return $errors;
+ if(wfReadOnly())
+ return array(array('readonlytext'));
+ if($wgUser->isBlocked())
+ return array(array('blocked'));
+
+ // Check token
+ if(!$wgUser->matchEditToken($token))
+ return array(array('sessionfailure'));
+
+ // Auto-generate a summary, if necessary
+ if(is_null($reason))
+ {
+ $reason = $article->generateReason($hasHistory);
+ if($reason === false)
+ return array(array('cannotdelete'));
+ }
+
+ // Luckily, Article.php provides a reusable delete function that does the hard work for us
+ if($article->doDeleteArticle($reason))
+ return array();
+ return array(array('cannotdelete', $article->mTitle->getPrefixedText()));
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'token' => null,
+ 'reason' => null,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'title' => 'Title of the page you want to delete.',
+ 'token' => 'A delete token previously retrieved through prop=info',
+ 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Deletes a page. You need to be logged in as a sysop to use this function, see also action=login.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=delete&title=Main%20Page&token=123ABC',
+ 'api.php?action=delete&title=Main%20Page&token=123ABC&reason=Preparing%20for%20move'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiDelete.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
new file mode 100644
index 00000000..278896fa
--- /dev/null
+++ b/includes/api/ApiExpandTemplates.php
@@ -0,0 +1,97 @@
+<?php
+
+/*
+ * Created on Oct 05, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * API module that functions as a shortcut to the wikitext preprocessor. Expands
+ * any templates in a provided string, and returns the result of this expansion
+ * to the caller.
+ *
+ * @addtogroup API
+ */
+class ApiExpandTemplates extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ // Get parameters
+ $params = $this->extractRequestParams();
+ $text = $params['text'];
+ $title = $params['title'];
+ $retval = '';
+
+ //Create title for parser
+ $title_obj = Title :: newFromText($params['title']);
+ if(!$title_obj)
+ $title_obj = Title :: newFromText("API"); // Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it
+
+ // Parse text
+ global $wgParser;
+ $retval = $wgParser->preprocess( $text, $title_obj, new ParserOptions() );
+
+ // Return result
+ $result = $this->getResult();
+ $retval_array = array();
+ $result->setContent( $retval_array, $retval );
+ $result->addValue( null, $this->getModuleName(), $retval_array );
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => array(
+ ApiBase :: PARAM_DFLT => 'API',
+ ),
+ 'text' => null
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'text' => 'Wikitext to convert',
+ 'title' => 'Title of page',
+ );
+ }
+
+ public function getDescription() {
+ return 'This module expand all templates in wikitext';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=expandtemplates&text={{Project:Sandbox}}'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiExpandTemplates.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
+
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index b2f6ceff..9b17b9d3 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -62,17 +62,24 @@ class ApiFeedWatchlist extends ApiBase {
// limit to the number of hours going from now back
$endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60));
- // Prepare nested request
- $fauxReq = new FauxRequest(array (
+ $dbr = wfGetDB( DB_SLAVE );
+ // Prepare parameters for nested request
+ $fauxReqArr = array (
'action' => 'query',
'meta' => 'siteinfo',
'siprop' => 'general',
'list' => 'watchlist',
'wlprop' => 'title|user|comment|timestamp',
'wldir' => 'older', // reverse order - from newest to oldest
- 'wlend' => $endTime, // stop at this time
+ 'wlend' => $dbr->timestamp($endTime), // stop at this time
'wllimit' => 50
- ));
+ );
+
+ // Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
+ if ( ! is_null ( $params['allrev'] ) ) $fauxReqArr['wlallrev'] = '';
+
+ // Create the request
+ $fauxReq = new FauxRequest ( $fauxReqArr );
// Execute
$module = new ApiMain($fauxReq);
@@ -131,7 +138,7 @@ class ApiFeedWatchlist extends ApiBase {
return new FeedItem($titleStr, $completeText, $titleUrl, $timestamp, $user);
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
global $wgFeedClasses;
$feedFormatNames = array_keys($wgFeedClasses);
return array (
@@ -144,18 +151,20 @@ class ApiFeedWatchlist extends ApiBase {
ApiBase :: PARAM_TYPE => 'integer',
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => 72,
- )
+ ),
+ 'allrev' => null
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'feedformat' => 'The format of the feed',
- 'hours' => 'List pages modified within this many hours from now'
+ 'hours' => 'List pages modified within this many hours from now',
+ 'allrev' => 'Include multiple revisions of the same page within given timeframe.'
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'This module returns a watchlist feed';
}
@@ -166,7 +175,7 @@ class ApiFeedWatchlist extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFeedWatchlist.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFeedWatchlist.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 861310d2..768a18ac 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -35,7 +35,7 @@ if (!defined('MEDIAWIKI')) {
*/
abstract class ApiFormatBase extends ApiBase {
- private $mIsHtml, $mFormat;
+ private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp;
/**
* Create a new instance of the formatter.
@@ -69,6 +69,18 @@ abstract class ApiFormatBase extends ApiBase {
}
/**
+ * Specify whether or not ampersands should be escaped to '&amp;' when rendering. This
+ * should only be set to true for the help message when rendered in the default (xmlfm)
+ * format. This is a temporary special-case fix that should be removed once the help
+ * has been reworked to use a fully html interface.
+ *
+ * @param boolean Whether or not ampersands should be escaped.
+ */
+ public function setUnescapeAmps ( $b ) {
+ $this->mUnescapeAmps = $b;
+ }
+
+ /**
* Returns true when an HTML filtering printer should be used.
* The default implementation assumes that formats ending with 'fm'
* should be formatted in HTML.
@@ -99,7 +111,11 @@ abstract class ApiFormatBase extends ApiBase {
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
- <title>MediaWiki API</title>
+<?php if ($this->mUnescapeAmps) {
+?> <title>MediaWiki API</title>
+<?php } else {
+?> <title>MediaWiki API Result</title>
+<?php } ?>
</head>
<body>
<?php
@@ -154,13 +170,20 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
}
/**
+ * Says pretty-printer that it should use *bold* and $italics$ formatting
+ */
+ public function setHelp( $help = true ) {
+ $this->mHelp = true;
+ }
+
+ /**
* Prety-print various elements in HTML format, such as xml tags and URLs.
* This method also replaces any '<' with &lt;
*/
protected function formatHTML($text) {
// Escape everything first for full coverage
$text = htmlspecialchars($text);
-
+
// encode all comments or tags as safe blue strings
$text = preg_replace('/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text);
// identify URLs
@@ -168,10 +191,19 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
$text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '<a href="\\0">\\0</a>', $text);
// identify requests to api.php
$text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '<a href="\\0">\\0</a>', $text);
- // make strings inside * bold
- $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text);
- // make strings inside $ italic
- $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text);
+ if( $this->mHelp ) {
+ // make strings inside * bold
+ $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text);
+ // make strings inside $ italic
+ $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text);
+ }
+
+ /* Temporary fix for bad links in help messages. As a special case,
+ * XML-escaped metachars are de-escaped one level in the help message
+ * for legibility. Should be removed once we have completed a fully-html
+ * version of the help message. */
+ if ( $this->mUnescapeAmps )
+ $text = preg_replace( '/&amp;(amp|quot|lt|gt);/', '&\1;', $text );
return $text;
}
@@ -183,12 +215,12 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
return 'api.php?action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName();
}
- protected function getDescription() {
+ public function getDescription() {
return $this->getIsHtml() ? ' (pretty-print in HTML)' : '';
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
@@ -250,6 +282,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php
new file mode 100644
index 00000000..f0fc5e91
--- /dev/null
+++ b/includes/api/ApiFormatDbg.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * Created on Oct 22, 2006
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiFormatBase.php');
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiFormatDbg extends ApiFormatBase {
+
+ public function __construct($main, $format) {
+ parent :: __construct($main, $format);
+ }
+
+ public function getMimeType() {
+ # This looks like it should be text/plain, but IE7 is so
+ # brain-damaged it tries to parse text/plain as HTML if it
+ # contains HTML tags. Using MIME text/text works around this bug
+ return 'text/text';
+ }
+
+ public function execute() {
+ $this->printText(var_export($this->getResultData(), true));
+ }
+
+ public function getDescription() {
+ return 'Output data in PHP\'s var_export() format' . parent :: getDescription();
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ }
+}
+
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index 59f3b492..852a64b6 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -66,19 +66,19 @@ class ApiFormatJson extends ApiFormatBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'callback' => null
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'callback' => 'If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.',
);
}
- protected function getDescription() {
+ public function getDescription() {
if ($this->mIsRaw)
return 'Output data with the debuging elements in JSON format' . parent :: getDescription();
else
@@ -86,7 +86,7 @@ class ApiFormatJson extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatJson.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatJson.php 31484 2008-03-03 05:46:20Z brion $';
}
}
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index 766d7041..f830d8e1 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -45,12 +45,12 @@ class ApiFormatPhp extends ApiFormatBase {
$this->printText(serialize($this->getResultData()));
}
- protected function getDescription() {
+ public function getDescription() {
return 'Output data in serialized PHP format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php
new file mode 100644
index 00000000..c4c45f68
--- /dev/null
+++ b/includes/api/ApiFormatTxt.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * Created on Oct 22, 2006
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiFormatBase.php');
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiFormatTxt extends ApiFormatBase {
+
+ public function __construct($main, $format) {
+ parent :: __construct($main, $format);
+ }
+
+ public function getMimeType() {
+ # This looks like it should be text/plain, but IE7 is so
+ # brain-damaged it tries to parse text/plain as HTML if it
+ # contains HTML tags. Using MIME text/text works around this bug
+ return 'text/text';
+ }
+
+ public function execute() {
+ $this->printText(print_r($this->getResultData(), true));
+ }
+
+ public function getDescription() {
+ return 'Output data in PHP\'s print_r() format' . parent :: getDescription();
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ }
+}
+
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 0ddfac73..22a0e482 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -80,12 +80,12 @@ class ApiFormatWddx extends ApiFormatBase {
}
}
- protected function getDescription() {
+ public function getDescription() {
return 'Output data in WDDX format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatWddx.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatWddx.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index 02647923..d39e8049 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -136,12 +136,12 @@ class ApiFormatXml extends ApiFormatBase {
break;
}
}
- protected function getDescription() {
+ public function getDescription() {
return 'Output data in XML format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatXml.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatXml.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index 400c0a4b..5e15aee6 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -45,12 +45,12 @@ class ApiFormatYaml extends ApiFormatBase {
$this->printText(Spyc :: YAMLDump($this->getResultData()));
}
- protected function getDescription() {
+ public function getDescription() {
return 'Output data in YAML format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatYaml.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatYaml.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php
index b3ccff0f..b2973b8c 100644
--- a/includes/api/ApiFormatYaml_spyc.php
+++ b/includes/api/ApiFormatYaml_spyc.php
@@ -385,6 +385,18 @@
return false;
}
}
+
+ /**
+ * Find out whether a string needs to be output as a literal rather than in plain style.
+ * Added by Roan Kattouw 13-03-2008
+ * @param $value The string to check
+ * @return bool
+ */
+ function _needLiteral($value) {
+ # Check whether the string contains # or : or begins with any of:
+ # [ - ? , [ ] { } ! * & | > ' " % @ ` ]
+ return (bool)(preg_match("/[#:]/", $value) || preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value));
+ }
/**
* Returns YAML from a key and a value
@@ -396,7 +408,7 @@
*/
function _dumpNode($key,$value,$indent) {
// do some folding here, for blocks
- if (strpos($value,"\n")) {
+ if (strpos($value,"\n") || $this->_needLiteral($value)) {
$value = $this->_doLiteralBlock($value,$indent);
} else {
$value = $this->_doFolding($value,$indent);
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index 9f1e88ea..47a45ea1 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -46,14 +46,18 @@ class ApiHelp extends ApiBase {
$this->dieUsage('', 'help');
}
- protected function getDescription() {
+ public function shouldCheckMaxlag() {
+ return false;
+ }
+
+ public function getDescription() {
return array (
'Display this help screen.'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiHelp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiHelp.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index af68b29d..3e66ed79 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -40,7 +40,7 @@ class ApiLogin extends ApiBase {
* Time (in seconds) a user must wait after submitting
* a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt)
*/
- const THROTTLE_TIME = 1;
+ const THROTTLE_TIME = 5;
/**
* The factor by which the wait-time in between authentication
@@ -91,10 +91,15 @@ class ApiLogin extends ApiBase {
'wpRemember' => ''
));
+ // Init session if necessary
+ if( session_id() == '' ) {
+ wfSetupSession();
+ }
+
$loginForm = new LoginForm($params);
switch ($loginForm->authenticateUserData()) {
case LoginForm :: SUCCESS :
- global $wgUser;
+ global $wgUser, $wgCookiePrefix;
$wgUser->setOption('rememberpassword', 1);
$wgUser->setCookies();
@@ -103,6 +108,8 @@ class ApiLogin extends ApiBase {
$result['lguserid'] = $_SESSION['wsUserID'];
$result['lgusername'] = $_SESSION['wsUserName'];
$result['lgtoken'] = $_SESSION['wsToken'];
+ $result['cookieprefix'] = $wgCookiePrefix;
+ $result['sessionid'] = session_id();
break;
case LoginForm :: NO_NAME :
@@ -129,6 +136,7 @@ class ApiLogin extends ApiBase {
if ($result['result'] != 'Success') {
$result['wait'] = $this->cacheBadLogin();
+ $result['details'] = "Please wait " . self::THROTTLE_TIME . " seconds before next log-in attempt";
}
// if we were allowed to try to login, memcache is fine
@@ -209,8 +217,10 @@ class ApiLogin extends ApiBase {
private function getMemCacheKey() {
return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() );
}
+
+ public function mustBePosted() { return true; }
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'name' => null,
'password' => null,
@@ -218,7 +228,7 @@ class ApiLogin extends ApiBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'name' => 'User Name',
'password' => 'Password',
@@ -226,7 +236,7 @@ class ApiLogin extends ApiBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return array (
'This module is used to login and get the authentication tokens. ',
'In the event of a successful log-in, a cookie will be attached',
@@ -243,7 +253,7 @@ class ApiLogin extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogin.php 24695 2007-08-09 09:53:05Z yurik $';
+ return __CLASS__ . ': $Id: ApiLogin.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
new file mode 100644
index 00000000..d578acf3
--- /dev/null
+++ b/includes/api/ApiLogout.php
@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * Created on Jan 4, 2008
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiBase.php');
+}
+
+/**
+ * API module to allow users to log out of the wiki. API equivalent of
+ * Special:Userlogout.
+ *
+ * @addtogroup API
+ */
+class ApiLogout extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $wgUser->logout();
+ }
+
+ public function getAllowedParams() {
+ return array ();
+ }
+
+ public function getParamDescription() {
+ return array ();
+ }
+
+ public function getDescription() {
+ return array (
+ 'This module is used to logout and clear session data'
+ );
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=logout'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id$';
+ }
+}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 00b3f63f..874e531c 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -53,10 +53,26 @@ class ApiMain extends ApiBase {
*/
private static $Modules = array (
'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
'query' => 'ApiQuery',
+ 'expandtemplates' => 'ApiExpandTemplates',
+ 'parse' => 'ApiParse',
'opensearch' => 'ApiOpenSearch',
'feedwatchlist' => 'ApiFeedWatchlist',
'help' => 'ApiHelp',
+ 'paraminfo' => 'ApiParamInfo',
+ );
+
+ private static $WriteModules = array (
+ 'rollback' => 'ApiRollback',
+ 'delete' => 'ApiDelete',
+ 'undelete' => 'ApiUndelete',
+ 'protect' => 'ApiProtect',
+ 'block' => 'ApiBlock',
+ 'unblock' => 'ApiUnblock',
+ 'move' => 'ApiMove',
+ #'changerights' => 'ApiChangeRights'
+ # Disabled for now
);
/**
@@ -73,7 +89,11 @@ class ApiMain extends ApiBase {
'xmlfm' => 'ApiFormatXml',
'yaml' => 'ApiFormatYaml',
'yamlfm' => 'ApiFormatYaml',
- 'rawfm' => 'ApiFormatJson'
+ 'rawfm' => 'ApiFormatJson',
+ 'txt' => 'ApiFormatTxt',
+ 'txtfm' => 'ApiFormatTxt',
+ 'dbg' => 'ApiFormatDbg',
+ 'dbgfm' => 'ApiFormatDbg'
);
private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames;
@@ -108,14 +128,17 @@ class ApiMain extends ApiBase {
if (!$wgUser->isAllowed('read')) {
self::$Modules = array(
- 'login' => self::$Modules['login'],
- 'help' => self::$Modules['help']
+ 'login' => self::$Modules['login'],
+ 'logout' => self::$Modules['logout'],
+ 'help' => self::$Modules['help'],
);
}
}
- global $wgAPIModules; // extension modules
+ global $wgAPIModules, $wgEnableWriteAPI; // extension modules
$this->mModules = $wgAPIModules + self :: $Modules;
+ if($wgEnableWriteAPI)
+ $this->mModules += self::$WriteModules;
$this->mModuleNames = array_keys($this->mModules); // todo: optimize
$this->mFormats = self :: $Formats;
@@ -157,7 +180,7 @@ class ApiMain extends ApiBase {
public function requestWriteMode() {
if (!$this->mEnableWrite)
$this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' .
- 'statement is included in the site\'s LocalSettings.php file', 'readonly');
+ 'statement is included in the site\'s LocalSettings.php file', 'noapiwrite');
}
/**
@@ -271,7 +294,7 @@ class ApiMain extends ApiBase {
// Something is seriously wrong
//
$errMessage = array (
- 'code' => 'internal_api_error',
+ 'code' => 'internal_api_error_'. get_class($e),
'info' => "Exception Caught: {$e->getMessage()}"
);
ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n");
@@ -287,16 +310,34 @@ class ApiMain extends ApiBase {
* Execute the actual module, without any error handling
*/
protected function executeAction() {
-
+
$params = $this->extractRequestParams();
-
+
$this->mShowVersions = $params['version'];
$this->mAction = $params['action'];
// Instantiate the module requested by the user
$module = new $this->mModules[$this->mAction] ($this, $this->mAction);
-
+
+ if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
+ // Check for maxlag
+ global $wgLoadBalancer, $wgShowHostnames;
+ $maxLag = $params['maxlag'];
+ list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ if ( $lag > $maxLag ) {
+ if( $wgShowHostnames ) {
+ ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
+ } else {
+ ApiBase :: dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
+ }
+ return;
+ }
+ }
+
if (!$this->mInternalMode) {
+ // Ignore mustBePosted() for internal calls
+ if($module->mustBePosted() && !$this->mRequest->wasPosted())
+ $this->dieUsage("The {$this->mAction} module requires a POST request", 'mustbeposted');
// See if custom printer is used
$this->mPrinter = $module->getCustomPrinter();
@@ -326,7 +367,16 @@ class ApiMain extends ApiBase {
protected function printResult($isError) {
$printer = $this->mPrinter;
$printer->profileIn();
+
+ /* If the help message is requested in the default (xmlfm) format,
+ * tell the printer not to escape ampersands so that our links do
+ * not break. */
+ $params = $this->extractRequestParams();
+ $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError )
+ && $params['format'] == ApiMain::API_DEFAULT_FORMAT );
+
$printer->initPrinter($isError);
+
$printer->execute();
$printer->closePrinter();
$printer->profileOut();
@@ -335,7 +385,7 @@ class ApiMain extends ApiBase {
/**
* See ApiBase for description.
*/
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'format' => array (
ApiBase :: PARAM_DFLT => ApiMain :: API_DEFAULT_FORMAT,
@@ -345,25 +395,29 @@ class ApiMain extends ApiBase {
ApiBase :: PARAM_DFLT => 'help',
ApiBase :: PARAM_TYPE => $this->mModuleNames
),
- 'version' => false
+ 'version' => false,
+ 'maxlag' => array (
+ ApiBase :: PARAM_TYPE => 'integer'
+ ),
);
}
/**
* See ApiBase for description.
*/
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'format' => 'The format of the output',
'action' => 'What action you would like to perform',
- 'version' => 'When showing help, include version for each module'
+ 'version' => 'When showing help, include version for each module',
+ 'maxlag' => 'Maximum lag'
);
}
/**
* See ApiBase for description.
*/
- protected function getDescription() {
+ public function getDescription() {
return array (
'',
'',
@@ -396,8 +450,9 @@ class ApiMain extends ApiBase {
*/
protected function getCredits() {
return array(
- 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / <Firstname><Lastname>@gmail.com',
- 'Please leave your comments and suggestions at http://www.mediawiki.org/wiki/API'
+ 'This API is being implemented by Roan Kattouw <Firstname>.<Lastname>@home.nl',
+ 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
+ 'or file a bug report at http://bugzilla.wikimedia.org/'
);
}
@@ -405,6 +460,8 @@ class ApiMain extends ApiBase {
* Override the parent to generate help messages for all available modules.
*/
public function makeHelpMsg() {
+
+ $this->mPrinter->setHelp();
// Use parent to make default message for the main module
$msg = parent :: makeHelpMsg();
@@ -445,11 +502,12 @@ class ApiMain extends ApiBase {
}
private $mIsBot = null;
-
private $mIsSysop = null;
+ private $mCanApiHighLimits = null;
/**
* Returns true if the currently logged in user is a bot, false otherwise
+ * OBSOLETE, use canApiHighLimits() instead
*/
public function isBot() {
if (!isset ($this->mIsBot)) {
@@ -462,6 +520,7 @@ class ApiMain extends ApiBase {
/**
* Similar to isBot(), this method returns true if the logged in user is
* a sysop, and false if not.
+ * OBSOLETE, use canApiHighLimits() instead
*/
public function isSysop() {
if (!isset ($this->mIsSysop)) {
@@ -471,6 +530,15 @@ class ApiMain extends ApiBase {
return $this->mIsSysop;
}
+
+ public function canApiHighLimits() {
+ if (!isset($this->mCanApiHighLimits)) {
+ global $wgUser;
+ $this->mCanApiHighLimits = $wgUser->isAllowed('apihighlimits');
+ }
+
+ return $this->mCanApiHighLimits;
+ }
public function getShowVersions() {
return $this->mShowVersions;
@@ -483,7 +551,7 @@ class ApiMain extends ApiBase {
public function getVersion() {
$vers = array ();
$vers[] = 'MediaWiki ' . SpecialVersion::getVersion();
- $vers[] = __CLASS__ . ': $Id: ApiMain.php 25364 2007-08-31 15:23:48Z tstarling $';
+ $vers[] = __CLASS__ . ': $Id: ApiMain.php 31484 2008-03-03 05:46:20Z brion $';
$vers[] = ApiBase :: getBaseVersion();
$vers[] = ApiFormatBase :: getBaseVersion();
$vers[] = ApiQueryBase :: getBaseVersion();
@@ -515,6 +583,13 @@ class ApiMain extends ApiBase {
protected function addFormat( $fmtName, $fmtClass ) {
$this->mFormats[$fmtName] = $fmtClass;
}
+
+ /**
+ * Get the array mapping module names to class names
+ */
+ function getModules() {
+ return $this->mModules;
+ }
}
/**
@@ -539,3 +614,4 @@ class UsageException extends Exception {
}
}
+
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
new file mode 100644
index 00000000..a8c39c9a
--- /dev/null
+++ b/includes/api/ApiMove.php
@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * Created on Oct 31, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+
+/**
+ * @addtogroup API
+ */
+class ApiMove extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+ if(is_null($params['reason']))
+ $params['reason'] = '';
+
+ $titleObj = NULL;
+ if(!isset($params['from']))
+ $this->dieUsageMsg(array('missingparam', 'from'));
+ if(!isset($params['to']))
+ $this->dieUsageMsg(array('missingparam', 'to'));
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+
+ $fromTitle = Title::newFromText($params['from']);
+ if(!$fromTitle)
+ $this->dieUsageMsg(array('invalidtitle', $params['from']));
+ if(!$fromTitle->exists())
+ $this->dieUsageMsg(array('notanarticle'));
+ $fromTalk = $fromTitle->getTalkPage();
+
+ $toTitle = Title::newFromText($params['to']);
+ if(!$toTitle)
+ $this->dieUsageMsg(array('invalidtitle', $params['to']));
+ $toTalk = $toTitle->getTalkPage();
+
+ // Run getUserPermissionsErrors() here so we get message arguments too,
+ // rather than just a message key. The latter is troublesome for messages
+ // that use arguments.
+ // FIXME: moveTo() should really return an array, requires some
+ // refactoring of other code, though (mainly SpecialMovepage.php)
+ $errors = array_merge($fromTitle->getUserPermissionsErrors('move', $wgUser),
+ $fromTitle->getUserPermissionsErrors('edit', $wgUser),
+ $toTitle->getUserPermissionsErrors('move', $wgUser),
+ $toTitle->getUserPermissionsErrors('edit', $wgUser));
+ if(!empty($errors))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg(current($errors));
+
+ $dbw = wfGetDB(DB_MASTER);
+ $dbw->begin();
+ $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']);
+ if($retval !== true)
+ $this->dieUsageMsg(array($retval));
+
+ $r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']);
+ if(!$params['noredirect'])
+ $r['redirectcreated'] = '';
+
+ if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage())
+ {
+ // We need to move the talk page as well
+ $toTalk = $toTitle->getTalkPage();
+ $retval = $fromTalk->moveTo($toTalk, true, $params['reason'], !$params['noredirect']);
+ if($retval === true)
+ {
+ $r['talkfrom'] = $fromTalk->getPrefixedText();
+ $r['talkto'] = $toTalk->getPrefixedText();
+ }
+ // We're not gonna dieUsage() on failure, since we already changed something
+ else
+ {
+ $r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code'];
+ $r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info'];
+ }
+ }
+ $dbw->commit(); // Make sure all changes are really written to the DB
+ $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'from' => null,
+ 'to' => null,
+ 'token' => null,
+ 'reason' => null,
+ 'movetalk' => false,
+ 'noredirect' => false
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'from' => 'Title of the page you want to move.',
+ 'to' => 'Title you want to rename the page to.',
+ 'token' => 'A move token previously retrieved through prop=info',
+ 'reason' => 'Reason for the move (optional).',
+ 'movetalk' => 'Move the talk page, if it exists.',
+ 'noredirect' => 'Don\'t create a redirect'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Moves a page.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=move&from=Exampel&to=Example&token=123ABC&reason=Misspelled%20title&movetalk&noredirect'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiMove.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index 8484b163..f4b600fe 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -44,37 +44,12 @@ class ApiOpenSearch extends ApiBase {
public function execute() {
$params = $this->extractRequestParams();
$search = $params['search'];
+ $limit = $params['limit'];
// Open search results may be stored for a very long time
$this->getMain()->setCacheMaxAge(1200);
-
- $title = Title :: newFromText($search);
- if(!$title)
- return; // Return empty result
-
- // Prepare nested request
- $req = new FauxRequest(array (
- 'action' => 'query',
- 'list' => 'allpages',
- 'apnamespace' => $title->getNamespace(),
- 'aplimit' => 10,
- 'apprefix' => $title->getDBkey()
- ));
-
- // Execute
- $module = new ApiMain($req);
- $module->execute();
-
- // Get resulting data
- $data = $module->getResultData();
-
- // Reformat useful data for future printing by JSON engine
- $srchres = array ();
- foreach ($data['query']['allpages'] as & $pageinfo) {
- // Note: this data will no be printable by the xml engine
- // because it does not support lists of unnamed items
- $srchres[] = $pageinfo['title'];
- }
+
+ $srchres = PrefixSearch::titleSearch( $search, $limit );
// Set top level elements
$result = $this->getResult();
@@ -82,19 +57,27 @@ class ApiOpenSearch extends ApiBase {
$result->addValue(null, 1, $srchres);
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
- 'search' => null
+ 'search' => null,
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => 100,
+ ApiBase :: PARAM_MAX2 => 100
+ )
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
- 'search' => 'Search string'
+ 'search' => 'Search string',
+ 'limit' => 'Maximum amount of results to return'
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'This module implements OpenSearch protocol';
}
@@ -105,7 +88,7 @@ class ApiOpenSearch extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiOpenSearch.php 24099 2007-07-15 00:52:35Z yurik $';
+ return __CLASS__ . ': $Id: ApiOpenSearch.php 30275 2008-01-30 01:07:49Z brion $';
}
}
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
new file mode 100644
index 00000000..7de22252
--- /dev/null
+++ b/includes/api/ApiParamInfo.php
@@ -0,0 +1,167 @@
+<?php
+
+/*
+ * Created on Dec 01, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiParamInfo extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ // Get parameters
+ $params = $this->extractRequestParams();
+ $result = $this->getResult();
+ $r = array();
+ if(is_array($params['modules']))
+ {
+ $modArr = $this->getMain()->getModules();
+ foreach($params['modules'] as $m)
+ {
+ if(!isset($modArr[$m]))
+ {
+ $r['modules'][] = array('name' => $m, 'missing' => '');
+ continue;
+ }
+ $obj = new $modArr[$m]($this->getMain(), $m);
+ $a = $this->getClassInfo($obj);
+ $a['name'] = $m;
+ $r['modules'][] = $a;
+ }
+ $result->setIndexedTagName($r['modules'], 'module');
+ }
+ if(is_array($params['querymodules']))
+ {
+ $queryObj = new ApiQuery($this->getMain(), 'query');
+ $qmodArr = $queryObj->getModules();
+ foreach($params['querymodules'] as $qm)
+ {
+ if(!isset($qmodArr[$qm]))
+ {
+ $r['querymodules'][] = array('name' => $qm, 'missing' => '');
+ continue;
+ }
+ $obj = new $qmodArr[$qm]($this, $qm);
+ $a = $this->getClassInfo($obj);
+ $a['name'] = $qm;
+ $r['querymodules'][] = $a;
+ }
+ $result->setIndexedTagName($r['querymodules'], 'module');
+ }
+ $result->addValue(null, $this->getModuleName(), $r);
+ }
+
+ function getClassInfo($obj)
+ {
+ $result = $this->getResult();
+ $retval['classname'] = get_class($obj);
+ $retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription());
+ $retval['prefix'] = $obj->getModulePrefix();
+ $allowedParams = $obj->getAllowedParams();
+ if(!is_array($allowedParams))
+ return $retval;
+ $retval['parameters'] = array();
+ $paramDesc = $obj->getParamDescription();
+ foreach($obj->getAllowedParams() as $n => $p)
+ {
+ $a = array('name' => $n);
+ if(!is_array($p))
+ {
+ if(is_bool($p))
+ {
+ $a['type'] = 'bool';
+ $a['default'] = ($p ? 'true' : 'false');
+ }
+ if(is_string($p))
+ $a['default'] = $p;
+ $retval['parameters'][] = $a;
+ continue;
+ }
+
+ if(isset($p[ApiBase::PARAM_DFLT]))
+ $a['default'] = $p[ApiBase::PARAM_DFLT];
+ if(isset($p[ApiBase::PARAM_ISMULTI]))
+ if($p[ApiBase::PARAM_ISMULTI])
+ $a['multi'] = '';
+ if(isset($p[ApiBase::PARAM_TYPE]))
+ {
+ $a['type'] = $p[ApiBase::PARAM_TYPE];
+ if(is_array($a['type']))
+ $result->setIndexedTagName($a['type'], 't');
+ }
+ if(isset($p[ApiBase::PARAM_MAX]))
+ $a['max'] = $p[ApiBase::PARAM_MAX];
+ if(isset($p[ApiBase::PARAM_MAX2]))
+ $a['highmax'] = $p[ApiBase::PARAM_MAX2];
+ if(isset($p[ApiBase::PARAM_MIN]))
+ $a['min'] = $p[ApiBase::PARAM_MIN];
+ if(isset($paramDesc[$n]))
+ $a['description'] = (is_array($paramDesc[$n]) ? implode("\n", $paramDesc[$n]) : $paramDesc[$n]);
+ $retval['parameters'][] = $a;
+ }
+ $result->setIndexedTagName($retval['parameters'], 'param');
+ return $retval;
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'modules' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'querymodules' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'modules' => 'List of module names (value of the action= parameter)',
+ 'querymodules' => 'List of query module names (value of prop=, meta= or list= parameter)',
+ );
+ }
+
+ public function getDescription() {
+ return 'Obtain information about certain API parameters';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=paraminfo&modules=parse&querymodules=allpages|siteinfo'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiParse.php 29810 2008-01-15 21:33:08Z catrope $';
+ }
+}
+
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
new file mode 100644
index 00000000..21a21e8d
--- /dev/null
+++ b/includes/api/ApiParse.php
@@ -0,0 +1,202 @@
+<?php
+
+/*
+ * Created on Dec 01, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiParse extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ // Get parameters
+ $params = $this->extractRequestParams();
+ $text = $params['text'];
+ $title = $params['title'];
+ $page = $params['page'];
+ if(!is_null($page) && (!is_null($text) || $title != "API"))
+ $this->dieUsage("The page parameter cannot be used together with the text and title parameters", 'params');
+ $prop = array_flip($params['prop']);
+
+ global $wgParser, $wgUser;
+ if(!is_null($page)) {
+ $titleObj = Title::newFromText($page);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('missingtitle', $page));
+
+ // Try the parser cache first
+ $articleObj = new Article($titleObj);
+ $pcache =& ParserCache::singleton();
+ $p_result = $pcache->get($articleObj, $wgUser);
+ if(!$p_result) {
+ $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, new ParserOptions());
+ global $wgUseParserCache;
+ if($wgUseParserCache)
+ $pcache->save($p_result, $articleObj, $wgUser);
+ }
+ } else {
+ $titleObj = Title::newFromText($title);
+ if(!$titleObj)
+ $titleObj = Title::newFromText("API");
+ $p_result = $wgParser->parse($text, $titleObj, new ParserOptions());
+ }
+
+ // Return result
+ $result = $this->getResult();
+ $result_array = array();
+ if(isset($prop['text'])) {
+ $result_array['text'] = array();
+ $result->setContent($result_array['text'], $p_result->getText());
+ }
+ if(isset($prop['langlinks']))
+ $result_array['langlinks'] = $this->formatLangLinks($p_result->getLanguageLinks());
+ if(isset($prop['categories']))
+ $result_array['categories'] = $this->formatCategoryLinks($p_result->getCategories());
+ if(isset($prop['links']))
+ $result_array['links'] = $this->formatLinks($p_result->getLinks());
+ if(isset($prop['templates']))
+ $result_array['templates'] = $this->formatLinks($p_result->getTemplates());
+ if(isset($prop['images']))
+ $result_array['images'] = array_keys($p_result->getImages());
+ if(isset($prop['externallinks']))
+ $result_array['externallinks'] = array_keys($p_result->getExternalLinks());
+ if(isset($prop['sections']))
+ $result_array['sections'] = $p_result->getSections();
+
+ $result_mapping = array(
+ 'langlinks' => 'll',
+ 'categories' => 'cl',
+ 'links' => 'pl',
+ 'templates' => 'tl',
+ 'images' => 'img',
+ 'externallinks' => 'el',
+ 'sections' => 's',
+ );
+ $this->setIndexedTagNames( $result_array, $result_mapping );
+ $result->addValue( null, $this->getModuleName(), $result_array );
+ }
+
+ private function formatLangLinks( $links ) {
+ $result = array();
+ foreach( $links as $link ) {
+ $entry = array();
+ $bits = split( ':', $link, 2 );
+ $entry['lang'] = $bits[0];
+ $this->getResult()->setContent( $entry, $bits[1] );
+ $result[] = $entry;
+ }
+ return $result;
+ }
+
+ private function formatCategoryLinks( $links ) {
+ $result = array();
+ foreach( $links as $link => $sortkey ) {
+ $entry = array();
+ $entry['sortkey'] = $sortkey;
+ $this->getResult()->setContent( $entry, $link );
+ $result[] = $entry;
+ }
+ return $result;
+ }
+
+ private function formatLinks( $links ) {
+ $result = array();
+ foreach( $links as $ns => $nslinks ) {
+ foreach( $nslinks as $title => $id ) {
+ $entry = array();
+ $entry['ns'] = $ns;
+ $this->getResult()->setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
+ if( $id != 0 )
+ $entry['exists'] = '';
+ $result[] = $entry;
+ }
+ }
+ return $result;
+ }
+
+ private function setIndexedTagNames( &$array, $mapping ) {
+ foreach( $mapping as $key => $name ) {
+ if( isset( $array[$key] ) )
+ $this->getResult()->setIndexedTagName( $array[$key], $name );
+ }
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => array(
+ ApiBase :: PARAM_DFLT => 'API',
+ ),
+ 'text' => null,
+ 'page' => null,
+ 'prop' => array(
+ ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections',
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array(
+ 'text',
+ 'langlinks',
+ 'categories',
+ 'links',
+ 'templates',
+ 'images',
+ 'externallinks',
+ 'sections'
+ )
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'text' => 'Wikitext to parse',
+ 'title' => 'Title of page the text belongs to',
+ 'page' => 'Parse the content of this page. Cannot be used together with text and title',
+ 'prop' => array('Which pieces of information to get.',
+ 'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present'
+ ),
+ );
+ }
+
+ public function getDescription() {
+ return 'This module parses wikitext and returns parser output';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=parse&text={{Project:Sandbox}}'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiParse.php 30262 2008-01-29 14:47:27Z catrope $';
+ }
+}
+
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
new file mode 100644
index 00000000..40a4b73d
--- /dev/null
+++ b/includes/api/ApiProtect.php
@@ -0,0 +1,154 @@
+<?php
+
+/*
+ * Created on Sep 1, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiProtect extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ $titleObj = NULL;
+ if(!isset($params['title']))
+ $this->dieUsageMsg(array('missingparam', 'title'));
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!isset($params['protections']) || empty($params['protections']))
+ $this->dieUsageMsg(array('missingparam', 'protections'));
+
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+
+ $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser);
+ if(!empty($errors))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg(current($errors));
+
+ if(in_array($params['expiry'], array('infinite', 'indefinite', 'never')))
+ $expiry = Block::infinity();
+ else
+ {
+ $expiry = strtotime($params['expiry']);
+ if($expiry < 0 || $expiry == false)
+ $this->dieUsageMsg(array('invalidexpiry'));
+
+ $expiry = wfTimestamp(TS_MW, $expiry);
+ if($expiry < wfTimestampNow())
+ $this->dieUsageMsg(array('pastexpiry'));
+ }
+
+ $protections = array();
+ foreach($params['protections'] as $prot)
+ {
+ $p = explode('=', $prot);
+ $protections[$p[0]] = ($p[1] == 'all' ? '' : $p[1]);
+ if($titleObj->exists() && $p[0] == 'create')
+ $this->dieUsageMsg(array('create-titleexists'));
+ if(!$titleObj->exists() && $p[0] != 'create')
+ $this->dieUsageMsg(array('missingtitles-createonly'));
+ }
+
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ if($titleObj->exists()) {
+ $articleObj = new Article($titleObj);
+ $ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry);
+ } else
+ $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiry);
+ if(!$ok)
+ // This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime?
+ // Just throw an unknown error in this case, as it's very likely to be a race condition
+ $this->dieUsageMsg(array());
+ $dbw->commit();
+ $res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']);
+ if($expiry == Block::infinity())
+ $res['expiry'] = 'infinity';
+ else
+ $res['expiry'] = wfTimestamp(TS_ISO_8601, $expiry);
+
+ if($params['cascade'])
+ $res['cascade'] = '';
+ $res['protections'] = $protections;
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'token' => null,
+ 'protections' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'expiry' => 'infinite',
+ 'reason' => '',
+ 'cascade' => false
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'title' => 'Title of the page you want to restore.',
+ 'token' => 'A protect token previously retrieved through prop=info',
+ 'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)',
+ 'expiry' => 'Expiry timestamp. If set to \'infinite\', \'indefinite\' or \'never\', the protection will never expire.',
+ 'reason' => 'Reason for (un)protecting (optional)',
+ 'cascade' => 'Enable cascading protection (i.e. protect pages included in this page)'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Change the protection level of a page.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000',
+ 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=all|move=all&reason=Lifting%20restrictions'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiProtect.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 76dbb338..29abd859 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -60,9 +60,12 @@ class ApiQuery extends ApiBase {
private $mQueryListModules = array (
'allpages' => 'ApiQueryAllpages',
'alllinks' => 'ApiQueryAllLinks',
+ 'allcategories' => 'ApiQueryAllCategories',
'allusers' => 'ApiQueryAllUsers',
'backlinks' => 'ApiQueryBacklinks',
+ 'blocks' => 'ApiQueryBlocks',
'categorymembers' => 'ApiQueryCategoryMembers',
+ 'deletedrevs' => 'ApiQueryDeletedrevs',
'embeddedin' => 'ApiQueryBacklinks',
'imageusage' => 'ApiQueryBacklinks',
'logevents' => 'ApiQueryLogEvents',
@@ -71,11 +74,14 @@ class ApiQuery extends ApiBase {
'usercontribs' => 'ApiQueryContributions',
'watchlist' => 'ApiQueryWatchlist',
'exturlusage' => 'ApiQueryExtLinksUsage',
+ 'users' => 'ApiQueryUsers',
+ 'random' => 'ApiQueryRandom',
);
private $mQueryMetaModules = array (
'siteinfo' => 'ApiQuerySiteinfo',
'userinfo' => 'ApiQueryUserInfo',
+ 'allmessages' => 'ApiQueryAllmessages',
);
private $mSlaveDB = null;
@@ -143,6 +149,13 @@ class ApiQuery extends ApiBase {
public function getPageSet() {
return $this->mPageSet;
}
+
+ /**
+ * Get the array mapping module names to class names
+ */
+ function getModules() {
+ return array_merge($this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules);
+ }
/**
* Query execution happens in the following steps:
@@ -375,7 +388,7 @@ class ApiQuery extends ApiBase {
* Returns the list of allowed parameters for this module.
* Qurey module also lists all ApiPageSet parameters as its own.
*/
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
@@ -456,8 +469,13 @@ class ApiQuery extends ApiBase {
$psModule = new ApiPageSet($this);
return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters();
}
+
+ // @todo should work correctly
+ public function shouldCheckMaxlag() {
+ return true;
+ }
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => 'Which properties to get for the titles/revisions/pageids',
'list' => 'Which lists to get',
@@ -468,7 +486,7 @@ class ApiQuery extends ApiBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return array (
'Query API module allows applications to get needed pieces of data from the MediaWiki databases,',
'and is loosely based on the Query API interface currently available on all MediaWiki servers.',
@@ -485,7 +503,7 @@ class ApiQuery extends ApiBase {
public function getVersion() {
$psModule = new ApiPageSet($this);
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiQuery.php 24494 2007-07-31 17:53:37Z yurik $';
+ $vers[] = __CLASS__ . ': $Id: ApiQuery.php 30222 2008-01-28 19:05:26Z catrope $';
$vers[] = $psModule->getVersion();
return $vers;
}
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
new file mode 100644
index 00000000..84494876
--- /dev/null
+++ b/includes/api/ApiQueryAllCategories.php
@@ -0,0 +1,142 @@
+<?php
+
+/*
+ * Created on December 12, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate all categories, even the ones that don't have
+ * category pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryAllCategories extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'ac');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ $db = $this->getDB();
+ $params = $this->extractRequestParams();
+
+ $this->addTables('categorylinks');
+ $this->addFields('cl_to');
+
+ if (!is_null($params['from']))
+ $this->addWhere('cl_to>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ if (isset ($params['prefix']))
+ $this->addWhere("cl_to LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+
+ $this->addOption('LIMIT', $params['limit']+1);
+ $this->addOption('ORDER BY', 'cl_to' . ($params['dir'] == 'descending' ? ' DESC' : ''));
+ $this->addOption('DISTINCT');
+
+ $res = $this->select(__METHOD__);
+
+ $pages = array();
+ $count = 0;
+ while ($row = $db->fetchObject($res)) {
+ if (++ $count > $params['limit']) {
+ // We've reached the one extra which shows that there are additional cats to be had. Stop here...
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
+ $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->cl_to));
+ break;
+ }
+
+ // Normalize titles
+ $titleObj = Title::makeTitle(NS_CATEGORY, $row->cl_to);
+ if(!is_null($resultPageSet))
+ $pages[] = $titleObj->getPrefixedText();
+ else
+ // Don't show "Category:" everywhere in non-generator mode
+ $pages[] = $titleObj->getText();
+ }
+ $db->freeResult($res);
+
+ if (is_null($resultPageSet)) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($pages, 'c');
+ $result->addValue('query', $this->getModuleName(), $pages);
+ } else {
+ $resultPageSet->populateFromTitles($pages);
+ }
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'from' => null,
+ 'prefix' => null,
+ 'dir' => array(
+ ApiBase :: PARAM_DFLT => 'ascending',
+ ApiBase :: PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ ),
+ ),
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'from' => 'The category to start enumerating from.',
+ 'prefix' => 'Search for all category titles that begin with this value.',
+ 'dir' => 'Direction to sort in.',
+ 'limit' => 'How many categories to return.'
+ );
+ }
+
+ public function getDescription() {
+ return 'Enumerate all categories';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&generator=allcategories&gacprefix=List&prop=info',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 28216 2007-12-06 18:33:18Z vasilievvv $';
+ }
+}
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index 17f24b65..d5b80644 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -125,7 +125,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'from' => null,
'prefix' => null,
@@ -152,7 +152,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'from' => 'The page title to start enumerating from.',
'prefix' => 'Search for all page titles that begin with this value.',
@@ -163,7 +163,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Enumerate all links that point to a given namespace';
}
@@ -174,6 +174,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllLinks.php 24453 2007-07-30 08:09:15Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index 92bcc1a1..e055b3c5 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -48,8 +48,9 @@ class ApiQueryAllUsers extends ApiQueryBase {
$prop = array_flip($prop);
$fld_editcount = isset($prop['editcount']);
$fld_groups = isset($prop['groups']);
+ $fld_registration = isset($prop['registration']);
} else {
- $fld_editcount = $fld_groups = false;
+ $fld_editcount = $fld_groups = $fld_registration = false;
}
$limit = $params['limit'];
@@ -80,6 +81,9 @@ class ApiQueryAllUsers extends ApiQueryBase {
} else {
$sqlLimit = $limit+1;
}
+
+ if ($fld_registration)
+ $this->addFields('user_registration');
$this->addOption('LIMIT', $sqlLimit);
$this->addTables($tables);
@@ -129,6 +133,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
$lastUserData = array( 'name' => $lastUser );
if ($fld_editcount)
$lastUserData['editcount'] = intval($row->user_editcount);
+ if ($fld_registration)
+ $lastUserData['registration'] = wfTimestamp(TS_ISO_8601, $row->user_registration);
}
@@ -152,7 +158,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$result->addValue('query', $this->getModuleName(), $data);
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'from' => null,
'prefix' => null,
@@ -164,6 +170,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
ApiBase :: PARAM_TYPE => array (
'editcount',
'groups',
+ 'registration',
)
),
'limit' => array (
@@ -176,7 +183,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'from' => 'The user name to start enumerating from.',
'prefix' => 'Search for all page titles that begin with this value.',
@@ -188,7 +195,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Enumerate all registered users';
}
@@ -199,6 +206,6 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllUsers.php 24870 2007-08-17 13:01:35Z robchurch $';
+ return __CLASS__ . ': $Id: ApiQueryAllUsers.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php
new file mode 100644
index 00000000..b7c86a91
--- /dev/null
+++ b/includes/api/ApiQueryAllmessages.php
@@ -0,0 +1,129 @@
+<?php
+
+/*
+ * Created on Dec 1, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * A query action to return messages from site message cache
+ *
+ * @addtogroup API
+ */
+class ApiQueryAllmessages extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'am');
+ }
+
+ public function execute() {
+ global $wgMessageCache;
+ $params = $this->extractRequestParams();
+
+ if(!is_null($params['lang']))
+ {
+ global $wgLang;
+ $wgLang = Language::factory($params['lang']);
+ }
+
+
+ //Determine which messages should we print
+ $messages_target = array();
+ if( $params['messages'] == '*' ) {
+ $wgMessageCache->loadAllMessages();
+ $message_names = array_keys( array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ) );
+ sort( $message_names );
+ $messages_target = $message_names;
+ } else {
+ $messages_target = explode( '|', $params['messages'] );
+ }
+
+ //Filter messages
+ if( isset( $params['filter'] ) ) {
+ $messages_filtered = array();
+ foreach( $messages_target as $message ) {
+ if( strpos( $message, $params['filter'] ) !== false ) { //!== is used because filter can be at the beginnig of the string
+ $messages_filtered[] = $message;
+ }
+ }
+ $messages_target = $messages_filtered;
+ }
+
+ $wgMessageCache->disableTransform();
+
+ //Get all requested messages
+ $messages = array();
+ foreach( $messages_target as $message ) {
+ $message = trim( $message ); //Message list can be formatted like "msg1 | msg2 | msg3", so let's trim() it
+ $messages[$message] = wfMsg( $message );
+ }
+
+ //Print the result
+ $result = $this->getResult();
+ $messages_out = array();
+ foreach( $messages as $name => $value ) {
+ $message = array();
+ $message['name'] = $name;
+ $result->setContent( $message, $value );
+ $messages_out[] = $message;
+ }
+ $result->setIndexedTagName( $messages_out, 'message' );
+ $result->addValue( 'query', $this->getModuleName(), $messages_out );
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'messages' => array (
+ ApiBase :: PARAM_DFLT => '*',
+ ),
+ 'filter' => array(),
+ 'lang' => null,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'messages' => 'Which messages to output. "*" means all messages',
+ 'filter' => 'Return only messages that contains specified string',
+ 'lang' => 'Language code',
+ );
+ }
+
+ public function getDescription() {
+ return 'Return messages from this site.';
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=query&meta=allmessages&amfilter=ipb-',
+ 'api.php?action=query&meta=allmessages&ammessages=august|mainpage&amlang=de',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryAllmessages.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php
index d9715b1a..280d1de2 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllpages.php
@@ -86,6 +86,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$prlevel = $params['prlevel'];
if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*')
$this->addWhereFld('pr_level', $prlevel);
+
+ $this->addOption('DISTINCT');
$forceNameTitleIndex = false;
@@ -93,10 +95,22 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$this->dieUsage('prlevel may not be used without prtype', 'params');
}
- $this->addTables('page');
+ if($params['filterlanglinks'] == 'withoutlanglinks') {
+ $pageName = $this->getDB()->tableName('page');
+ $llName = $this->getDB()->tableName('langlinks');
+ $tables = "$pageName LEFT JOIN $llName ON page_id=ll_from";
+ $this->addWhere('ll_from IS NULL');
+ $this->addTables($tables);
+ $forceNameTitleIndex = false;
+ } else if($params['filterlanglinks'] == 'withlanglinks') {
+ $this->addTables(array('page', 'langlinks'));
+ $this->addWhere('page_id=ll_from');
+ $forceNameTitleIndex = false;
+ } else {
+ $this->addTables('page');
+ }
if ($forceNameTitleIndex)
$this->addOption('USE INDEX', 'name_title');
-
if (is_null($resultPageSet)) {
$this->addFields(array (
@@ -110,7 +124,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$limit = $params['limit'];
$this->addOption('LIMIT', $limit+1);
- $this->addOption('ORDER BY', 'page_namespace, page_title');
+ $this->addOption('ORDER BY', 'page_namespace, page_title' .
+ ($params['dir'] == 'descending' ? ' DESC' : ''));
$res = $this->select(__METHOD__);
@@ -143,7 +158,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
global $wgRestrictionTypes, $wgRestrictionLevels;
return array (
@@ -169,9 +184,11 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
),
'prtype' => array (
ApiBase :: PARAM_TYPE => $wgRestrictionTypes,
+ ApiBase :: PARAM_ISMULTI => true
),
'prlevel' => array (
ApiBase :: PARAM_TYPE => $wgRestrictionLevels,
+ ApiBase :: PARAM_ISMULTI => true
),
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
@@ -179,25 +196,42 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'dir' => array (
+ ApiBase :: PARAM_DFLT => 'ascending',
+ ApiBase :: PARAM_TYPE => array (
+ 'ascending',
+ 'descending'
+ )
+ ),
+ 'filterlanglinks' => array(
+ ApiBase :: PARAM_TYPE => array(
+ 'withlanglinks',
+ 'withoutlanglinks',
+ 'all'
+ ),
+ ApiBase :: PARAM_DFLT => 'all'
)
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'from' => 'The page title to start enumerating from.',
'prefix' => 'Search for all page titles that begin with this value.',
'namespace' => 'The namespace to enumerate.',
'filterredir' => 'Which pages to list.',
+ 'dir' => 'The direction in which to list',
'minsize' => 'Limit to pages with at least this many bytes',
'maxsize' => 'Limit to pages with at most this many bytes',
'prtype' => 'Limit to protected pages only',
'prlevel' => 'The protection level (must be used with apprtype= parameter)',
+ 'filterlanglinks' => 'Filter based on whether a page has langlinks',
'limit' => 'How many total pages to return.'
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Enumerate all pages sequentially in a given namespace';
}
@@ -215,7 +249,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllpages.php 24694 2007-08-09 08:41:58Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryAllpages.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index a676b4bf..1ca5c33a 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -179,7 +179,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
$db->freeResult($res);
- if (is_null($resultPageSet) && !empty($data)) {
+ if (is_null($resultPageSet)) {
$result = $this->getResult();
$result->setIndexedTagName($data, $this->bl_code);
$result->addValue('query', $this->getModuleName(), $data);
@@ -315,7 +315,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
'|' . $lastPageID;
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'title' => null,
@@ -343,7 +343,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.',
'continue' => 'When more results are available, use this to continue.',
@@ -354,7 +354,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
switch ($this->getModuleName()) {
case 'backlinks' :
return 'Find all pages that link to the given page';
@@ -387,7 +387,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBacklinks.php 25476 2007-09-04 14:44:46Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBacklinks.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index c810cfa7..031e3c02 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -110,8 +110,9 @@ abstract class ApiQueryBase extends ApiBase {
if (!is_null($end))
$this->addWhere($field . $before . $db->addQuotes($end));
-
- $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC'));
+
+ if (!isset($this->options['ORDER BY']))
+ $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC'));
}
protected function addOption($name, $value = null) {
@@ -230,7 +231,7 @@ abstract class ApiQueryBase extends ApiBase {
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiQueryBase.php 24533 2007-08-01 22:46:22Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryBase.php 31484 2008-03-03 05:46:20Z brion $';
}
}
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
new file mode 100644
index 00000000..165792b5
--- /dev/null
+++ b/includes/api/ApiQueryBlocks.php
@@ -0,0 +1,239 @@
+<?php
+
+/*
+ * Created on Sep 10, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate all available pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryBlocks extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'bk');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ private function run() {
+ global $wgUser;
+
+ $params = $this->extractRequestParams();
+ $prop = array_flip($params['prop']);
+ $fld_id = isset($prop['id']);
+ $fld_user = isset($prop['user']);
+ $fld_by = isset($prop['by']);
+ $fld_timestamp = isset($prop['timestamp']);
+ $fld_expiry = isset($prop['expiry']);
+ $fld_reason = isset($prop['reason']);
+ $fld_range = isset($prop['range']);
+ $fld_flags = isset($prop['flags']);
+
+ $result = $this->getResult();
+ $pageSet = $this->getPageSet();
+ $titles = $pageSet->getTitles();
+ $data = array();
+
+ $this->addTables('ipblocks');
+ if($fld_id)
+ $this->addFields('ipb_id');
+ if($fld_user)
+ $this->addFields(array('ipb_address', 'ipb_user'));
+ if($fld_by)
+ {
+ $this->addTables('user');
+ $this->addFields(array('ipb_by', 'user_name'));
+ $this->addWhere('user_id = ipb_by');
+ }
+ if($fld_timestamp)
+ $this->addFields('ipb_timestamp');
+ if($fld_expiry)
+ $this->addFields('ipb_expiry');
+ if($fld_reason)
+ $this->addFields('ipb_reason');
+ if($fld_range)
+ $this->addFields(array('ipb_range_start', 'ipb_range_end'));
+ if($fld_flags)
+ $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted'));
+
+ $this->addOption('LIMIT', $params['limit'] + 1);
+ $this->addWhereRange('ipb_timestamp', $params['dir'], $params['start'], $params['end']);
+ if(isset($params['ids']))
+ $this->addWhere(array('ipb_id' => $params['ids']));
+ if(isset($params['users']))
+ $this->addWhere(array('ipb_address' => $params['users']));
+ if(!$wgUser->isAllowed('oversight'))
+ $this->addWhere(array('ipb_deleted' => 0));
+
+ // Purge expired entries on one in every 10 queries
+ if(!mt_rand(0, 10))
+ Block::purgeExpired();
+
+ $res = $this->select(__METHOD__);
+ $db = wfGetDB();
+
+ $count = 0;
+ while($row = $db->fetchObject($res))
+ {
+ if($count++ == $params['limit'])
+ {
+ // We've had enough
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp));
+ break;
+ }
+ $block = array();
+ if($fld_id)
+ $block['id'] = $row->ipb_id;
+ if($fld_user && !$row->ipb_auto)
+ {
+ $block['user'] = $row->ipb_address;
+ }
+ if($fld_by)
+ {
+ $block['by'] = $row->user_name;
+ }
+ if($fld_timestamp)
+ $block['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ipb_timestamp);
+ if($fld_expiry)
+ $block['expiry'] = Block::decodeExpiry($row->ipb_expiry, TS_ISO_8601);
+ if($fld_reason)
+ $block['reason'] = $row->ipb_reason;
+ if($fld_range)
+ {
+ $block['rangestart'] = $this->convertHexIP($row->ipb_range_start);
+ $block['rangeend'] = $this->convertHexIP($row->ipb_range_end);
+ }
+ if($fld_flags)
+ {
+ // For clarity, these flags use the same names as their action=block counterparts
+ if($row->ipb_auto)
+ $block['automatic'] = '';
+ if($row->ipb_anon_only)
+ $block['anononly'] = '';
+ if($row->ipb_create_account)
+ $block['nocreate'] = '';
+ if($row->ipb_enable_autoblock)
+ $block['autoblock'] = '';
+ if($row->ipb_block_email)
+ $block['noemail'] = '';
+ if($row->ipb_deleted)
+ $block['hidden'] = '';
+ }
+ $data[] = $block;
+ }
+ $result->setIndexedTagName($data, 'block');
+ $result->addValue('query', $this->getModuleName(), $data);
+ }
+
+ protected function convertHexIP($ip)
+ {
+ // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format
+ $dec = wfBaseConvert($ip, 16, 10);
+ $parts[0] = (int)($dec / (256*256*256));
+ $dec %= 256*256*256;
+ $parts[1] = (int)($dec / (256*256));
+ $dec %= 256*256;
+ $parts[2] = (int)($dec / 256);
+ $parts[3] = $dec % 256;
+ return implode('.', $parts);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'start' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
+ ),
+ 'end' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp',
+ ),
+ 'dir' => array(
+ ApiBase :: PARAM_TYPE => array(
+ 'newer',
+ 'older'
+ ),
+ ApiBase :: PARAM_DFLT => 'older'
+ ),
+ 'ids' => array(
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'users' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'limit' => array(
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'prop' => array(
+ ApiBase :: PARAM_DFLT => 'id|user|by|timestamp|expiry|reason|flags',
+ ApiBase :: PARAM_TYPE => array(
+ 'id',
+ 'user',
+ 'by',
+ 'timestamp',
+ 'expiry',
+ 'reason',
+ 'range',
+ 'flags'
+ ),
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'start' => 'The timestamp to start enumerating from',
+ 'end' => 'The timestamp to stop enumerating at',
+ 'dir' => 'The direction in which to enumerate',
+ 'ids' => 'Pipe-separated list of block IDs to list (optional)',
+ 'users' => 'Pipe-separated list of users to search for (optional)',
+ 'limit' => 'The maximum amount of blocks to list',
+ 'prop' => 'Which properties to get',
+ );
+ }
+
+ public function getDescription() {
+ return 'List all blocked users and IP addresses.';
+ }
+
+ protected function getExamples() {
+ return array (
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryBlocks.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 42bc1c38..63d42bfa 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -120,7 +120,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$db->freeResult($res);
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
@@ -131,13 +131,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => 'Which additional properties to get for each category.',
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'List all categories the page(s) belong to';
}
@@ -151,7 +151,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategories.php 24092 2007-07-14 19:04:31Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryCategories.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 58a454a5..e831f291 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -51,12 +51,18 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
- $category = $params['category'];
- if (is_null($category))
- $this->dieUsage("Category parameter is required", 'param_category');
- $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
- if ( is_null( $categoryTitle ) )
- $this->dieUsage("Category name $category is not valid", 'param_category');
+ if (is_null($params['category'])) {
+ if (is_null($params['title']))
+ $this->dieUsage("Either the cmcategory or the cmtitle parameter is required", 'notitle');
+ else
+ $categoryTitle = Title::newFromText($params['title']);
+ } else if(is_null($params['title']))
+ $categoryTitle = Title::makeTitleSafe(NS_CATEGORY, $params['category']);
+ else
+ $this->dieUsage("The cmcategory and cmtitle parameters can't be used together", 'titleandcategory');
+
+ if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY )
+ $this->dieUsage("The category name you entered is not valid", 'invalidcategory');
$prop = array_flip($params['prop']);
$fld_ids = isset($prop['ids']);
@@ -78,18 +84,19 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
if($params['sort'] == 'timestamp')
{
$this->addOption('USE INDEX', 'cl_timestamp');
- $this->addOption('ORDER BY', 'cl_to, cl_timestamp');
+ $this->addOption('ORDER BY', 'cl_to, cl_timestamp' . ($params['dir'] == 'desc' ? ' DESC' : ''));
}
else
{
$this->addOption('USE INDEX', 'cl_sortkey');
- $this->addOption('ORDER BY', 'cl_to, cl_sortkey, cl_from');
+ $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . ($params['dir'] == 'desc' ? ' DESC' : '') . ', cl_from');
}
$this->addWhere('cl_from=page_id');
$this->setContinuation($params['continue']);
$this->addWhereFld('cl_to', $categoryTitle->getDBkey());
$this->addWhereFld('page_namespace', $params['namespace']);
+ $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']);
$limit = $params['limit'];
$this->addOption('LIMIT', $limit +1);
@@ -172,9 +179,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
- 'category' => null,
+ 'title' => null,
+ 'category' => null, // DEPRECATED, will be removed in early March
'prop' => array (
ApiBase :: PARAM_DFLT => 'ids|title',
ApiBase :: PARAM_ISMULTI => true,
@@ -203,36 +211,53 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'sortkey',
'timestamp'
)
+ ),
+ 'dir' => array(
+ ApiBase :: PARAM_DFLT => 'asc',
+ ApiBase :: PARAM_TYPE => array(
+ 'asc',
+ 'desc'
+ )
+ ),
+ 'start' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
+ ),
+ 'end' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
)
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
- 'category' => 'Which category to enumerate (required)',
+ 'title' => 'Which category to enumerate (required). Must include Category: prefix',
'prop' => 'What pieces of information to include',
'namespace' => 'Only include pages in these namespaces',
'sort' => 'Property to sort by',
+ 'dir' => 'In which direction to sort',
+ 'start' => 'Timestamp to start listing from',
+ 'end' => 'Timestamp to end listing at',
'continue' => 'For large categories, give the value retured from previous query',
'limit' => 'The maximum number of pages to return.',
+ 'category' => 'DEPRECATED. Like title, but without the Category: prefix.',
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'List all pages in a given category';
}
protected function getExamples() {
return array (
- "Get first 10 pages in the categories [[Physics]]:",
- " api.php?action=query&list=categorymembers&cmcategory=Physics",
- "Get page info about first 10 pages in the categories [[Physics]]:",
- " api.php?action=query&generator=categorymembers&gcmcategory=Physics&prop=info",
+ "Get first 10 pages in [[Category:Physics]]:",
+ " api.php?action=query&list=categorymembers&cmtitle=Category:Physics",
+ "Get page info about first 10 pages in [[Category:Physics]]:",
+ " api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info",
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 25474 2007-09-04 14:30:31Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 30670 2008-02-07 15:17:42Z catrope $';
}
}
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
new file mode 100644
index 00000000..1b7fbdb0
--- /dev/null
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -0,0 +1,235 @@
+<?php
+
+/*
+ * Created on Jul 2, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate all available pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryDeletedrevs extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'dr');
+ }
+
+ public function execute() {
+
+ global $wgUser;
+ // Before doing anything at all, let's check permissions
+ if(!$wgUser->isAllowed('deletedhistory'))
+ $this->dieUsage('You don\'t have permission to view deleted revision information', 'permissiondenied');
+
+ $db = $this->getDB();
+ $params = $this->extractRequestParams(false);
+ $prop = array_flip($params['prop']);
+ $fld_revid = isset($prop['revid']);
+ $fld_user = isset($prop['user']);
+ $fld_comment = isset($prop['comment']);
+ $fld_minor = isset($prop['minor']);
+ $fld_len = isset($prop['len']);
+ $fld_content = isset($prop['content']);
+ $fld_token = isset($prop['token']);
+
+ $result = $this->getResult();
+ $pageSet = $this->getPageSet();
+ $titles = $pageSet->getTitles();
+ $data = array();
+
+ $this->addTables('archive');
+ $this->addFields(array('ar_title', 'ar_namespace', 'ar_timestamp'));
+ if($fld_revid)
+ $this->addFields('ar_rev_id');
+ if($fld_user)
+ $this->addFields('ar_user_text');
+ if($fld_comment)
+ $this->addFields('ar_comment');
+ if($fld_minor)
+ $this->addFields('ar_minor_edit');
+ if($fld_len)
+ $this->addFields('ar_len');
+ if($fld_content)
+ {
+ $this->addTables('text');
+ $this->addFields(array('ar_text', 'ar_text_id', 'old_text', 'old_flags'));
+ $this->addWhere('ar_text_id = old_id');
+
+ // This also means stricter restrictions
+ if(!$wgUser->isAllowed('undelete'))
+ $this->dieUsage('You don\'t have permission to view deleted revision content', 'permissiondenied');
+ }
+ // Check limits
+ $userMax = $fld_content ? ApiBase :: LIMIT_SML1 : ApiBase :: LIMIT_BIG1;
+ $botMax = $fld_content ? ApiBase :: LIMIT_SML2 : ApiBase :: LIMIT_BIG2;
+ if( $limit == 'max' ) {
+ $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+ $this->getResult()->addValue( 'limits', 'limit', $limit );
+ }
+ $this->validateLimit('limit', $params['limit'], 1, $userMax, $botMax);
+ if($fld_token)
+ // Undelete tokens are identical for all pages, so we cache one here
+ $token = $wgUser->editToken();
+
+ // We need a custom WHERE clause that matches all titles.
+ if(count($titles) > 0)
+ {
+ $lb = new LinkBatch($titles);
+ $where = $lb->constructSet('ar', $db);
+ $this->addWhere($where);
+ }
+
+ $this->addOption('LIMIT', $params['limit'] + 1);
+ $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']);
+ if(isset($params['namespace']))
+ $this->addWhereFld('ar_namespace', $params['namespace']);
+ $res = $this->select(__METHOD__);
+ $pages = array();
+ $count = 0;
+ // First populate the $pages array
+ while($row = $db->fetchObject($res))
+ {
+ if($count++ == $params['limit'])
+ {
+ // We've had enough
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp));
+ break;
+ }
+
+ $rev = array();
+ $rev['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ar_timestamp);
+ if($fld_revid)
+ $rev['revid'] = $row->ar_rev_id;
+ if($fld_user)
+ $rev['user'] = $row->ar_user_text;
+ if($fld_comment)
+ $rev['comment'] = $row->ar_comment;
+ if($fld_minor)
+ if($row->ar_minor_edit == 1)
+ $rev['minor'] = '';
+ if($fld_len)
+ $rev['len'] = $row->ar_len;
+ if($fld_content)
+ ApiResult::setContent($rev, Revision::getRevisionText($row));
+
+ $t = Title::makeTitle($row->ar_namespace, $row->ar_title);
+ if(!isset($pages[$t->getPrefixedText()]))
+ {
+ $pages[$t->getPrefixedText()] = array(
+ 'title' => $t->getPrefixedText(),
+ 'ns' => intval($row->ar_namespace),
+ 'revisions' => array($rev)
+ );
+ if($fld_token)
+ $pages[$t->getPrefixedText()]['token'] = $token;
+ }
+ else
+ $pages[$t->getPrefixedText()]['revisions'][] = $rev;
+ }
+ $db->freeResult($res);
+
+ // We don't want entire pagenames as keys, so let's make this array indexed
+ foreach($pages as $page)
+ {
+ $result->setIndexedTagName($page['revisions'], 'rev');
+ $data[] = $page;
+ }
+ $result->setIndexedTagName($data, 'page');
+ $result->addValue('query', $this->getModuleName(), $data);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'start' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
+ ),
+ 'end' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp',
+ ),
+ 'dir' => array(
+ ApiBase :: PARAM_TYPE => array(
+ 'newer',
+ 'older'
+ ),
+ ApiBase :: PARAM_DFLT => 'older'
+ ),
+ 'namespace' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => 'namespace'
+ ),
+ 'limit' => array(
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'prop' => array(
+ ApiBase :: PARAM_DFLT => 'user|comment',
+ ApiBase :: PARAM_TYPE => array(
+ 'revid',
+ 'user',
+ 'comment',
+ 'minor',
+ 'len',
+ 'content',
+ 'token'
+ ),
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'start' => 'The timestamp to start enumerating from',
+ 'end' => 'The timestamp to stop enumerating at',
+ 'dir' => 'The direction in which to enumerate',
+ 'namespace' => 'The namespaces to search in',
+ 'limit' => 'The maximum amount of revisions to list',
+ 'prop' => 'Which properties to get'
+ );
+ }
+
+ public function getDescription() {
+ return 'List deleted revisions.';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'List the first 50 deleted revisions in the Category and Category talk namespaces',
+ ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=14|15',
+ 'List the last deleted revisions of Main Page and Talk:Main Page, with content:',
+ ' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 385ae65b..896a0171 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -134,7 +134,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
global $wgUrlProtocols;
$protocols = array();
foreach ($wgUrlProtocols as $p) {
@@ -173,7 +173,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => 'What pieces of information to include',
'offset' => 'Used for paging. Use the value returned for "continue"',
@@ -184,7 +184,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Enumerate pages that contain a given URL';
}
@@ -195,6 +195,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 24694 2007-08-09 08:41:58Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index 440b31d6..07183910 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -75,7 +75,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
$db->freeResult($res);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Returns all external urls (not interwikies) from the given page(s)';
}
@@ -87,7 +87,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 23819 2007-07-07 03:05:09Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index 3d568ba1..3714ccf6 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -42,15 +42,20 @@ class ApiQueryImageInfo extends ApiQueryBase {
public function execute() {
$params = $this->extractRequestParams();
- $history = $params['history'];
-
$prop = array_flip($params['prop']);
- $fld_timestamp = isset($prop['timestamp']);
- $fld_user = isset($prop['user']);
- $fld_comment = isset($prop['comment']);
- $fld_url = isset($prop['url']);
- $fld_size = isset($prop['size']);
- $fld_sha1 = isset($prop['sha1']);
+ $this->fld_timestamp = isset($prop['timestamp']);
+ $this->fld_user = isset($prop['user']);
+ $this->fld_comment = isset($prop['comment']);
+ $this->fld_url = isset($prop['url']);
+ $this->fld_size = isset($prop['size']);
+ $this->fld_sha1 = isset($prop['sha1']);
+ $this->fld_metadata = isset($prop['metadata']);
+
+ if($params['urlheight'] != -1 && $params['urlwidth'] == -1)
+ $this->dieUsage("iiurlheight cannot be used without iiurlwidth", 'iiurlwidth');
+ $this->scale = ($params['urlwidth'] != -1);
+ $this->urlwidth = $params['urlwidth'];
+ $this->urlheight = $params['urlheight'];
$pageIds = $this->getPageSet()->getAllTitlesByNamespace();
if (!empty($pageIds[NS_IMAGE])) {
@@ -65,54 +70,84 @@ class ApiQueryImageInfo extends ApiQueryBase {
} else {
$repository = $img->getRepoName();
-
- $isCur = true;
- while($line = $img->nextHistoryLine()) { // assignment
- $row = get_object_vars( $line );
- $vals = array();
- $prefix = $isCur ? 'img' : 'oi';
-
- if ($fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row["${prefix}_timestamp"]);
- if ($fld_user) {
- $vals['user'] = $row["${prefix}_user_text"];
- if(!$row["${prefix}_user"])
- $vals['anon'] = '';
- }
- if ($fld_size) {
- $vals['size'] = intval($row["{$prefix}_size"]);
- $vals['width'] = intval($row["{$prefix}_width"]);
- $vals['height'] = intval($row["{$prefix}_height"]);
- }
- if ($fld_url)
- $vals['url'] = $isCur ? $img->getURL() : $img->getArchiveUrl($row["oi_archive_name"]);
- if ($fld_comment)
- $vals['comment'] = $row["{$prefix}_description"];
-
- if ($fld_sha1)
- $vals['sha1'] = wfBaseConvert($row["{$prefix}_sha1"], 36, 16, 40);
-
- $data[] = $vals;
-
- if (!$history) // Stop after the first line.
- break;
-
- $isCur = false;
+
+ // Get information about the current version first
+ // Check that the current version is within the start-end boundaries
+ if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) &&
+ (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) {
+ $data[] = $this->getInfo($img);
}
- $img->resetHistory();
+ // Now get the old revisions
+ // Get one more to facilitate query-continue functionality
+ $count = count($data);
+ $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']);
+ foreach($oldies as $oldie) {
+ if(++$count > $params['limit']) {
+ // We've reached the extra one which shows that there are additional pages to be had. Stop here...
+ // Only set a query-continue if there was only one title
+ if(count($pageIds[NS_IMAGE]) == 1)
+ $this->setContinueEnumParameter('start', $oldie->getTimestamp());
+ break;
+ }
+ $data[] = $this->getInfo($oldie);
+ }
}
- $this->getResult()->addValue(array ('query', 'pages', intval($pageId)),
- 'imagerepository',
- $repository);
- if (!empty($data))
- $this->addPageSubItems($pageId, $data);
+ $this->getResult()->addValue(array(
+ 'query', 'pages', intval($pageId)),
+ 'imagerepository', $repository
+ );
+ if (!empty($data))
+ $this->addPageSubItems($pageId, $data);
}
}
}
- protected function getAllowedParams() {
+ /**
+ * Get result information for an image revision
+ * @param File f The image
+ * @return array Result array
+ */
+ protected function getInfo($f) {
+ $vals = array();
+ if($this->fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $f->getTimestamp());
+ if($this->fld_user) {
+ $vals['user'] = $f->getUser();
+ if(!$f->getUser('id'))
+ $vals['anon'] = '';
+ }
+ if($this->fld_size) {
+ $vals['size'] = intval($f->getSize());
+ $vals['width'] = intval($f->getWidth());
+ $vals['height'] = intval($f->getHeight());
+ }
+ if($this->fld_url) {
+ if($this->scale && !$f->isOld()) {
+ $thumb = $f->getThumbnail($this->urlwidth, $this->urlheight);
+ if($thumb)
+ {
+ $vals['thumburl'] = $thumb->getURL();
+ $vals['thumbwidth'] = $thumb->getWidth();
+ $vals['thumbheight'] = $thumb->getHeight();
+ }
+ }
+ $vals['url'] = $f->getURL();
+ }
+ if($this->fld_comment)
+ $vals['comment'] = $f->getDescription();
+ if($this->fld_sha1)
+ $vals['sha1'] = wfBaseConvert($f->getSha1(), 36, 16, 40);
+ if($this->fld_metadata) {
+ $metadata = unserialize($f->getMetadata());
+ $vals['metadata'] = $metadata ? $metadata : null;
+ $this->getResult()->setIndexedTagName_recursive($vals['metadata'], 'meta');
+ }
+ return $vals;
+ }
+
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
@@ -123,21 +158,46 @@ class ApiQueryImageInfo extends ApiQueryBase {
'comment',
'url',
'size',
- 'sha1'
+ 'sha1',
+ 'metadata'
)
),
- 'history' => false,
+ 'limit' => array(
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_DFLT => 1,
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'start' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
+ ),
+ 'end' => array(
+ ApiBase :: PARAM_TYPE => 'timestamp'
+ ),
+ 'urlwidth' => array(
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_DFLT => -1
+ ),
+ 'urlheight' => array(
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_DFLT => -1
+ )
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => 'What image information to get.',
- 'history' => 'Include upload history',
+ 'limit' => 'How many image revisions to return',
+ 'start' => 'Timestamp to start listing from',
+ 'end' => 'Timestamp to stop listing at',
+ 'urlwidth' => 'If iiprop=url is set, a URL to an image scaled to this width will be returned. Only the current version of the image can be scaled.',
+ 'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth',
);
}
- protected function getDescription() {
+ public function getDescription() {
return array (
'Returns image information and upload history'
);
@@ -146,11 +206,11 @@ class ApiQueryImageInfo extends ApiQueryBase {
protected function getExamples() {
return array (
'api.php?action=query&titles=Image:Albert%20Einstein%20Head.jpg&prop=imageinfo',
- 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iihistory&iiprop=timestamp|user|url',
+ 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iilimit=50&iiend=20071231235959&iiprop=timestamp|user|url',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImageInfo.php 25456 2007-09-03 19:58:05Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryImageInfo.php 30665 2008-02-07 12:21:48Z catrope $';
}
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index d64a653b..f7405374 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -98,7 +98,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$db->freeResult($res);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Returns all images contained on the given page(s)';
}
@@ -112,7 +112,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImages.php 24092 2007-07-14 19:04:31Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryImages.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index bebf4006..2dee22b0 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -40,6 +40,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
public function requestExtraData($pageSet) {
+ $pageSet->requestField('page_restrictions');
$pageSet->requestField('page_is_redirect');
$pageSet->requestField('page_is_new');
$pageSet->requestField('page_counter');
@@ -65,11 +66,16 @@ class ApiQueryInfo extends ApiQueryBase {
$tok_protect = $this->getTokenFlag($token, 'protect');
$tok_move = $this->getTokenFlag($token, 'move');
}
+ else
+ // Fix E_NOTICEs about unset variables
+ $token = $tok_edit = $tok_delete = $tok_protect = $tok_move = null;
$pageSet = $this->getPageSet();
$titles = $pageSet->getGoodTitles();
+ $missing = $pageSet->getMissingTitles();
$result = $this->getResult();
+ $pageRestrictions = $pageSet->getCustomField('page_restrictions');
$pageIsRedir = $pageSet->getCustomField('page_is_redirect');
$pageIsNew = $pageSet->getCustomField('page_is_new');
$pageCounter = $pageSet->getCustomField('page_counter');
@@ -77,23 +83,46 @@ class ApiQueryInfo extends ApiQueryBase {
$pageLatest = $pageSet->getCustomField('page_latest');
$pageLength = $pageSet->getCustomField('page_len');
- if ($fld_protection && count($titles) > 0) {
+ $db = $this->getDB();
+ if ($fld_protection && !empty($titles)) {
$this->addTables('page_restrictions');
- $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry'));
+ $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade'));
$this->addWhereFld('pr_page', array_keys($titles));
- $db = $this->getDB();
$res = $this->select(__METHOD__);
while($row = $db->fetchObject($res)) {
- $protections[$row->pr_page][] = array(
- 'type' => $row->pr_type,
- 'level' => $row->pr_level,
- 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
- );
+ $a = array(
+ 'type' => $row->pr_type,
+ 'level' => $row->pr_level,
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
+ );
+ if($row->pr_cascade)
+ $a['cascade'] = '';
+ $protections[$row->pr_page][] = $a;
}
$db->freeResult($res);
}
-
+ // We don't need to check for pt stuff if there are no nonexistent titles
+ if($fld_protection && !empty($missing))
+ {
+ $this->resetQueryParams();
+ // Construct a custom WHERE clause that matches all titles in $missing
+ $lb = new LinkBatch($missing);
+ $this->addTables('protected_titles');
+ $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry'));
+ $this->addWhere($lb->constructSet('pt', $db));
+ $res = $this->select(__METHOD__);
+ $prottitles = array();
+ while($row = $db->fetchObject($res)) {
+ $prottitles[$row->pt_namespace][$row->pt_title] = array(
+ 'type' => 'create',
+ 'level' => $row->pt_create_perm,
+ 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601)
+ );
+ }
+ $db->freeResult($res);
+ }
+
foreach ( $titles as $pageid => $title ) {
$pageInfo = array (
'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]),
@@ -125,7 +154,36 @@ class ApiQueryInfo extends ApiQueryBase {
$pageInfo['protection'] = $protections[$pageid];
$result->setIndexedTagName($pageInfo['protection'], 'pr');
} else {
- $pageInfo['protection'] = array();
+ # Also check old restrictions
+ if( $pageRestrictions[$pageid] ) {
+ foreach( explode( ':', trim( $pageRestrictions[$pageid] ) ) as $restrict ) {
+ $temp = explode( '=', trim( $restrict ) );
+ if(count($temp) == 1) {
+ // old old format should be treated as edit/move restriction
+ $restriction = trim( $temp[0] );
+ $pageInfo['protection'][] = array(
+ 'type' => 'edit',
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ $pageInfo['protection'][] = array(
+ 'type' => 'move',
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ } else {
+ $restriction = trim( $temp[1] );
+ $pageInfo['protection'][] = array(
+ 'type' => $temp[0],
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ }
+ }
+ $result->setIndexedTagName($pageInfo['protection'], 'pr');
+ } else {
+ $pageInfo['protection'] = array();
+ }
}
}
@@ -135,18 +193,31 @@ class ApiQueryInfo extends ApiQueryBase {
), $pageid, $pageInfo);
}
- // Get edit tokens for missing titles if requested
- // Delete, protect and move tokens are N/A for missing titles anyway
- if($tok_edit)
+ // Get edit/protect tokens and protection data for missing titles if requested
+ // Delete and move tokens are N/A for missing titles anyway
+ if($tok_edit || $tok_protect || $fld_protection)
{
- $missing = $pageSet->getMissingTitles();
- $res = $result->getData();
- foreach($missing as $pageid => $title)
- $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken();
+ $res = &$result->getData();
+ foreach($missing as $pageid => $title) {
+ if($tok_edit)
+ $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken();
+ if($tok_protect)
+ $res['query']['pages'][$pageid]['protecttoken'] = $wgUser->editToken();
+ if($fld_protection)
+ {
+ // Apparently the XML formatting code doesn't like array(null)
+ // This is painful to fix, so we'll just work around it
+ if(isset($prottitles[$title->getNamespace()][$title->getDBkey()]))
+ $res['query']['pages'][$pageid]['protection'][] = $prottitles[$title->getNamespace()][$title->getDBkey()];
+ else
+ $res['query']['pages'][$pageid]['protection'] = array();
+ $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr');
+ }
+ }
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_DFLT => NULL,
@@ -166,7 +237,7 @@ class ApiQueryInfo extends ApiQueryBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => array (
'Which additional properties to get:',
@@ -177,7 +248,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
- protected function getDescription() {
+ public function getDescription() {
return 'Get basic page information such as namespace, title, last touched date, ...';
}
@@ -189,7 +260,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryInfo.php 25457 2007-09-03 20:17:53Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryInfo.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index ae5ff790..04a930db 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -76,7 +76,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
$db->freeResult($res);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Returns all interlanguage links from the given page(s)';
}
@@ -88,7 +88,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLangLinks.php 23819 2007-07-07 03:05:09Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryLangLinks.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 7ec20f44..d77e627a 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -123,7 +123,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$db->freeResult($res);
}
- protected function getAllowedParams()
+ public function getAllowedParams()
{
return array(
'namespace' => array(
@@ -133,14 +133,14 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription()
+ public function getParamDescription()
{
return array(
'namespace' => "Show {$this->description}s in this namespace(s) only"
);
}
- protected function getDescription() {
+ public function getDescription() {
return "Returns all {$this->description}s from the given page(s)";
}
@@ -156,7 +156,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLinks.php 24092 2007-07-14 19:04:31Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryLinks.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 0f143658..e25e5275 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -198,7 +198,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
global $wgLogTypes;
return array (
'prop' => array (
@@ -243,8 +243,9 @@ class ApiQueryLogEvents extends ApiQueryBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
+ 'prop' => 'Which properties to get',
'type' => 'Filter log entries to only this type(s)',
'start' => 'The timestamp to start enumerating from.',
'end' => 'The timestamp to end enumerating.',
@@ -255,7 +256,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Get events from logs.';
}
@@ -266,7 +267,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLogEvents.php 24256 2007-07-18 21:47:09Z robchurch $';
+ return __CLASS__ . ': $Id: ApiQueryLogEvents.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
new file mode 100644
index 00000000..b8282098
--- /dev/null
+++ b/includes/api/ApiQueryRandom.php
@@ -0,0 +1,157 @@
+<?php
+
+/*
+ * Created on Monday, January 28, 2008
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Brent Garber
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to get list of random pages
+ *
+ * @addtogroup API
+ */
+
+ class ApiQueryRandom extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'rn');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) {
+ $this->resetQueryParams();
+ $this->addTables('page');
+ $this->addOption('LIMIT', $limit);
+ $this->addWhereFld('page_namespace', $namespace);
+ $this->addWhereRange('page_random', 'newer', $randstr, null);
+ $this->addWhere(array('page_is_redirect' => 0));
+ $this->addOption('USE INDEX', 'page_random');
+ if(is_null($resultPageSet))
+ $this->addFields(array('page_id', 'page_title', 'page_namespace'));
+ else
+ $this->addFields($resultPageSet->getPageTableFields());
+ }
+
+ protected function runQuery(&$data, &$resultPageSet) {
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+ $count = 0;
+ while($row = $db->fetchObject($res)) {
+ $count++;
+ if(is_null($resultPageSet))
+ {
+ // Prevent duplicates
+ if(!in_array($row->page_id, $this->pageIDs))
+ {
+ $data[] = $this->extractRowInfo($row);
+ $this->pageIDs[] = $row->page_id;
+ }
+ }
+ else
+ $resultPageSet->processDbRow($row);
+ }
+ $db->freeResult($res);
+ return $count;
+ }
+
+ public function run($resultPageSet = null) {
+ $params = $this->extractRequestParams();
+ $result = $this->getResult();
+ $data = array();
+ $this->pageIDs = array();
+ $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet);
+ $count = $this->runQuery($data, $resultPageSet);
+ if($count < $params['limit'])
+ {
+ /* We got too few pages, we probably picked a high value
+ * for page_random. We'll just take the lowest ones, see
+ * also the comment in Title::getRandomTitle()
+ */
+ $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet);
+ $this->runQuery($data, $resultPageSet);
+ }
+
+ if(is_null($resultPageSet)) {
+ $result->setIndexedTagName($data, 'page');
+ $result->addValue('query', $this->getModuleName(), $data);
+ }
+ }
+
+ private function extractRowInfo($row) {
+ $title = Title::makeTitle($row->page_namespace, $row->page_title);
+ $vals = array();
+ $vals['title'] = $title->getPrefixedText();
+ $vals['ns'] = $row->page_namespace;
+ $vals['id'] = $row->page_id;
+ return $vals;
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'namespace' => array(
+ ApiBase :: PARAM_TYPE => 'namespace',
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'limit' => array (
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_DFLT => 1,
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => 10,
+ ApiBase :: PARAM_MAX2 => 20
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'namespace' => 'Return pages in these namespaces only',
+ 'limit' => 'Limit how many random pages will be returned'
+ );
+ }
+
+ public function getDescription() {
+ return array( 'Get a set of random pages',
+ 'NOTE: Pages are listed in a fixed sequence, only the starting point is random. This means that if, for example, "Main Page" is the first ',
+ ' random page on your list, "List of fictional monkeys" will *always* be second, "List of people on stamps of Vanuatu" third, etc.',
+ 'NOTE: If the number of pages in the namespace is lower than rnlimit, you will get fewer pages. You will not get the same page twice.'
+ );
+ }
+
+ protected function getExamples() {
+ return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2';
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$';
+ }
+}
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 309beaf9..44093854 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -44,20 +44,40 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$fld_timestamp = false, $fld_title = false, $fld_ids = false,
$fld_sizes = false;
+ /**
+ * Generates and outputs the result of this query based upon the provided parameters.
+ */
public function execute() {
- $limit = $prop = $namespace = $show = $dir = $start = $end = null;
+ /* Initialize vars */
+ $limit = $prop = $namespace = $show = $type = $dir = $start = $end = null;
+
+ /* Get the parameters of the request. */
extract($this->extractRequestParams());
+ /* Build our basic query. Namely, something along the lines of:
+ * SELECT * from recentchanges WHERE rc_timestamp > $start
+ * AND rc_timestamp < $end AND rc_namespace = $namespace
+ * AND rc_deleted = '0'
+ */
$this->addTables('recentchanges');
$this->addWhereRange('rc_timestamp', $dir, $start, $end);
$this->addWhereFld('rc_namespace', $namespace);
$this->addWhereFld('rc_deleted', 0);
+ if(!is_null($type))
+ $this->addWhereFld('rc_type', $this->parseRCType($type));
if (!is_null($show)) {
$show = array_flip($show);
- if ((isset ($show['minor']) && isset ($show['!minor'])) || (isset ($show['bot']) && isset ($show['!bot'])) || (isset ($show['anon']) && isset ($show['!anon'])))
+
+ /* Check for conflicting parameters. */
+ if ((isset ($show['minor']) && isset ($show['!minor']))
+ || (isset ($show['bot']) && isset ($show['!bot']))
+ || (isset ($show['anon']) && isset ($show['!anon']))) {
+
$this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+ }
+ /* Add additional conditions to query depending upon parameters. */
$this->addWhereIf('rc_minor = 0', isset ($show['!minor']));
$this->addWhereIf('rc_minor != 0', isset ($show['minor']));
$this->addWhereIf('rc_bot = 0', isset ($show['!bot']));
@@ -66,6 +86,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addWhereIf('rc_user != 0', isset ($show['!anon']));
}
+ /* Add the fields we're concerned with to out query. */
$this->addFields(array (
'rc_timestamp',
'rc_namespace',
@@ -75,9 +96,11 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'rc_moved_to_title'
));
+ /* Determine what properties we need to display. */
if (!is_null($prop)) {
$prop = array_flip($prop);
+ /* Set up internal members based upon params. */
$this->fld_comment = isset ($prop['comment']);
$this->fld_user = isset ($prop['user']);
$this->fld_flags = isset ($prop['flags']);
@@ -85,7 +108,8 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->fld_title = isset ($prop['title']);
$this->fld_ids = isset ($prop['ids']);
$this->fld_sizes = isset ($prop['sizes']);
-
+
+ /* Add fields to our query if they are specified as a needed parameter. */
$this->addFieldsIf('rc_id', $this->fld_ids);
$this->addFieldsIf('rc_cur_id', $this->fld_ids);
$this->addFieldsIf('rc_this_oldid', $this->fld_ids);
@@ -100,14 +124,21 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addFieldsIf('rc_new_len', $this->fld_sizes);
}
+ /* Specify the limit for our query. It's $limit+1 because we (possibly) need to
+ * generate a "continue" parameter, to allow paging. */
$this->addOption('LIMIT', $limit +1);
+
+ /* Specify the index to use in the query as rc_timestamp, instead of rc_revid (default). */
$this->addOption('USE INDEX', 'rc_timestamp');
$data = array ();
$count = 0;
+
+ /* Perform the actual query. */
$db = $this->getDB();
$res = $this->select(__METHOD__);
-
+
+ /* Iterate through the rows, adding data extracted from them to our query result. */
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
@@ -115,33 +146,61 @@ class ApiQueryRecentChanges extends ApiQueryBase {
break;
}
+ /* Extract the data from a single row. */
$vals = $this->extractRowInfo($row);
+
+ /* Add that row's data to our final output. */
if($vals)
$data[] = $vals;
}
+
$db->freeResult($res);
+ /* Format the result */
$result = $this->getResult();
$result->setIndexedTagName($data, 'rc');
$result->addValue('query', $this->getModuleName(), $data);
}
+ /**
+ * Extracts from a single sql row the data needed to describe one recent change.
+ *
+ * @param $row The row from which to extract the data.
+ * @return An array mapping strings (descriptors) to their respective string values.
+ * @access private
+ */
private function extractRowInfo($row) {
+ /* If page was moved somewhere, get the title of the move target. */
$movedToTitle = false;
if (!empty($row->rc_moved_to_title))
$movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title);
+ /* Determine the title of the page that has been changed. */
$title = Title :: makeTitle($row->rc_namespace, $row->rc_title);
+
+ /* Our output data. */
$vals = array ();
- $vals['type'] = intval($row->rc_type);
+ $type = intval ( $row->rc_type );
+ /* Determine what kind of change this was. */
+ switch ( $type ) {
+ case RC_EDIT: $vals['type'] = 'edit'; break;
+ case RC_NEW: $vals['type'] = 'new'; break;
+ case RC_MOVE: $vals['type'] = 'move'; break;
+ case RC_LOG: $vals['type'] = 'log'; break;
+ case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break;
+ default: $vals['type'] = $type;
+ }
+
+ /* Create a new entry in the result for the title. */
if ($this->fld_title) {
ApiQueryBase :: addTitleInfo($vals, $title);
if ($movedToTitle)
ApiQueryBase :: addTitleInfo($vals, $movedToTitle, "new_");
}
+ /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */
if ($this->fld_ids) {
$vals['rcid'] = intval($row->rc_id);
$vals['pageid'] = intval($row->rc_cur_id);
@@ -149,12 +208,14 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$vals['old_revid'] = intval( $row->rc_last_oldid );
}
+ /* Add user data and 'anon' flag, if use is anonymous. */
if ($this->fld_user) {
$vals['user'] = $row->rc_user_text;
if(!$row->rc_user)
$vals['anon'] = '';
}
+ /* Add flags, such as new, minor, bot. */
if ($this->fld_flags) {
if ($row->rc_bot)
$vals['bot'] = '';
@@ -164,22 +225,42 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$vals['minor'] = '';
}
+ /* Add sizes of each revision. (Only available on 1.10+) */
if ($this->fld_sizes) {
$vals['oldlen'] = intval($row->rc_old_len);
$vals['newlen'] = intval($row->rc_new_len);
}
-
+
+ /* Add the timestamp. */
if ($this->fld_timestamp)
$vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
+ /* Add edit summary / log summary. */
if ($this->fld_comment && !empty ($row->rc_comment)) {
$vals['comment'] = $row->rc_comment;
}
return $vals;
}
+
+ private function parseRCType($type)
+ {
+ if(is_array($type))
+ {
+ $retval = array();
+ foreach($type as $t)
+ $retval[] = $this->parseRCType($t);
+ return $retval;
+ }
+ switch($type)
+ {
+ case 'edit': return RC_EDIT;
+ case 'new': return RC_NEW;
+ case 'log': return RC_LOG;
+ }
+ }
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'start' => array (
ApiBase :: PARAM_TYPE => 'timestamp'
@@ -228,11 +309,19 @@ class ApiQueryRecentChanges extends ApiQueryBase {
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'type' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'edit',
+ 'new',
+ 'log'
+ )
)
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'start' => 'The timestamp to start enumerating from.',
'end' => 'The timestamp to end enumerating.',
@@ -243,11 +332,12 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'Show only items that meet this criteria.',
'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
),
+ 'type' => 'Which types of changes to show.',
'limit' => 'How many total pages to return.'
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Enumerate recent changes';
}
@@ -258,7 +348,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 24100 2007-07-15 01:12:54Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index 2672478b..e22d3b30 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -45,14 +45,15 @@ class ApiQueryRevisions extends ApiQueryBase {
$fld_comment = false, $fld_user = false, $fld_content = false;
public function execute() {
- $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = null;
- extract($this->extractRequestParams());
+ $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $token = null;
+ extract($this->extractRequestParams(false));
// If any of those parameters are used, work in 'enumeration' mode.
// Enum mode can only be used when exactly one page is provided.
- // Enumerating revisions on multiple pages make it extremelly
- // difficult to manage continuations and require additional sql indexes
+ // Enumerating revisions on multiple pages make it extremely
+ // difficult to manage continuations and require additional SQL indexes
$enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end));
+
$pageSet = $this->getPageSet();
$pageCount = $pageSet->getGoodTitleCount();
@@ -66,7 +67,7 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids');
if ($pageCount > 1 && $enumRevMode)
- $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start, and end parameters may only be used on a single page.', 'multpages');
+ $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages');
$this->addTables('revision');
$this->addWhere('rev_deleted=0');
@@ -84,12 +85,20 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp']));
$this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment']));
$this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size']));
+ $this->tok_rollback = false; // Prevent PHP undefined property notice
+ if(!is_null($token))
+ {
+ $this->tok_rollback = $this->getTokenFlag($token, 'rollback');
+ }
if (isset ($prop['user'])) {
$this->addFields('rev_user');
$this->addFields('rev_user_text');
$this->fld_user = true;
}
+ else if($this->tok_rollback)
+ $this->addFields('rev_user_text');
+
if (isset ($prop['content'])) {
// For each page we will request, the user must have read rights for that page
@@ -105,15 +114,22 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->addFields('old_id');
$this->addFields('old_text');
$this->addFields('old_flags');
+
$this->fld_content = true;
+
+ $this->expandTemplates = $expandtemplates;
}
- $userMax = ($this->fld_content ? 50 : 500);
- $botMax = ($this->fld_content ? 200 : 10000);
+ $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
+ $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
+ if( $limit == 'max' ) {
+ $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+ $this->getResult()->addValue( 'limits', $this->getModuleName(), $limit );
+ }
if ($enumRevMode) {
- // This is mostly to prevent parameter errors (and optimize sql?)
+ // This is mostly to prevent parameter errors (and optimize SQL?)
if (!is_null($startid) && !is_null($start))
$this->dieUsage('start and startid cannot be used together', 'badparams');
@@ -130,7 +146,7 @@ class ApiQueryRevisions extends ApiQueryBase {
// one row with the same timestamp for the same page.
// The order needs to be the same as start parameter to avoid SQL filesort.
- if (is_null($startid))
+ if (is_null($startid) && is_null($endid))
$this->addWhereRange('rev_timestamp', $dir, $start, $end);
else
$this->addWhereRange('rev_id', $dir, $startid, $endid);
@@ -201,7 +217,7 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->extractRowInfo($row));
}
$db->freeResult($res);
-
+
// Ensure that all revisions are shown as '<rev>' elements
$result = $this->getResult();
if ($result->getIsRawMode()) {
@@ -244,14 +260,27 @@ class ApiQueryRevisions extends ApiQueryBase {
$vals['comment'] = $row->rev_comment;
}
- if ($this->fld_content) {
- ApiResult :: setContent($vals, Revision :: getRevisionText($row));
+ if($this->tok_rollback || ($this->fld_content && $this->expandTemplates))
+ $title = Title::newFromID($row->rev_page);
+
+ if($this->tok_rollback) {
+ global $wgUser;
+ $vals['rollbacktoken'] = $wgUser->editToken(array($title->getPrefixedText(), $row->rev_user_text));
}
+
+ if ($this->fld_content) {
+ $text = Revision :: getRevisionText($row);
+ if ($this->expandTemplates) {
+ global $wgParser;
+ $text = $wgParser->preprocess( $text, $title, new ParserOptions() );
+ }
+ ApiResult :: setContent($vals, $text);
+ }
return $vals;
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
@@ -269,8 +298,8 @@ class ApiQueryRevisions extends ApiQueryBase {
'limit' => array (
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX => ApiBase :: LIMIT_SML1,
- ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
),
'startid' => array (
ApiBase :: PARAM_TYPE => 'integer'
@@ -296,11 +325,19 @@ class ApiQueryRevisions extends ApiQueryBase {
),
'excludeuser' => array(
ApiBase :: PARAM_TYPE => 'user'
- )
+ ),
+
+ 'expandtemplates' => false,
+ 'token' => array(
+ ApiBase :: PARAM_TYPE => array(
+ 'rollback'
+ ),
+ ApiBase :: PARAM_ISMULTI => true
+ ),
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => 'Which properties to get for each revision.',
'limit' => 'limit how many revisions will be returned (enum)',
@@ -311,10 +348,12 @@ class ApiQueryRevisions extends ApiQueryBase {
'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)',
'user' => 'only include revisions made by user',
'excludeuser' => 'exclude revisions made by user',
+ 'expandtemplates' => 'expand templates in revision content',
+ 'token' => 'Which tokens to obtain for each revision',
);
}
- protected function getDescription() {
+ public function getDescription() {
return array (
'Get revision information.',
'This module may be used in several ways:',
@@ -343,7 +382,7 @@ class ApiQueryRevisions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRevisions.php 25407 2007-09-02 14:00:11Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryRevisions.php 31259 2008-02-25 14:14:55Z catrope $';
}
}
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 268616b1..b15f36ce 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -94,7 +94,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'search' => null,
'namespace' => array (
@@ -121,7 +121,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'search' => 'Search for all page titles (or content) that has this value.',
'namespace' => 'The namespace(s) to enumerate.',
@@ -132,7 +132,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Perform a full text search';
}
@@ -145,7 +145,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySearch.php 24453 2007-07-30 08:09:15Z yurik $';
+ return __CLASS__ . ': $Id: ApiQuerySearch.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 1fa3d8fc..81af7997 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -53,6 +53,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'namespaces' :
$this->appendNamespaces($p);
break;
+ case 'namespacealiases' :
+ $this->appendNamespaceAliases($p);
+ break;
case 'interwikimap' :
$filteriw = isset($params['filteriw']) ? $params['filteriw'] : false;
$this->appendInterwikiMap($p, $filteriw);
@@ -68,7 +71,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function appendGeneralInfo($property) {
- global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode;
+ global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode, $IP;
$data = array ();
$mainPage = Title :: newFromText(wfMsgForContent('mainpage'));
@@ -76,6 +79,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['base'] = $mainPage->getFullUrl();
$data['sitename'] = $wgSitename;
$data['generator'] = "MediaWiki $wgVersion";
+
+ $svn = SpecialVersion::getSvnRevision ( $IP );
+ if ( $svn ) $data['rev'] = $svn;
+
$data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future
if (isset($wgRightsCode))
$data['rightscode'] = $wgRightsCode;
@@ -100,6 +107,22 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->getResult()->addValue('query', $property, $data);
}
+ protected function appendNamespaceAliases($property) {
+ global $wgNamespaceAliases;
+
+ $data = array ();
+ foreach ($wgNamespaceAliases as $title => $ns) {
+ $item = array (
+ 'id' => $ns
+ );
+ ApiResult :: setContent($item, strtr($title, '_', ' '));
+ $data[] = $item;
+ }
+
+ $this->getResult()->setIndexedTagName($data, 'ns');
+ $this->getResult()->addValue('query', $property, $data);
+ }
+
protected function appendInterwikiMap($property, $filter) {
$this->resetQueryParams();
@@ -177,7 +200,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->getResult()->addValue('query', $property, $data);
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
@@ -186,6 +209,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
ApiBase :: PARAM_TYPE => array (
'general',
'namespaces',
+ 'namespacealiases',
'interwikimap',
'dbrepllag',
'statistics',
@@ -201,12 +225,13 @@ class ApiQuerySiteinfo extends ApiQueryBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => array (
'Which sysinfo properties to get:',
' "general" - Overall system information',
' "namespaces" - List of registered namespaces (localized)',
+ ' "namespacealiases" - List of registered namespace aliases',
' "statistics" - Returns site statistics',
' "interwikimap" - Returns interwiki map (optionally filtered)',
' "dbrepllag" - Returns database server with the highest replication lag',
@@ -216,19 +241,19 @@ class ApiQuerySiteinfo extends ApiQueryBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Return general information about the site.';
}
protected function getExamples() {
return array(
- 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|statistics',
+ 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics',
'api.php?action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local',
'api.php?action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 25238 2007-08-28 15:37:31Z robchurch $';
+ return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 30484 2008-02-03 19:29:59Z btongminh $';
}
-} \ No newline at end of file
+}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 05c3d945..57d51cdb 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -60,7 +60,11 @@ class ApiQueryContributions extends ApiQueryBase {
$db = $this->getDB();
// Prepare query
- $this->prepareUsername();
+ $this->usernames = array();
+ if(!is_array($this->params['user']))
+ $this->params['user'] = array($this->params['user']);
+ foreach($this->params['user'] as $u)
+ $this->prepareUsername($u);
$this->prepareQuery();
//Do the actual query.
@@ -96,8 +100,7 @@ class ApiQueryContributions extends ApiQueryBase {
* Validate the 'user' parameter and set the value to compare
* against `revision`.`rev_user_text`
*/
- private function prepareUsername() {
- $user = $this->params['user'];
+ private function prepareUsername($user) {
if( $user ) {
$name = User::isIP( $user )
? $user
@@ -105,7 +108,7 @@ class ApiQueryContributions extends ApiQueryBase {
if( $name === false ) {
$this->dieUsage( "User name {$user} is not valid", 'param_user' );
} else {
- $this->username = $name;
+ $this->usernames[] = $name;
}
} else {
$this->dieUsage( 'User parameter may not be empty', 'param_user' );
@@ -123,14 +126,11 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page");
$this->addWhereFld('rev_deleted', 0);
-
- // We only want pages by the specified user.
- $this->addWhereFld( 'rev_user_text', $this->username );
-
+ // We only want pages by the specified users.
+ $this->addWhereFld( 'rev_user_text', $this->usernames );
// ... and in the specified timeframe.
$this->addWhereRange('rev_timestamp',
$this->params['dir'], $this->params['start'], $this->params['end'] );
-
$this->addWhereFld('page_namespace', $this->params['namespace']);
$show = $this->params['show'];
@@ -142,15 +142,16 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addWhereIf('rev_minor_edit = 0', isset ($show['!minor']));
$this->addWhereIf('rev_minor_edit != 0', isset ($show['minor']));
}
-
$this->addOption('LIMIT', $this->params['limit'] + 1);
// Mandatory fields: timestamp allows request continuation
- // ns+title checks if the user has access rights for this page
+ // ns+title checks if the user has access rights for this page
+ // user_text is necessary if multiple users were specified
$this->addFields(array(
'rev_timestamp',
'page_namespace',
'page_title',
+ 'rev_user_text',
));
$this->addFieldsIf('rev_page', $this->fld_ids);
@@ -158,8 +159,6 @@ class ApiQueryContributions extends ApiQueryBase {
// $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed?
$this->addFieldsIf('rev_comment', $this->fld_comment);
$this->addFieldsIf('rev_minor_edit', $this->fld_flags);
-
- // These fields depend only work if the page table is joined
$this->addFieldsIf('page_is_new', $this->fld_flags);
}
@@ -170,6 +169,7 @@ class ApiQueryContributions extends ApiQueryBase {
$vals = array();
+ $vals['user'] = $row->rev_user_text;
if ($this->fld_ids) {
$vals['pageid'] = intval($row->rev_page);
$vals['revid'] = intval($row->rev_id);
@@ -196,7 +196,7 @@ class ApiQueryContributions extends ApiQueryBase {
return $vals;
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
@@ -212,7 +212,7 @@ class ApiQueryContributions extends ApiQueryBase {
ApiBase :: PARAM_TYPE => 'timestamp'
),
'user' => array (
- ApiBase :: PARAM_TYPE => 'user'
+ ApiBase :: PARAM_ISMULTI => true
),
'dir' => array (
ApiBase :: PARAM_DFLT => 'older',
@@ -246,7 +246,7 @@ class ApiQueryContributions extends ApiQueryBase {
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'limit' => 'The maximum number of contributions to return.',
'start' => 'The start timestamp to return from.',
@@ -259,7 +259,7 @@ class ApiQueryContributions extends ApiQueryBase {
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Get all edits by a user';
}
@@ -270,7 +270,7 @@ class ApiQueryContributions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserContributions.php 24754 2007-08-13 18:18:18Z robchurch $';
+ return __CLASS__ . ': $Id: ApiQueryUserContributions.php 30578 2008-02-05 15:40:58Z catrope $';
}
}
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index a41b8679..010d9f4f 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -40,50 +40,90 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
public function execute() {
-
- global $wgUser;
-
$params = $this->extractRequestParams();
$result = $this->getResult();
+ $r = array();
+ if (!is_null($params['prop'])) {
+ $this->prop = array_flip($params['prop']);
+ } else {
+ $this->prop = array();
+ }
+ $r = $this->getCurrentUserInfo();
+ $result->addValue("query", $this->getModuleName(), $r);
+ }
+
+ protected function getCurrentUserInfo() {
+ global $wgUser;
+ $result = $this->getResult();
$vals = array();
+ $vals['id'] = $wgUser->getId();
$vals['name'] = $wgUser->getName();
- if( $wgUser->isAnon() ) $vals['anon'] = '';
-
- if (!is_null($params['prop'])) {
- $prop = array_flip($params['prop']);
- if (isset($prop['blockinfo'])) {
- if ($wgUser->isBlocked()) {
- $vals['blockedby'] = User::whoIs($wgUser->blockedBy());
- $vals['blockreason'] = $wgUser->blockedFor();
- }
- }
- if (isset($prop['hasmsg']) && $wgUser->getNewtalk()) {
- $vals['messages'] = '';
- }
- if (isset($prop['groups'])) {
- $vals['groups'] = $wgUser->getGroups();
- $result->setIndexedTagName($vals['groups'], 'g'); // even if empty
- }
- if (isset($prop['rights'])) {
- $vals['rights'] = $wgUser->getRights();
- $result->setIndexedTagName($vals['rights'], 'r'); // even if empty
+ if($wgUser->isAnon())
+ $vals['anon'] = '';
+ if (isset($this->prop['blockinfo'])) {
+ if ($wgUser->isBlocked()) {
+ $vals['blockedby'] = User::whoIs($wgUser->blockedBy());
+ $vals['blockreason'] = $wgUser->blockedFor();
}
+ }
+ if (isset($this->prop['hasmsg']) && $wgUser->getNewtalk()) {
+ $vals['messages'] = '';
}
-
- if (!empty($params['option'])) {
- foreach( $params['option'] as $option ) {
- if (empty($option))
- $this->dieUsage('Empty value is not allowed for the option parameter', 'option');
- $vals['options'][$option] = $wgUser->getOption($option);
- }
+ if (isset($this->prop['groups'])) {
+ $vals['groups'] = $wgUser->getGroups();
+ $result->setIndexedTagName($vals['groups'], 'g'); // even if empty
}
-
- $result->addValue(null, $this->getModuleName(), $vals);
+ if (isset($this->prop['rights'])) {
+ $vals['rights'] = $wgUser->getRights();
+ $result->setIndexedTagName($vals['rights'], 'r'); // even if empty
+ }
+ if (isset($this->prop['options'])) {
+ $vals['options'] = (is_null($wgUser->mOptions) ? User::getDefaultOptions() : $wgUser->mOptions);
+ }
+ if (isset($this->prop['editcount'])) {
+ $vals['editcount'] = $wgUser->getEditCount();
+ }
+ if (isset($this->prop['ratelimits'])) {
+ $vals['ratelimits'] = $this->getRateLimits();
+ }
+ return $vals;
}
+
+ protected function getRateLimits()
+ {
+ global $wgUser, $wgRateLimits;
+ if(!$wgUser->isPingLimitable())
+ return array(); // No limits
+
+ // Find out which categories we belong to
+ $categories = array();
+ if($wgUser->isAnon())
+ $categories[] = 'anon';
+ else
+ $categories[] = 'user';
+ if($wgUser->isNewBie())
+ {
+ $categories[] = 'ip';
+ $categories[] = 'subnet';
+ if(!$wgUser->isAnon())
+ $categories[] = 'newbie';
+ }
+
+ // Now get the actual limits
+ $retval = array();
+ foreach($wgRateLimits as $action => $limits)
+ foreach($categories as $cat)
+ if(isset($limits[$cat]) && !is_null($limits[$cat]))
+ {
+ $retval[$action][$cat]['hits'] = $limits[$cat][0];
+ $retval[$action][$cat]['seconds'] = $limits[$cat][1];
+ }
+ return $retval;
+ }
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_DFLT => NULL,
@@ -93,28 +133,30 @@ class ApiQueryUserInfo extends ApiQueryBase {
'hasmsg',
'groups',
'rights',
- )),
- 'option' => array (
- ApiBase :: PARAM_DFLT => NULL,
- ApiBase :: PARAM_ISMULTI => true,
- ),
+ 'options',
+ 'editcount',
+ 'ratelimits'
+ )
+ )
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'prop' => array(
'What pieces of information to include',
- ' blockinfo - tags if the user is blocked, by whom, and for what reason',
- ' hasmsg - adds a tag "message" if user has pending messages',
- ' groups - lists all the groups the current user belongs to',
- ' rights - lists of all rights the current user has',
- ),
- 'option' => 'A list of user preference options to get',
+ ' blockinfo - tags if the current user is blocked, by whom, and for what reason',
+ ' hasmsg - adds a tag "message" if the current user has pending messages',
+ ' groups - lists all the groups the current user belongs to',
+ ' rights - lists of all rights the current user has',
+ ' options - lists all preferences the current user has set',
+ ' editcount - adds the current user\'s edit count',
+ ' ratelimits - lists all rate limits applying to the current user'
+ )
);
}
- protected function getDescription() {
+ public function getDescription() {
return 'Get information about the current user';
}
@@ -122,12 +164,10 @@ class ApiQueryUserInfo extends ApiQueryBase {
return array (
'api.php?action=query&meta=userinfo',
'api.php?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg',
- 'api.php?action=query&meta=userinfo&uioption=rememberpassword',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserInfo.php 24529 2007-08-01 20:11:29Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30395 2008-02-01 14:46:46Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
new file mode 100644
index 00000000..144bfba2
--- /dev/null
+++ b/includes/api/ApiQueryUsers.php
@@ -0,0 +1,162 @@
+<?php
+
+/*
+ * Created on July 30, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to get information about a list of users
+ *
+ * @addtogroup API
+ */
+
+ class ApiQueryUsers extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'us');
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $result = $this->getResult();
+ $r = array();
+
+ if (!is_null($params['prop'])) {
+ $this->prop = array_flip($params['prop']);
+ } else {
+ $this->prop = array();
+ }
+
+ if(is_array($params['users'])) {
+ $r = $this->getOtherUsersInfo($params['users']);
+ $result->setIndexedTagName($r, 'user');
+ }
+ $result->addValue("query", $this->getModuleName(), $r);
+ }
+
+ protected function getOtherUsersInfo($users) {
+ $goodNames = $retval = array();
+ // Canonicalize user names
+ foreach($users as $u) {
+ $n = User::getCanonicalName($u);
+ if($n === false)
+ $retval[] = array('name' => $u, 'invalid' => '');
+ else
+ $goodNames[] = $n;
+ }
+
+ $db = $this->getDb();
+ $userTable = $db->tableName('user');
+ $tables = "$userTable AS u1";
+ $this->addFields('u1.user_name');
+ $this->addWhereFld('u1.user_name', $goodNames);
+ $this->addFieldsIf('u1.user_editcount', isset($this->prop['editcount']));
+
+ if(isset($this->prop['groups'])) {
+ $ug = $db->tableName('user_groups');
+ $tables = "$tables LEFT JOIN $ug ON ug_user=u1.user_id";
+ $this->addFields('ug_group');
+ }
+ if(isset($this->prop['blockinfo'])) {
+ $ipb = $db->tableName('ipblocks');
+ $tables = "$tables LEFT JOIN $ipb ON ipb_user=u1.user_id";
+ $tables = "$tables LEFT JOIN $userTable AS u2 ON ipb_by=u2.user_id";
+ $this->addFields(array('ipb_reason', 'u2.user_name AS blocker_name'));
+ }
+ $this->addTables($tables);
+
+ $data = array();
+ $res = $this->select(__METHOD__);
+ while(($r = $db->fetchObject($res))) {
+ $data[$r->user_name]['name'] = $r->user_name;
+ if(isset($this->prop['editcount']))
+ $data[$r->user_name]['editcount'] = $r->user_editcount;
+ if(isset($this->prop['groups']))
+ // This row contains only one group, others will be added from other rows
+ if(!is_null($r->ug_group))
+ $data[$r->user_name]['groups'][] = $r->ug_group;
+ if(isset($this->prop['blockinfo']))
+ if(!is_null($r->blocker_name)) {
+ $data[$r->user_name]['blockedby'] = $r->blocker_name;
+ $data[$r->user_name]['blockreason'] = $r->ipb_reason;
+ }
+ }
+
+ // Second pass: add result data to $retval
+ foreach($goodNames as $u) {
+ if(!isset($data[$u]))
+ $retval[] = array('name' => $u, 'missing' => '');
+ else {
+ if(isset($this->prop['groups']) && isset($data[$u]['groups']))
+ $this->getResult()->setIndexedTagName($data[$u]['groups'], 'g');
+ $retval[] = $data[$u];
+ }
+ }
+ return $retval;
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'blockinfo',
+ 'groups',
+ 'editcount'
+ )
+ ),
+ 'users' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'prop' => array(
+ 'What pieces of information to include',
+ ' blockinfo - tags if the user is blocked, by whom, and for what reason',
+ ' groups - lists all the groups the user belongs to',
+ ' editcount - adds the user\'s edit count'
+ ),
+ 'users' => 'A list of users to obtain the same information for'
+ );
+ }
+
+ public function getDescription() {
+ return 'Get information about a list of users';
+ }
+
+ protected function getExamples() {
+ return 'api.php?action=query&list=users&ususers=brion|TimStarling&usprop=groups|editcount';
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30128 2008-01-24 17:59:07Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index 16586a40..91a0c951 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -59,7 +59,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if (!$wgUser->isLoggedIn())
$this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
- $allrev = $start = $end = $namespace = $dir = $limit = $prop = null;
+ $allrev = $start = $end = $namespace = $dir = $limit = $prop = $show = null;
extract($this->extractRequestParams());
if (!is_null($prop) && is_null($resultPageSet)) {
@@ -135,7 +135,28 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addWhereFld('wl_namespace', $namespace);
$this->addWhereIf('rc_this_oldid=page_latest', !$allrev);
- # This is a index optimization for mysql, as done in the Special:Watchlist page
+ if (!is_null($show)) {
+ $show = array_flip($show);
+
+ /* Check for conflicting parameters. */
+ if ((isset ($show['minor']) && isset ($show['!minor']))
+ || (isset ($show['bot']) && isset ($show['!bot']))
+ || (isset ($show['anon']) && isset ($show['!anon']))) {
+
+ $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+ }
+
+ /* Add additional conditions to query depending upon parameters. */
+ $this->addWhereIf('rc_minor = 0', isset ($show['!minor']));
+ $this->addWhereIf('rc_minor != 0', isset ($show['minor']));
+ $this->addWhereIf('rc_bot = 0', isset ($show['!bot']));
+ $this->addWhereIf('rc_bot != 0', isset ($show['bot']));
+ $this->addWhereIf('rc_user = 0', isset ($show['anon']));
+ $this->addWhereIf('rc_user != 0', isset ($show['!anon']));
+ }
+
+
+ # This is an index optimization for mysql, as done in the Special:Watchlist page
$this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql');
$this->addOption('LIMIT', $limit +1);
@@ -222,7 +243,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
return $vals;
}
- protected function getAllowedParams() {
+ public function getAllowedParams() {
return array (
'allrev' => false,
'start' => array (
@@ -262,11 +283,22 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'patrol',
'sizes',
)
+ ),
+ 'show' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'minor',
+ '!minor',
+ 'bot',
+ '!bot',
+ 'anon',
+ '!anon'
+ )
)
);
}
- protected function getParamDescription() {
+ public function getParamDescription() {
return array (
'allrev' => 'Include multiple revisions of the same page within given timeframe.',
'start' => 'The timestamp to start enumerating from.',
@@ -274,11 +306,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'namespace' => 'Filter changes to only the given namespace(s).',
'dir' => 'In which direction to enumerate pages.',
'limit' => 'How many total pages to return per request.',
- 'prop' => 'Which additional items to get (non-generator mode only).'
+ 'prop' => 'Which additional items to get (non-generator mode only).',
+ 'show' => array (
+ 'Show only items that meet this criteria.',
+ 'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
+ )
);
}
- protected function getDescription() {
+ public function getDescription() {
return '';
}
@@ -293,7 +329,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlist.php 24092 2007-07-14 19:04:31Z yurik $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlist.php 30222 2008-01-28 19:05:26Z catrope $';
}
}
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index a318d808..ffab51ef 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -139,6 +139,22 @@ class ApiResult extends ApiBase {
// Do not use setElement() as it is ok to call this more than once
$arr['_element'] = $tag;
}
+
+ /**
+ * Calls setIndexedTagName() on $arr and each sub-array
+ */
+ public function setIndexedTagName_recursive(&$arr, $tag)
+ {
+ if(!is_array($arr))
+ return;
+ foreach($arr as $a)
+ {
+ if(!is_array($a))
+ continue;
+ $this->setIndexedTagName($a, $tag);
+ $this->setIndexedTagName_recursive($a, $tag);
+ }
+ }
/**
* Add value to the output data at the given path.
@@ -175,7 +191,34 @@ class ApiResult extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiResult.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiResult.php 26855 2007-10-20 18:27:39Z catrope $';
}
}
+/* For compatibility with PHP versions < 5.1.0, define our own array_intersect_key function. */
+if (!function_exists('array_intersect_key')) {
+ function array_intersect_key($isec, $keys) {
+ $argc = func_num_args();
+
+ if ($argc > 2) {
+ for ($i = 1; !empty($isec) && $i < $argc; $i++) {
+ $arr = func_get_arg($i);
+
+ foreach (array_keys($isec) as $key) {
+ if (!isset($arr[$key]))
+ unset($isec[$key]);
+ }
+ }
+
+ return $isec;
+ } else {
+ $res = array();
+ foreach (array_keys($isec) as $key) {
+ if (isset($keys[$key]))
+ $res[$key] = $isec[$key];
+ }
+
+ return $res;
+ }
+ }
+}
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
new file mode 100644
index 00000000..d714f99c
--- /dev/null
+++ b/includes/api/ApiRollback.php
@@ -0,0 +1,128 @@
+<?php
+
+/*
+ * Created on Jun 20, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiRollback extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ $titleObj = NULL;
+ if(!isset($params['title']))
+ $this->dieUsageMsg(array('missingparam', 'title'));
+ if(!isset($params['user']))
+ $this->dieUsageMsg(array('missingparam', 'user'));
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ if(!$titleObj->exists())
+ $this->dieUsageMsg(array('notanarticle'));
+
+ $username = User::getCanonicalName($params['user']);
+ if(!$username)
+ $this->dieUsageMsg(array('invaliduser', $params['user']));
+
+ $articleObj = new Article($titleObj);
+ $summary = (isset($params['summary']) ? $params['summary'] : "");
+ $details = null;
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details);
+
+ if(!empty($retval))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg(current($retval));
+
+ $dbw->commit();
+ $current = $target = $summary = NULL;
+ extract($details);
+
+ $info = array(
+ 'title' => $titleObj->getPrefixedText(),
+ 'pageid' => $current->getPage(),
+ 'summary' => $summary,
+ 'revid' => $titleObj->getLatestRevID(),
+ 'old_revid' => $current->getID(),
+ 'last_revid' => $target->getID()
+ );
+
+ $this->getResult()->addValue(null, $this->getModuleName(), $info);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'user' => null,
+ 'token' => null,
+ 'summary' => null,
+ 'markbot' => false
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'title' => 'Title of the page you want to rollback.',
+ 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.',
+ 'token' => 'A rollback token previously retrieved through prop=info',
+ 'summary' => 'Custom edit summary. If not set, default summary will be used.',
+ 'markbot' => 'Mark the reverted edits and the revert as bot edits'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Undoes the last edit to the page. If the last user who edited the page made multiple edits in a row,',
+ 'they will all be rolled back. You need to be logged in as a sysop to use this function, see also action=login.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=rollback&title=Main%20Page&user=Catrope&token=123ABC',
+ 'api.php?action=rollback&title=Main%20Page&user=217.121.114.116&token=123ABC&summary=Reverting%20vandalism&markbot=1'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiRollback.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
new file mode 100644
index 00000000..afbd3f0e
--- /dev/null
+++ b/includes/api/ApiUnblock.php
@@ -0,0 +1,124 @@
+<?php
+
+/*
+ * Created on Sep 7, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * API module that facilitates the unblocking of users. Requires API write mode
+ * to be enabled.
+ *
+ * @addtogroup API
+ */
+class ApiUnblock extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ /**
+ * Unblocks the specified user or provides the reason the unblock failed.
+ */
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ if($params['gettoken'])
+ {
+ $res['unblocktoken'] = $wgUser->editToken();
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ return;
+ }
+
+ if(is_null($params['id']) && is_null($params['user']))
+ $this->dieUsageMsg(array('unblock-notarget'));
+ if(!is_null($params['id']) && !is_null($params['user']))
+ $this->dieUsageMsg(array('unblock-idanduser'));
+ if(is_null($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+ if(!$wgUser->isAllowed('block'))
+ $this->dieUsageMsg(array('cantunblock'));
+ if(wfReadOnly())
+ $this->dieUsageMsg(array('readonlytext'));
+
+ $id = $params['id'];
+ $user = $params['user'];
+ $reason = (is_null($params['reason']) ? '' : $params['reason']);
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $retval = IPUnblockForm::doUnblock($id, $user, $reason, $range);
+ if(!empty($retval))
+ $this->dieUsageMsg($retval);
+
+ $dbw->commit();
+ $res['id'] = $id;
+ $res['user'] = $user;
+ $res['reason'] = $reason;
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'id' => null,
+ 'user' => null,
+ 'token' => null,
+ 'gettoken' => false,
+ 'reason' => null,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'id' => 'ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with user',
+ 'user' => 'Username, IP address or IP range you want to unblock. Cannot be used together with id',
+ 'token' => 'An unblock token previously obtained through the gettoken parameter',
+ 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken',
+ 'reason' => 'Reason for unblock (optional)',
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Unblock a user.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=unblock&id=105',
+ 'api.php?action=unblock&user=Bob&reason=Sorry%20Bob'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiUnblock.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
new file mode 100644
index 00000000..b27841a8
--- /dev/null
+++ b/includes/api/ApiUndelete.php
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * Created on Jul 3, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiUndelete extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ $titleObj = NULL;
+ if(!isset($params['title']))
+ $this->dieUsageMsg(array('missingparam', 'title'));
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+
+ if(!$wgUser->isAllowed('undelete'))
+ $this->dieUsageMsg(array('permdenied-undelete'));
+ if($wgUser->isBlocked())
+ $this->dieUsageMsg(array('blockedtext'));
+ if(wfReadOnly())
+ $this->dieUsageMsg(array('readonlytext'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+
+ // Convert timestamps
+ if(!is_array($params['timestamps']))
+ $params['timestamps'] = array($params['timestamps']);
+ foreach($params['timestamps'] as $i => $ts)
+ $params['timestamps'][$i] = wfTimestamp(TS_MW, $ts);
+
+ $pa = new PageArchive($titleObj);
+ $dbw = wfGetDb(DB_MASTER);
+ $dbw->begin();
+ $retval = $pa->undelete((isset($params['timestamps']) ? $params['timestamps'] : array()), $params['reason']);
+ if(!is_array($retval))
+ $this->dieUsageMsg(array('cannotundelete'));
+
+ $dbw->commit();
+ $info['title'] = $titleObj->getPrefixedText();
+ $info['revisions'] = $retval[0];
+ $info['fileversions'] = $retval[1];
+ $info['reason'] = $retval[2];
+ $this->getResult()->addValue(null, $this->getModuleName(), $info);
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'token' => null,
+ 'reason' => "",
+ 'timestamps' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'title' => 'Title of the page you want to restore.',
+ 'token' => 'An undelete token previously retrieved through list=deletedrevs',
+ 'reason' => 'Reason for restoring (optional)',
+ 'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.'
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be',
+ 'retrieved through list=deletedrevs'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=undelete&title=Main%20Page&token=123ABC&reason=Restoring%20main%20page',
+ 'api.php?action=undelete&title=Main%20Page&token=123ABC&timestamps=20070703220045|20070702194856'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiUndelete.php 30222 2008-01-28 19:05:26Z catrope $';
+ }
+}
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
index bd9ff633..cc70b26d 100644
--- a/includes/filerepo/ArchivedFile.php
+++ b/includes/filerepo/ArchivedFile.php
@@ -5,24 +5,70 @@
*/
class ArchivedFile
{
- /**
- * Returns a file object from the filearchive table
- * @param $title, the corresponding image page title
- * @param $id, the image id, a unique key
- * @param $key, optional storage key
- * @return ResultWrapper
+ /**#@+
+ * @private
*/
+ var $id, # filearchive row ID
+ $title, # image title
+ $name, # image name
+ $group, # FileStore storage group
+ $key, # FileStore sha1 key
+ $size, # file dimensions
+ $bits, # size in bytes
+ $width, # width
+ $height, # height
+ $metadata, # metadata string
+ $mime, # mime type
+ $media_type, # media type
+ $description, # upload description
+ $user, # user ID of uploader
+ $user_text, # user name of uploader
+ $timestamp, # time of upload
+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+ $deleted; # Bitfield akin to rev_deleted
+
+ /**#@-*/
+
function ArchivedFile( $title, $id=0, $key='' ) {
- if( !is_object( $title ) ) {
+ if( !is_object($title) ) {
throw new MWException( 'ArchivedFile constructor given bogus title.' );
}
- $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'";
- if( $title->getNamespace() == NS_IMAGE ) {
+ $this->id = -1;
+ $this->title = $title;
+ $this->name = $title->getDBkey();
+ $this->group = '';
+ $this->key = '';
+ $this->size = 0;
+ $this->bits = 0;
+ $this->width = 0;
+ $this->height = 0;
+ $this->metadata = '';
+ $this->mime = "unknown/unknown";
+ $this->media_type = '';
+ $this->description = '';
+ $this->user = 0;
+ $this->user_text = '';
+ $this->timestamp = NULL;
+ $this->deleted = 0;
+ $this->dataLoaded = false;
+ }
+
+ /**
+ * Loads a file object from the filearchive table
+ * @return ResultWrapper
+ */
+ public function load() {
+ if ( $this->dataLoaded ) {
+ return true;
+ }
+ $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'";
+ if( $this->title->getNamespace() == NS_IMAGE ) {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'filearchive',
array(
'fa_id',
'fa_name',
+ 'fa_archive_name',
'fa_storage_key',
'fa_storage_group',
'fa_size',
@@ -39,7 +85,7 @@ class ArchivedFile
'fa_timestamp',
'fa_deleted' ),
array(
- 'fa_name' => $title->getDbKey(),
+ 'fa_name' => $this->title->getDBkey(),
$conds ),
__METHOD__,
array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -52,36 +98,229 @@ class ArchivedFile
$row = $ret->fetchObject();
// initialize fields for filestore image object
- $this->mId = intval($row->fa_id);
- $this->mName = $row->fa_name;
- $this->mGroup = $row->fa_storage_group;
- $this->mKey = $row->fa_storage_key;
- $this->mSize = $row->fa_size;
- $this->mBits = $row->fa_bits;
- $this->mWidth = $row->fa_width;
- $this->mHeight = $row->fa_height;
- $this->mMetaData = $row->fa_metadata;
- $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime";
- $this->mType = $row->fa_media_type;
- $this->mDescription = $row->fa_description;
- $this->mUser = $row->fa_user;
- $this->mUserText = $row->fa_user_text;
- $this->mTimestamp = $row->fa_timestamp;
- $this->mDeleted = $row->fa_deleted;
+ $this->id = intval($row->fa_id);
+ $this->name = $row->fa_name;
+ $this->archive_name = $row->fa_archive_name;
+ $this->group = $row->fa_storage_group;
+ $this->key = $row->fa_storage_key;
+ $this->size = $row->fa_size;
+ $this->bits = $row->fa_bits;
+ $this->width = $row->fa_width;
+ $this->height = $row->fa_height;
+ $this->metadata = $row->fa_metadata;
+ $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+ $this->media_type = $row->fa_media_type;
+ $this->description = $row->fa_description;
+ $this->user = $row->fa_user;
+ $this->user_text = $row->fa_user_text;
+ $this->timestamp = $row->fa_timestamp;
+ $this->deleted = $row->fa_deleted;
} else {
throw new MWException( 'This title does not correspond to an image page.' );
return;
}
+ $this->dataLoaded = true;
+
return true;
}
/**
+ * Loads a file object from the filearchive table
+ * @return ResultWrapper
+ */
+ public static function newFromRow( $row ) {
+ $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) );
+
+ $file->id = intval($row->fa_id);
+ $file->name = $row->fa_name;
+ $file->archive_name = $row->fa_archive_name;
+ $file->group = $row->fa_storage_group;
+ $file->key = $row->fa_storage_key;
+ $file->size = $row->fa_size;
+ $file->bits = $row->fa_bits;
+ $file->width = $row->fa_width;
+ $file->height = $row->fa_height;
+ $file->metadata = $row->fa_metadata;
+ $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+ $file->media_type = $row->fa_media_type;
+ $file->description = $row->fa_description;
+ $file->user = $row->fa_user;
+ $file->user_text = $row->fa_user_text;
+ $file->timestamp = $row->fa_timestamp;
+ $file->deleted = $row->fa_deleted;
+
+ return $file;
+ }
+
+ /**
+ * Return the associated title object
+ * @public
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Return the file name
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ public function getID() {
+ $this->load();
+ return $this->id;
+ }
+
+ /**
+ * Return the FileStore key
+ */
+ public function getKey() {
+ $this->load();
+ return $this->key;
+ }
+
+ /**
+ * Return the FileStore storage group
+ */
+ public function getGroup() {
+ return $file->group;
+ }
+
+ /**
+ * Return the width of the image
+ */
+ public function getWidth() {
+ $this->load();
+ return $this->width;
+ }
+
+ /**
+ * Return the height of the image
+ */
+ public function getHeight() {
+ $this->load();
+ return $this->height;
+ }
+
+ /**
+ * Get handler-specific metadata
+ */
+ public function getMetadata() {
+ $this->load();
+ return $this->metadata;
+ }
+
+ /**
+ * Return the size of the image file, in bytes
+ * @public
+ */
+ public function getSize() {
+ $this->load();
+ return $this->size;
+ }
+
+ /**
+ * Return the bits of the image file, in bytes
+ * @public
+ */
+ public function getBits() {
+ $this->load();
+ return $this->bits;
+ }
+
+ /**
+ * Returns the mime type of the file.
+ */
+ public function getMimeType() {
+ $this->load();
+ return $this->mime;
+ }
+
+ /**
+ * Return the type of the media in the file.
+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
+ */
+ public function getMediaType() {
+ $this->load();
+ return $this->media_type;
+ }
+
+ /**
+ * Return upload timestamp.
+ */
+ public function getTimestamp() {
+ $this->load();
+ return $this->timestamp;
+ }
+
+ /**
+ * Return the user ID of the uploader.
+ */
+ public function getUser() {
+ $this->load();
+ if( $this->isDeleted( File::DELETED_USER ) ) {
+ return 0;
+ } else {
+ return $this->user;
+ }
+ }
+
+ /**
+ * Return the user name of the uploader.
+ */
+ public function getUserText() {
+ $this->load();
+ if( $this->isDeleted( File::DELETED_USER ) ) {
+ return 0;
+ } else {
+ return $this->user_text;
+ }
+ }
+
+ /**
+ * Return upload description.
+ */
+ public function getDescription() {
+ $this->load();
+ if( $this->isDeleted( File::DELETED_COMMENT ) ) {
+ return 0;
+ } else {
+ return $this->description;
+ }
+ }
+
+ /**
+ * Return the user ID of the uploader.
+ */
+ public function getRawUser() {
+ $this->load();
+ return $this->user;
+ }
+
+ /**
+ * Return the user name of the uploader.
+ */
+ public function getRawUserText() {
+ $this->load();
+ return $this->user_text;
+ }
+
+ /**
+ * Return upload description.
+ */
+ public function getRawDescription() {
+ $this->load();
+ return $this->description;
+ }
+
+ /**
* int $field one of DELETED_* bitfield constants
* for file or revision rows
* @return bool
*/
- function isDeleted( $field ) {
- return ($this->mDeleted & $field) == $field;
+ public function isDeleted( $field ) {
+ return ($this->deleted & $field) == $field;
}
/**
@@ -90,19 +329,16 @@ class ArchivedFile
* @param int $field
* @return bool
*/
- function userCan( $field ) {
- if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) {
- // images
+ public function userCan( $field ) {
+ if( ($this->deleted & $field) == $field ) {
global $wgUser;
- $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+ $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
? 'hiderevision'
: 'deleterevision';
- wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+ wfDebug( "Checking for $permission due to $field match on $this->deleted\n" );
return $wgUser->isAllowed( $permission );
} else {
return true;
}
}
}
-
-
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index 84ec9a27..86887d09 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -422,7 +422,7 @@ class FSRepo extends FileRepo {
$status->error( 'filerenameerror', $srcPath, $archivePath );
$good = false;
} else {
- chmod( $archivePath, 0644 );
+ @chmod( $archivePath, 0644 );
}
}
if ( $good ) {
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
index 21b7a865..5172ad0f 100644
--- a/includes/filerepo/File.php
+++ b/includes/filerepo/File.php
@@ -46,7 +46,7 @@ abstract class File {
/**
* The following member variables are not lazy-initialised
*/
- var $repo, $title, $lastError;
+ var $repo, $title, $lastError, $redirected;
/**
* Call this constructor from child classes
@@ -135,20 +135,28 @@ abstract class File {
/**
* Return the associated title object
- * @public
*/
- function getTitle() { return $this->title; }
+ public function getTitle() { return $this->title; }
/**
* Return the URL of the file
- * @public
*/
- function getUrl() {
+ public function getUrl() {
if ( !isset( $this->url ) ) {
$this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
}
return $this->url;
}
+
+ /**
+ * Return a fully-qualified URL to the file.
+ * Upload URL paths _may or may not_ be fully qualified, so
+ * we check. Local paths are assumed to belong on $wgServer.
+ * @return string
+ */
+ public function getFullUrl() {
+ return wfExpandUrl( $this->getUrl() );
+ }
function getViewURL() {
if( $this->mustRender()) {
@@ -173,10 +181,8 @@ abstract class File {
* or in hashed paths like /images/3/3c.
*
* May return false if the file is not locally accessible.
- *
- * @public
*/
- function getPath() {
+ public function getPath() {
if ( !isset( $this->path ) ) {
$this->path = $this->repo->getZonePath('public') . '/' . $this->getRel();
}
@@ -185,9 +191,8 @@ abstract class File {
/**
* Alias for getPath()
- * @public
*/
- function getFullPath() {
+ public function getFullPath() {
return $this->getPath();
}
@@ -210,6 +215,14 @@ abstract class File {
public function getHeight( $page = 1 ) { return false; }
/**
+ * Returns ID or name of user who uploaded the file
+ * STUB
+ *
+ * @param $type string 'text' or 'id'
+ */
+ public function getUser( $type='text' ) { return null; }
+
+ /**
* Get the duration of a media file in seconds
*/
public function getLength() {
@@ -487,9 +500,11 @@ abstract class File {
$script = $this->getTransformScript();
if ( $script && !($flags & self::RENDER_NOW) ) {
- // Use a script to transform on client request
+ // Use a script to transform on client request, if possible
$thumb = $this->handler->getScriptedTransform( $this, $script, $params );
- break;
+ if( $thumb ) {
+ break;
+ }
}
$normalisedParams = $params;
@@ -497,7 +512,7 @@ abstract class File {
$thumbName = $this->thumbName( $normalisedParams );
$thumbPath = $this->getThumbPath( $thumbName );
$thumbUrl = $this->getThumbUrl( $thumbName );
-
+
if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
$thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
break;
@@ -585,7 +600,7 @@ abstract class File {
* STUB
* Overridden by LocalFile
*/
- function purgeCache( $archiveFiles = array() ) {}
+ function purgeCache() {}
/**
* Purge the file description page, but don't go after
@@ -618,6 +633,18 @@ abstract class File {
}
/**
+ * Return a fragment of the history of file.
+ *
+ * STUB
+ * @param $limit integer Limit of rows to return
+ * @param $start timestamp Only revisions older than $start will be returned
+ * @param $end timestamp Only revisions newer than $end will be returned
+ */
+ function getHistory($limit = null, $start = null, $end = null) {
+ return false;
+ }
+
+ /**
* Return the history of this file, line by line. Starts with current version,
* then old versions. Should return an object similar to an image/oldimage
* database row.
@@ -887,7 +914,7 @@ abstract class File {
* STUB
* Overridden by LocalFile
*/
- function delete( $reason, $suppress=false ) {
+ function delete( $reason ) {
$this->readOnlyError();
}
@@ -984,6 +1011,14 @@ abstract class File {
}
/**
+ * Get discription of file revision
+ * STUB
+ */
+ function getDescription() {
+ return null;
+ }
+
+ /**
* Get the 14-character timestamp of the file upload, or false if
* it doesn't exist
*/
@@ -1014,7 +1049,7 @@ abstract class File {
}
/**
- * Get an associative array containing information about a file in the local filesystem\
+ * Get an associative array containing information about a file in the local filesystem.
*
* @param string $path Absolute local filesystem path
* @param mixed $ext The file extension, or true to extract it from the filename.
@@ -1121,6 +1156,14 @@ abstract class File {
return '';
}
}
+
+ function getRedirected() {
+ return $this->redirected;
+ }
+
+ function redirectedFrom( $from ) {
+ $this->redirected = $from;
+ }
}
/**
* Aliases for backwards compatibility with 1.6
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index cf6d65c2..ee7691a6 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -82,7 +82,7 @@ abstract class FileRepo {
if ( !$img ) {
return false;
}
- if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
+ if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
return $img;
}
# Now try an old version of the file
@@ -90,6 +90,19 @@ abstract class FileRepo {
if ( $img->exists() ) {
return $img;
}
+
+ # Now try redirects
+ $redir = $this->checkRedirect( $title );
+ if( $redir && $redir->getNamespace() == NS_IMAGE) {
+ $img = $this->newFile( $redir );
+ if( !$img ) {
+ return false;
+ }
+ if( $img->exists() ) {
+ $img->redirectedFrom( $title->getText() );
+ return $img;
+ }
+ }
}
/**
@@ -400,5 +413,15 @@ abstract class FileRepo {
* STUB
*/
function cleanupDeletedBatch( $storageKeys ) {}
+
+ /**
+ * Checks if there is a redirect named as $title
+ * STUB
+ *
+ * @param Title $title Title of image
+ */
+ function checkRedirect( $title ) {
+ return false;
+ }
}
diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php
index 972b2e46..5dd1dbda 100644
--- a/includes/filerepo/FileRepoStatus.php
+++ b/includes/filerepo/FileRepoStatus.php
@@ -135,22 +135,22 @@ class FileRepoStatus {
}
if ( count( $this->errors ) == 1 ) {
$params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) );
- $s = wfMsgReal( $this->errors[0]['message'], $params );
+ $s = wfMsgReal( $this->errors[0]['message'], $params, true, false, false );
if ( $shortContext ) {
- $s = wfMsg( $shortContext, $s );
+ $s = wfMsgNoTrans( $shortContext, $s );
} elseif ( $longContext ) {
- $s = wfMsg( $longContext, "* $s\n" );
+ $s = wfMsgNoTrans( $longContext, "* $s\n" );
}
} else {
$s = '';
foreach ( $this->errors as $error ) {
$params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) );
- $s .= '* ' . wfMsgReal( $error['message'], $params ) . "\n";
+ $s .= '* ' . wfMsgReal( $error['message'], $params, true, false, false ) . "\n";
}
if ( $longContext ) {
- $s = wfMsg( $longContext, $s );
+ $s = wfMsgNoTrans( $longContext, $s );
} elseif ( $shortContext ) {
- $s = wfMsg( $shortContext, "\n* $s\n" );
+ $s = wfMsgNoTrans( $shortContext, "\n* $s\n" );
}
}
return $s;
diff --git a/includes/filerepo/ICRepo.php b/includes/filerepo/ICRepo.php
index 124fe2b6..ab686f9b 100644
--- a/includes/filerepo/ICRepo.php
+++ b/includes/filerepo/ICRepo.php
@@ -1,24 +1,24 @@
<?php
/**
- * A repository for files accessible via InstantCommons.
+ * A repository for files accessible via InstantCommons.
*/
class ICRepo extends LocalRepo {
- var $directory, $url, $hashLevels, $cache;
+ var $directory, $url, $hashLevels, $cache;
var $fileFactory = array( 'ICFile', 'newFromTitle' );
var $oldFileFactory = false;
function __construct( $info ) {
- parent::__construct( $info );
+ parent::__construct( $info );
// Required settings
$this->directory = $info['directory'];
$this->url = $info['url'];
$this->hashLevels = $info['hashLevels'];
if(isset($info['cache'])){
$this->cache = getcwd().'/images/'.$info['cache'];
- }
- }
+ }
+ }
}
/**
@@ -26,30 +26,30 @@ class ICRepo extends LocalRepo {
*/
class ICFile extends LocalFile{
static function newFromTitle($title,$repo){
- return new self($title, $repo);
+ return new self($title, $repo);
}
-
+
/**
* Returns true if the file comes from the local file repository.
*
* @return bool
*/
- function isLocal() {
- return true;
+ function isLocal() {
+ return true;
}
-
+
function load(){
if (!$this->dataLoaded ) {
if ( !$this->loadFromCache() ) {
if(!$this->loadFromDB()){
$this->loadFromIC();
- }
- $this->saveToCache();
+ }
+ $this->saveToCache();
}
$this->dataLoaded = true;
- }
+ }
}
-
+
/**
* Load file metadata from the DB
*/
@@ -62,15 +62,15 @@ class ICFile extends LocalFile{
$dbr = $this->repo->getSlaveDB();
$row = $dbr->selectRow( 'ic_image', $this->getCacheFields( 'img_' ),
- array( 'img_name' => $this->getName() ), __METHOD__ );
+ array( 'img_name' => $this->getName() ), __METHOD__ );
if ( $row ) {
if (trim($row->img_media_type)==NULL) {
$this->upgradeRow();
$this->upgraded = true;
- }
+ }
$this->loadFromRow( $row );
//This means that these files are local so the repository locations are local
- $this->setUrlPathLocal();
+ $this->setUrlPathLocal();
$this->fileExists = true;
//var_dump($this); exit;
} else {
@@ -78,10 +78,10 @@ class ICFile extends LocalFile{
}
wfProfileOut( __METHOD__ );
-
+
return $this->fileExists;
}
-
+
/**
* Fix assorted version-related problems with the image row by reloading it from the file
*/
@@ -110,106 +110,102 @@ class ICFile extends LocalFile{
$this->saveToCache();
wfProfileOut( __METHOD__ );
}
-
+
function exists(){
$this->load();
return $this->fileExists;
-
}
-
+
/**
* Fetch the file from the repository. Check local ic_images table first. If not available, check remote server
- */
- function loadFromIC(){
- # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ */
+ function loadFromIC(){
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
$this->dataLoaded = true;
- $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform;
- if($h = @fopen($icUrl, 'rb')){
- $contents = fread($h, 3000);
- $image = $this->api_xml_to_array($contents);
- if($image['fileExists']){
- foreach($image as $property=>$value){
- if($property=="url"){$value=$this->repo->url.$value; }
- $this->$property = $value;
- }
- if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){
- //Record the image
- $this->recordDownload("Downloaded with InstantCommons");
-
- //Then cache it
- }else{//set fileExists back to false
- $this->fileExists = false;
- }
- }
+ $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform;
+ if($h = @fopen($icUrl, 'rb')){
+ $contents = fread($h, 3000);
+ $image = $this->api_xml_to_array($contents);
+ if($image['fileExists']){
+ foreach($image as $property=>$value){
+ if($property=="url"){$value=$this->repo->url.$value; }
+ $this->$property = $value;
+ }
+ if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){
+ //Record the image
+ $this->recordDownload("Downloaded with InstantCommons");
+
+ //Then cache it
+ }else{//set fileExists back to false
+ $this->fileExists = false;
+ }
+ }
}
- }
-
-
- function setUrlPathLocal(){
- global $wgScriptPath;
- $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath));
- $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform);
+ }
+
+ function setUrlPathLocal(){
+ global $wgScriptPath;
+ $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath));
+ $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform);
$this->repo->directory = $this->repo->cache;//.'/'.rawurlencode($this->title->mDbkeyform);
-
- }
-
- function getThumbPath( $suffix=false ){
- $path = $this->repo->cache;
- if ( $suffix !== false ) {
+
+ }
+
+ function getThumbPath( $suffix=false ){
+ $path = $this->repo->cache;
+ if ( $suffix !== false ) {
$path .= '/thumb/' . rawurlencode( $suffix );
}
return $path;
- }
- function getThumbUrl( $suffix=false ){
- global $wgScriptPath;
+ }
+ function getThumbUrl( $suffix=false ){
+ global $wgScriptPath;
$path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath));
- if ( $suffix !== false ) {
+ if ( $suffix !== false ) {
$path .= '/thumb/' . rawurlencode( $suffix );
}
return $path;
- }
-
- /**
- * Convert the InstantCommons Server API XML Response to an associative array
- */
- function api_xml_to_array($xml){
- preg_match("/<instantcommons><image(.*?)<\/instantcommons>/",$xml,$match);
- preg_match_all("/(.*?=\".*?\")/",$match[1], $matches);
- foreach($matches[1] as $match){
- list($key,$value) = split("=",$match);
- $image[trim($key,'<" ')]=trim($value,' "');
- }
- return $image;
- }
-
+ }
+
+ /**
+ * Convert the InstantCommons Server API XML Response to an associative array
+ */
+ function api_xml_to_array($xml){
+ preg_match("/<instantcommons><image(.*?)<\/instantcommons>/",$xml,$match);
+ preg_match_all("/(.*?=\".*?\")/",$match[1], $matches);
+ foreach($matches[1] as $match){
+ list($key,$value) = split("=",$match);
+ $image[trim($key,'<" ')]=trim($value,' "');
+ }
+ return $image;
+ }
+
/**
- * Use cURL to read the content of a URL into a string
- * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot
- * @param string $url - the URL to fetch
- * @param resource $fp - filename to write file contents to
- * @param boolean $bg - call cURL in the background (don't hang page until complete)
- * @param int $timeout - cURL connect timeout
- */
- function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) {
- {
- # Call curl in the background to download the file
- $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &';
- wfDebug('Curl download initiated='.$cmd );
- $success = false;
- $file_contents = array();
- $file_contents['err'] = wfShellExec($cmd, $file_contents['return']);
- if($file_contents['err']==0){//Success
- $success = true;
- }
- }
- return $success;
- }
-
+ * Use cURL to read the content of a URL into a string
+ * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot
+ * @param string $url - the URL to fetch
+ * @param resource $fp - filename to write file contents to
+ * @param boolean $bg - call cURL in the background (don't hang page until complete)
+ * @param int $timeout - cURL connect timeout
+ */
+ function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) {
+ # Call curl in the background to download the file
+ $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &';
+ wfDebug('Curl download initiated='.$cmd );
+ $success = false;
+ $file_contents = array();
+ $file_contents['err'] = wfShellExec($cmd, $file_contents['return']);
+ if($file_contents['err']==0){//Success
+ $success = true;
+ }
+ return $success;
+ }
+
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
$class = 'Database' . ucfirst( $this->dbType );
- $this->dbConn = new $class( $this->dbServer, $this->dbUser,
- $this->dbPassword, $this->dbName, false, $this->dbFlags,
+ $this->dbConn = new $class( $this->dbServer, $this->dbUser,
+ $this->dbPassword, $this->dbName, false, $this->dbFlags,
$this->tablePrefix );
}
return $this->dbConn;
@@ -219,10 +215,10 @@ class ICFile extends LocalFile{
* Record a file upload in the upload log and the image table
*/
private function recordDownload($comment='', $timestamp = false ){
- global $wgUser;
+ global $wgUser;
$dbw = $this->repo->getMasterDB();
-
+
if ( $timestamp === false ) {
$timestamp = $dbw->timestamp();
}
@@ -252,7 +248,7 @@ class ICFile extends LocalFile{
);
if( $dbw->affectedRows() == 0 ) {
- # Collision, this is an update of a file
+ # Collision, this is an update of a file
# Update the current image row
$dbw->update( 'ic_image',
array( /* SET */
@@ -297,7 +293,7 @@ class ICFile extends LocalFile{
$descTitle->purgeSquid();
}
-
+
# Commit the transaction now, in case something goes wrong later
# The most important thing is that files don't get lost, especially archives
$dbw->immediateCommit();
@@ -308,6 +304,6 @@ class ICFile extends LocalFile{
return true;
}
-
+
}
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
index 1e5fc449..9b06fe2d 100644
--- a/includes/filerepo/LocalFile.php
+++ b/includes/filerepo/LocalFile.php
@@ -5,7 +5,7 @@
/**
* Bump this number when serialized cache records may be incompatible.
*/
-define( 'MW_FILE_VERSION', 4 );
+define( 'MW_FILE_VERSION', 7 );
/**
* Class to represent a local file in the wiki's own database
@@ -29,24 +29,26 @@ class LocalFile extends File
/**#@+
* @private
*/
- var $fileExists, # does the file file exist on disk? (loadFromXxx)
- $historyLine, # Number of line to return by nextHistoryLine() (constructor)
- $historyRes, # result of the query for the file's history (nextHistoryLine)
- $width, # \
- $height, # |
- $bits, # --- returned by getimagesize (loadFromXxx)
- $attr, # /
- $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
- $mime, # MIME type, determined by MimeMagic::guessMimeType
- $major_mime, # Major mime type
- $minor_mime, # Minor mime type
- $size, # Size in bytes (loadFromXxx)
- $metadata, # Handler-specific metadata
- $timestamp, # Upload timestamp
- $sha1, # SHA-1 base 36 content hash
- $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
- $upgraded, # Whether the row was upgraded on load
- $locked; # True if the image row is locked
+ var $fileExists, # does the file file exist on disk? (loadFromXxx)
+ $historyLine, # Number of line to return by nextHistoryLine() (constructor)
+ $historyRes, # result of the query for the file's history (nextHistoryLine)
+ $width, # \
+ $height, # |
+ $bits, # --- returned by getimagesize (loadFromXxx)
+ $attr, # /
+ $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
+ $mime, # MIME type, determined by MimeMagic::guessMimeType
+ $major_mime, # Major mime type
+ $minor_mime, # Minor mime type
+ $size, # Size in bytes (loadFromXxx)
+ $metadata, # Handler-specific metadata
+ $timestamp, # Upload timestamp
+ $sha1, # SHA-1 base 36 content hash
+ $user, $user_text, # User, who uploaded the file
+ $description, # Description of current revision of the file
+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+ $upgraded, # Whether the row was upgraded on load
+ $locked; # True if the image row is locked
/**#@-*/
@@ -110,12 +112,9 @@ class LocalFile extends File
wfDebug( "Pulling file metadata from cache key $key\n" );
$this->fileExists = $cachedValues['fileExists'];
if ( $this->fileExists ) {
- unset( $cachedValues['version'] );
- unset( $cachedValues['fileExists'] );
- foreach ( $cachedValues as $name => $value ) {
- $this->$name = $value;
- }
+ $this->setProps( $cachedValues );
}
+ $this->dataLoaded = true;
}
if ( $this->dataLoaded ) {
wfIncrStats( 'image_cache_hit' );
@@ -158,7 +157,7 @@ class LocalFile extends File
function getCacheFields( $prefix = 'img_' ) {
static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
- 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' );
+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
static $results = array();
if ( $prefix == '' ) {
return $fields;
@@ -184,7 +183,7 @@ class LocalFile extends File
# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
$this->dataLoaded = true;
- $dbr = $this->repo->getSlaveDB();
+ $dbr = $this->repo->getMasterDB();
$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
array( 'img_name' => $this->getName() ), $fname );
@@ -294,6 +293,9 @@ class LocalFile extends File
$dbw = $this->repo->getMasterDB();
list( $major, $minor ) = self::splitMime( $this->mime );
+ if ( wfReadOnly() ) {
+ return;
+ }
wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
$dbw->update( 'image',
@@ -313,6 +315,13 @@ class LocalFile extends File
wfProfileOut( __METHOD__ );
}
+ /**
+ * Set properties in this object to be equal to those given in the
+ * associative array $info. Only cacheable fields can be set.
+ *
+ * If 'mime' is given, it will be split into major_mime/minor_mime.
+ * If major_mime/minor_mime are given, $this->mime will also be set.
+ */
function setProps( $info ) {
$this->dataLoaded = true;
$fields = $this->getCacheFields( '' );
@@ -378,6 +387,20 @@ class LocalFile extends File
}
/**
+ * Returns ID or name of user who uploaded the file
+ *
+ * @param $type string 'text' or 'id'
+ */
+ function getUser($type='text') {
+ $this->load();
+ if( $type == 'text' ) {
+ return $this->user_text;
+ } elseif( $type == 'id' ) {
+ return $this->user;
+ }
+ }
+
+ /**
* Get handler-specific metadata
*/
function getMetadata() {
@@ -555,6 +578,28 @@ class LocalFile extends File
/** purgeDescription inherited */
/** purgeEverything inherited */
+ function getHistory($limit = null, $start = null, $end = null) {
+ $dbr = $this->repo->getSlaveDB();
+ $conds = $opts = array();
+ $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() );
+ if( $start !== null ) {
+ $conds[] = "oi_timestamp <= " . $dbr->addQuotes( $dbr->timestamp( $start ) );
+ }
+ if( $end !== null ) {
+ $conds[] = "oi_timestamp >= " . $dbr->addQuotes( $dbr->timestamp( $end ) );
+ }
+ if( $limit ) {
+ $opts['LIMIT'] = $limit;
+ }
+ $opts['ORDER BY'] = 'oi_timestamp DESC';
+ $res = $dbr->select('oldimage', '*', $conds, __METHOD__, $opts);
+ $r = array();
+ while( $row = $dbr->fetchObject($res) ) {
+ $r[] = OldLocalFile::newFromRow($row, $this->repo);
+ }
+ return $r;
+ }
+
/**
* Return the history of this file, line by line.
* starts with current version, then old versions.
@@ -566,6 +611,9 @@ class LocalFile extends File
* @public
*/
function nextHistoryLine() {
+ # Polymorphic function name to distinguish foreign and local fetches
+ $fname = get_class( $this ) . '::' . __FUNCTION__;
+
$dbr = $this->repo->getSlaveDB();
if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
@@ -575,7 +623,7 @@ class LocalFile extends File
"'' AS oi_archive_name"
),
array( 'img_name' => $this->title->getDBkey() ),
- __METHOD__
+ $fname
);
if ( 0 == $dbr->numRows( $this->historyRes ) ) {
$dbr->freeResult($this->historyRes);
@@ -586,7 +634,7 @@ class LocalFile extends File
$dbr->freeResult($this->historyRes);
$this->historyRes = $dbr->select( 'oldimage', '*',
array( 'oi_name' => $this->title->getDBkey() ),
- __METHOD__,
+ $fname,
array( 'ORDER BY' => 'oi_timestamp DESC' )
);
}
@@ -678,6 +726,10 @@ class LocalFile extends File
if ( !$props ) {
$props = $this->repo->getFileProps( $this->getVirtualUrl() );
}
+ $props['description'] = $comment;
+ $props['user'] = $wgUser->getID();
+ $props['user_text'] = $wgUser->getName();
+ $props['timestamp'] = wfTimestamp( TS_MW );
$this->setProps( $props );
// Delete thumbnails and refresh the metadata cache
@@ -964,6 +1016,11 @@ class LocalFile extends File
return $html;
}
+ function getDescription() {
+ $this->load();
+ return $this->description;
+ }
+
function getTimestamp() {
$this->load();
return $this->timestamp;
@@ -1188,12 +1245,12 @@ class LocalFileDeleteBatch {
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
if ( $deleteCurrent ) {
+ $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
$where = array( 'img_name' => $this->file->getName() );
$dbw->insertSelect( 'filearchive', 'image',
array(
'fa_storage_group' => $encGroup,
- 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))",
-
+ 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
@@ -1217,15 +1274,14 @@ class LocalFileDeleteBatch {
}
if ( count( $oldRels ) ) {
+ $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
$where = array(
'oi_name' => $this->file->getName(),
'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
-
$dbw->insertSelect( 'filearchive', 'oldimage',
array(
'fa_storage_group' => $encGroup,
- 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))",
-
+ 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
@@ -1252,9 +1308,6 @@ class LocalFileDeleteBatch {
function doDBDeletes() {
$dbw = $this->file->repo->getMasterDB();
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
- if ( $deleteCurrent ) {
- $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
- }
if ( count( $oldRels ) ) {
$dbw->delete( 'oldimage',
array(
@@ -1262,6 +1315,9 @@ class LocalFileDeleteBatch {
'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
), __METHOD__ );
}
+ if ( $deleteCurrent ) {
+ $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
+ }
}
/**
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index 72f9e9a6..a259bd48 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -62,4 +62,52 @@ class LocalRepo extends FSRepo {
}
return $status;
}
+
+ /**
+ * Function link Title::getArticleID().
+ * We can't say Title object, what database it should use, so we duplicate that function here.
+ */
+ private function getArticleID( $title ) {
+ if( !$title instanceof Title ) {
+ return 0;
+ }
+ $dbr = $this->getSlaveDB();
+ $id = $dbr->selectField(
+ 'page', // Table
+ 'page_id', //Field
+ array( //Conditions
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbKey(),
+ ),
+ __METHOD__ //Function name
+ );
+ return $id;
+ }
+
+ function checkRedirect( $title ) {
+ global $wgFileRedirects;
+ if( !$wgFileRedirects ) {
+ return false;
+ }
+
+ if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) {
+ $title = Title::makeTitle( NS_IMAGE, $title->getText() );
+ }
+
+ $id = $this->getArticleID( $title );
+ if( !$id ) {
+ return false;
+ }
+ $dbr = $this->getSlaveDB();
+ $row = $dbr->selectRow(
+ 'redirect',
+ array( 'rd_title', 'rd_namespace' ),
+ array( 'rd_from' => $id ),
+ __METHOD__
+ );
+ if( !$row ) {
+ return false;
+ }
+ return Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ }
}
diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php
new file mode 100644
index 00000000..87bfd3ab
--- /dev/null
+++ b/includes/filerepo/NullRepo.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * File repository with no files, for performance testing
+ */
+
+class NullRepo extends FileRepo {
+ function __construct( $info ) {}
+
+ function storeBatch( $triplets, $flags = 0 ) {
+ return false;
+ }
+
+ function storeTemp( $originalName, $srcPath ) {
+ return false;
+ }
+ function publishBatch( $triplets, $flags = 0 ) {
+ return false;
+ }
+ function deleteBatch( $sourceDestPairs ) {
+ return false;
+ }
+ function getFileProps( $virtualUrl ) {
+ return false;
+ }
+ function newFile( $title, $time = false ) {
+ return false;
+ }
+ function findFile( $title, $time = false ) {
+ return false;
+ }
+}
+
+?>
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index 23d222af..b0e1d782 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -32,6 +32,13 @@ class RepoGroup {
}
/**
+ * Set the singleton instance to a given object
+ */
+ static function setSingleton( $instance ) {
+ self::$instance = $instance;
+ }
+
+ /**
* Construct a group of file repositories.
* @param array $data Array of repository info arrays.
* Each info array is an associative array with the 'class' member
@@ -47,8 +54,8 @@ class RepoGroup {
* Search repositories for an image.
* You can also use wfGetFile() to do this.
* @param mixed $title Title object or string
- * @param mixed $time The 14-char timestamp before which the file should
- * have been uploaded, or false for the current version
+ * @param mixed $time The 14-char timestamp the file should have
+ * been uploaded, or false for the current version
* @return File object or false if it is not found
*/
function findFile( $title, $time = false ) {
@@ -70,13 +77,34 @@ class RepoGroup {
}
/**
+ * Interface for FileRepo::checkRedirect()
+ */
+ function checkRedirect( $title ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+
+ $redir = $this->localRepo->checkRedirect( $title );
+ if( $redir ) {
+ return $redir;
+ }
+ foreach ( $this->foreignRepos as $repo ) {
+ $redir = $repo->checkRedirect( $title );
+ if ( $redir ) {
+ return $redir;
+ }
+ }
+ return false;
+ }
+
+ /**
* Get the repo instance with a given key.
*/
function getRepo( $index ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
- if ( $index == 'local' ) {
+ if ( $index === 'local' ) {
return $this->localRepo;
} elseif ( isset( $this->foreignRepos[$index] ) ) {
return $this->foreignRepos[$index];
@@ -84,6 +112,19 @@ class RepoGroup {
return false;
}
}
+ /**
+ * Get the repo instance by its name
+ */
+ function getRepoByName( $name ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+ foreach ( $this->foreignRepos as $key => $repo ) {
+ if ( $repo->name == $name)
+ return $repo;
+ }
+ return false;
+ }
/**
* Get the local repository, i.e. the one corresponding to the local image
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index c7ab7d81..19914929 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -96,6 +96,19 @@ abstract class MediaHandler {
*/
function isMetadataValid( $image, $metadata ) { return true; }
+
+ /**
+ * Get a MediaTransformOutput object representing an alternate of the transformed
+ * output which will call an intermediary thumbnail assist script.
+ *
+ * Used when the repository has a thumbnailScriptUrl option configured.
+ *
+ * Return false to fall back to the regular getTransform().
+ */
+ function getScriptedTransform( $image, $script, $params ) {
+ return false;
+ }
+
/**
* Get a MediaTransformOutput object representing the transformed output. Does not
* actually do the transform.
@@ -191,7 +204,7 @@ abstract class MediaHandler {
* to do things like visual indication of grouped and chained streams
* in ogg container files.
*/
- function formatMetadata( $image, $metadata ) {
+ function formatMetadata( $image ) {
return false;
}
@@ -224,7 +237,7 @@ abstract class MediaHandler {
return wfMsg( 'file-info', $sk->formatSize( $file->getSize() ), $file->getMimeType() );
}
- function getDimensionsString() {
+ function getDimensionsString( $file ) {
return '';
}
@@ -372,7 +385,10 @@ abstract class ImageHandler extends MediaHandler {
}
$url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
$page = isset( $params['page'] ) ? $params['page'] : false;
- return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
+
+ if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
+ return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
+ }
}
function getImageSize( $image, $path ) {
@@ -386,26 +402,24 @@ abstract class ImageHandler extends MediaHandler {
global $wgLang;
$nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
$wgLang->formatNum( $file->getSize() ) ) . ')';
- $widthheight = wfMsgHtml( 'widthheight', $file->getWidth(), $file->getHeight() );
-
+ $widthheight = wfMsgHtml( 'widthheight', $wgLang->formatNum( $file->getWidth() ) ,$wgLang->formatNum( $file->getHeight() ) );
+
return "$widthheight ($nbytes)";
}
function getLongDesc( $file ) {
global $wgLang;
- return wfMsgHtml('file-info-size', $file->getWidth(), $file->getHeight(),
+ return wfMsgHtml('file-info-size', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ),
$wgLang->formatSize( $file->getSize() ), $file->getMimeType() );
}
function getDimensionsString( $file ) {
+ global $wgLang;
$pages = $file->pageCount();
if ( $pages > 1 ) {
- return wfMsg( 'widthheightpage', $file->getWidth(), $file->getHeight(), $pages );
+ return wfMsg( 'widthheightpage', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ), $wgLang->formatNum( $pages ) );
} else {
- return wfMsg( 'widthheight', $file->getWidth(), $file->getHeight() );
+ return wfMsg( 'widthheight', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ) );
}
}
}
-
-
-
diff --git a/includes/mime.info b/includes/mime.info
index a960f023..dd3af7d0 100644
--- a/includes/mime.info
+++ b/includes/mime.info
@@ -18,6 +18,7 @@ image/x-portable-pixmap [BITMAP]
image/x-portable-graymap image/x-portable-greymap [BITMAP]
image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP]
image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP]
+image/vnd.djvu image/x.djvu image/x-djvu [BITMAP]
image/svg+xml application/svg+xml application/svg image/svg [DRAWING]
application/postscript [DRAWING]
diff --git a/includes/mime.types b/includes/mime.types
index 19a61517..64f77c12 100644
--- a/includes/mime.types
+++ b/includes/mime.types
@@ -55,7 +55,7 @@ application/x-wais-source src
application/x-xpinstall xpi
application/xhtml+xml xhtml xht
application/xslt+xml xslt
-application/xml xml xsl
+application/xml xml xsl xsd
application/xml-dtd dtd
application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw
audio/basic au snd
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index 127c30a0..ac24800a 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -24,6 +24,7 @@ class UserloginTemplate extends QuickTemplate {
<div class="visualClear"></div>
<?php } ?>
+<div id="loginstart"><?php $this->msgWiki( 'loginstart' ); ?></div>
<div id="userloginForm">
<form name="userlogin" method="post" action="<?php $this->text('action') ?>">
<h2><?php $this->msg('login') ?></h2>
@@ -33,16 +34,16 @@ class UserloginTemplate extends QuickTemplate {
<?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
<table>
<tr>
- <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpName1'><?php $this->msg('yourname') ?></label></td>
+ <td class="mw-input">
<input type='text' class='loginText' name="wpName" id="wpName1"
tabindex="1"
value="<?php $this->text('name') ?>" size='20' />
</td>
</tr>
<tr>
- <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td>
+ <td class="mw-input">
<input type='password' class='loginPassword' name="wpPassword" id="wpPassword1"
tabindex="2"
value="" size='20' />
@@ -55,8 +56,8 @@ class UserloginTemplate extends QuickTemplate {
}
?>
<tr>
- <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td>
- <td align='left'>
+ <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td>
+ <td class="mw-input">
<select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
tabindex="3">
<?php echo $doms ?>
@@ -66,7 +67,7 @@ class UserloginTemplate extends QuickTemplate {
<?php } ?>
<tr>
<td></td>
- <td align='left'>
+ <td class="mw-input">
<input type='checkbox' name="wpRemember"
tabindex="4"
value="1" id="wpRemember"
@@ -76,7 +77,7 @@ class UserloginTemplate extends QuickTemplate {
</tr>
<tr>
<td></td>
- <td align='left' style="white-space:nowrap">
+ <td class="mw-submit">
<input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" />&nbsp;<?php if( $this->data['useemail'] && $this->data['canreset']) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword"
tabindex="6"
value="<?php $this->msg('mailmypassword') ?>" />
@@ -117,16 +118,16 @@ class UsercreateTemplate extends QuickTemplate {
<?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
<table>
<tr>
- <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpName2'><?php $this->msg('yourname') ?></label></td>
+ <td class="mw-input">
<input type='text' class='loginText' name="wpName" id="wpName2"
tabindex="1"
value="<?php $this->text('name') ?>" size='20' />
</td>
</tr>
<tr>
- <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td>
+ <td class="mw-input">
<input type='password' class='loginPassword' name="wpPassword" id="wpPassword2"
tabindex="2"
value="" size='20' />
@@ -139,8 +140,8 @@ class UsercreateTemplate extends QuickTemplate {
}
?>
<tr>
- <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td>
- <td align='left'>
+ <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td>
+ <td class="mw-input">
<select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
tabindex="3">
<?php echo $doms ?>
@@ -149,8 +150,8 @@ class UsercreateTemplate extends QuickTemplate {
</tr>
<?php } ?>
<tr>
- <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td>
+ <td class="mw-input">
<input type='password' class='loginPassword' name="wpRetype" id="wpRetype"
tabindex="4"
value=""
@@ -159,21 +160,25 @@ class UsercreateTemplate extends QuickTemplate {
</tr>
<tr>
<?php if( $this->data['useemail'] ) { ?>
- <td align='right' style='vertical-align: top'><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
+ <td class="mw-input">
<input type='text' class='loginText' name="wpEmail" id="wpEmail"
tabindex="5"
value="<?php $this->text('email') ?>" size='20' />
<div class="prefsectiontip">
- <?php $this->msgWiki('prefs-help-email'); ?>
+ <?php if( $this->data['emailrequired'] ) {
+ $this->msgWiki('prefs-help-email-required');
+ } else {
+ $this->msgWiki('prefs-help-email');
+ } ?>
</div>
</td>
<?php } ?>
<?php if( $this->data['userealname'] ) { ?>
</tr>
<tr>
- <td align='right' style='vertical-align: top'><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td>
- <td align='left'>
+ <td class="mw-label"><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td>
+ <td class="mw-input">
<input type='text' class='loginText' name="wpRealName" id="wpRealName"
tabindex="6"
value="<?php $this->text('realname') ?>" size='20' />
@@ -185,7 +190,7 @@ class UsercreateTemplate extends QuickTemplate {
</tr>
<tr>
<td></td>
- <td align='left'>
+ <td class="mw-input">
<input type='checkbox' name="wpRemember"
tabindex="7"
value="1" id="wpRemember"
@@ -195,7 +200,7 @@ class UsercreateTemplate extends QuickTemplate {
</tr>
<tr>
<td></td>
- <td align='left'>
+ <td class="mw-submit">
<input type='submit' name="wpCreateaccount" id="wpCreateaccount"
tabindex="8"
value="<?php $this->msg('createaccount') ?>" />
diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile
index 30679fbb..c63e4db7 100644
--- a/includes/zhtable/Makefile
+++ b/includes/zhtable/Makefile
@@ -11,30 +11,35 @@ SED = LANG=zh_CN.UTF8 sed
DIFF = LANG=zh_CN.UTF8 diff
CC ?= gcc
-#installation directory
+SF_MIRROR = easynews
+SCIM_TABLES_VER = 0.5.7
+SCIM_PINYIN_VER = 0.5.91
+LIBTABE_VER = 0.2.3
+
+# Installation directory
INSTDIR = /usr/local/share/zhdaemons/
-all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toCN.dict toTW.dict toHK.dict toSG.dict
+all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toHans.dict toHant.dict toCN.dict toTW.dict toHK.dict toSG.dict
Unihan.txt:
wget -nc ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip
unzip -q Unihan.zip
EZ.txt.in:
- wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-tables-0.5.1.tar.gz
- tar -xzf scim-tables-0.5.1.tar.gz -O scim-tables-0.5.1/zh/EZ.txt.in > EZ.txt.in
+ wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-tables-$(SCIM_TABLES_VER).tar.gz
+ tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/EZ-Big.txt.in > EZ.txt.in
phrase_lib.txt:
- wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-pinyin-0.5.0.tar.gz
- tar -xzf scim-pinyin-0.5.0.tar.gz -O scim-pinyin-0.5.0/data/phrase_lib.txt > phrase_lib.txt
+ wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-pinyin-$(SCIM_PINYIN_VER).tar.gz
+ tar -xzf scim-pinyin-$(SCIM_PINYIN_VER).tar.gz -O scim-pinyin-$(SCIM_PINYIN_VER)/data/phrase_lib.txt > phrase_lib.txt
tsi.src:
- wget -nc http://unc.dl.sourceforge.net/sourceforge/libtabe/libtabe-0.2.3.tgz
- tar -xzf libtabe-0.2.3.tgz -O libtabe/tsi-src/tsi.src > tsi.src
+ wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/libtabe/libtabe-$(LIBTABE_VER).tgz
+ tar -xzf libtabe-$(LIBTABE_VER).tgz -O libtabe/tsi-src/tsi.src > tsi.src
wordlist: phrase_lib.txt EZ.txt.in tsi.src
iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/# //g' | $(SED) 's/[ ][0-9].*//' > wordlist
- $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >>wordlist
+ $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >> wordlist
$(SED) '1,/BEGIN_TABLE/d' EZ.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" >> wordlist
sort wordlist | uniq | $(SED) 's/ //g' > t
mv t wordlist
@@ -184,67 +189,68 @@ simp2trad.php: simp2trad1to1.t simpphrases.t
cat simpphrases.t >> simp2trad.php
printf '";\n$$t=strtr($$str, $$simp2trad);\necho $$t;\n?>' >> simp2trad.php
-simp2trad.phrases.t: trad2simp.php tradphrases.t toTW.manual
+simp2trad.phrases.t: trad2simp.php tradphrases.t
php -f trad2simp.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
cat tradphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
paste tmp1 tmp2 > simp2trad.phrases.t
- $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toTW.manual >> simp2trad.phrases.t
-trad2simp.phrases.t: simp2trad.php simpphrases.t toCN.manual
+trad2simp.phrases.t: simp2trad.php simpphrases.t
php -f simp2trad.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
cat simpphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
paste tmp1 tmp2 > trad2simp.phrases.t
- $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toCN.manual >> trad2simp.phrases.t
-toCN.dict: trad2simp1to1.t trad2simp.phrases.t
- cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toCN.dict
- cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toCN.dict
+toHans.dict: trad2simp1to1.t trad2simp.phrases.t
+ cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHans.dict
+ cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHans.dict
+
+toHant.dict: simp2trad1to1.t simp2trad.phrases.t
+ cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHant.dict
+ cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHant.dict
-toTW.dict: simp2trad1to1.t simp2trad.phrases.t
- cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toTW.dict
- cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toTW.dict
+toTW.dict: toTW.manual
+ cat toTW.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toTW.dict
toHK.dict: toHK.manual
cat toHK.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toHK.dict
+toCN.dict: toCN.manual
+ cat toCN.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toCN.dict
+
toSG.dict: toSG.manual
cat toSG.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toSG.dict
-
-
-ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toHK.manual toSG.manual
- printf '<?php\n/**\n * Simplified/Traditional Chinese conversion tables\n' > ZhConversion.php
+ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toCN.manual toHK.manual toSG.manual toTW.manual
+ printf '<?php\n/**\n * Simplified / Traditional Chinese conversion tables\n' > ZhConversion.php
printf ' *\n * Automatically generated using code and data in includes/zhtable/\n' >> ZhConversion.php
- printf ' * Do not modify directly! \n *\n * @package MediaWiki\n*/\n\n' >> ZhConversion.php
- printf '$$zh2TW=array(\n' >> ZhConversion.php
+ printf ' * Do not modify directly!\n */\n\n' >> ZhConversion.php
+ printf '$$zh2Hant = array(\n' >> ZhConversion.php
cat simp2trad1to1.t >> ZhConversion.php
echo >> ZhConversion.php
cat simp2trad.phrases.t >> ZhConversion.php
- echo >> ZhConversion.php
echo ');' >> ZhConversion.php
echo >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2CN=array(\n' >> ZhConversion.php
+ printf '$$zh2Hans = array(\n' >> ZhConversion.php
cat trad2simp1to1.t >> ZhConversion.php
echo >> ZhConversion.php
cat trad2simp.phrases.t >> ZhConversion.php
+ echo ');' >> ZhConversion.php
echo >> ZhConversion.php
- printf ');' >> ZhConversion.php
- echo >> ZhConversion.php
+ printf '$$zh2TW = array(\n' >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toTW.manual >> ZhConversion.php
+ echo ');' >> ZhConversion.php
echo >> ZhConversion.php
- printf '$$zh2HK=array(\n' >> ZhConversion.php
+ printf '$$zh2HK = array(\n' >> ZhConversion.php
$(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toHK.manual >> ZhConversion.php
+ echo ');' >> ZhConversion.php
echo >> ZhConversion.php
- printf ');' >> ZhConversion.php
- echo >> ZhConversion.php
+ printf '$$zh2CN = array(\n' >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toCN.manual >> ZhConversion.php
+ echo ');' >> ZhConversion.php
echo >> ZhConversion.php
- printf '$$zh2SG=array(\n' >> ZhConversion.php
+ printf '$$zh2SG = array(\n' >> ZhConversion.php
$(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSG.manual >> ZhConversion.php
echo >> ZhConversion.php
printf ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '?>' >> ZhConversion.php
-
clean: cleantmp cleandl
@@ -262,7 +268,7 @@ cleantmp:
cleandl:
rm -f \
Unihan.zip \
- scim-tables-0.5.1.tar.gz \
- scim-pinyin-0.5.0.tar.gz \
- libtabe-0.2.3.tgz
+ scim-tables-$(SCIM_TABLES_VER).tar.gz \
+ scim-pinyin-$(SCIM_PINYIN_VER).tar.gz \
+ libtabe-$(LIBTABE_VER).tgz
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
index caff9c14..427afad2 100644
--- a/includes/zhtable/toCN.manual
+++ b/includes/zhtable/toCN.manual
@@ -1,26 +1,19 @@
記憶體 内存
預設 默认
-預設 缺省
串列 串行
乙太網 以太网
點陣圖 位图
常式 例程
-通道 信道
游標 光标
光碟 光盘
光碟機 光驱
全形 全角
共用 共享
-相容 兼容
-首碼 前缀
-尾碼 后缀
載入 加载
半形 半角
變數 变量
雜訊 噪声
因數 因子
-線上 在线
-離線 脱机
功能變數名稱 域名
音效卡 声卡
字型大小 字号
@@ -43,19 +36,16 @@
磁碟 磁盘
磁軌 磁道
程式控制 程控
-埠 端口
運算元 算子
演算法 算法
晶片 芯片
晶元 芯片
片語 词组
-解碼 译码
軟碟機 软驱
-快閃記憶體 闪存
+快閃記憶體 快闪存储器
滑鼠 鼠标
進位 进制
互動式 交互式
-模擬 仿真
優先順序 优先级
感測 传感
攜帶型 便携式
@@ -64,26 +54,19 @@
防寫 写保护
分散式 分布式
解析度 分辨率
-程式 程序
伺服器 服务器
等於 等于
區域網 局域网
-上傳 上载
-電腦 计算机
巨集 宏
掃瞄器 扫瞄仪
寬頻 宽带
-視窗 窗口
資料庫 数据库
-西曆 公历
乳酪 奶酪
鉅賈 巨商
手電筒 手电
萬曆 万历
永曆 永历
辭彙 词汇
-保全 保安
-慣用 习用
母音 元音
自由球 任意球
頭槌 头球
@@ -177,11 +160,11 @@
坦桑尼亞 坦桑尼亚
坦尚尼亞 坦桑尼亚
埃塞俄比亞 埃塞俄比亚
+衣索匹亞 埃塞俄比亚
衣索比亞 埃塞俄比亚
吉里巴斯 基里巴斯
基里巴斯 基里巴斯
塔吉克 塔吉克斯坦
-獅子山 塞拉利昂
塞拉利昂 塞拉利昂
塞普勒斯 塞浦路斯
塞浦路斯 塞浦路斯
@@ -218,13 +201,11 @@
斯洛維尼亞 斯洛文尼亚
新西蘭 新西兰
紐西蘭 新西兰
-北韓 朝鲜
格林納達 格林纳达
格瑞那達 格林纳达
-格魯吉亞 格鲁吉亚
-喬治亞 格鲁吉亚
+格魯吉亞 乔治亚
+喬治亞 乔治亚
梵蒂岡 梵蒂冈
-教廷 梵蒂冈
毛里塔尼亞 毛里塔尼亚
茅利塔尼亞 毛里塔尼亚
毛里裘斯 毛里求斯
@@ -271,8 +252,7 @@
馬爾代夫 马尔代夫
馬爾地夫 马尔代夫
馬爾他 马耳他
-馬里 马里
-馬利 马里
+馬利共和國 马里共和国
即食麵 方便面
快速面 方便面
速食麵 方便面
@@ -286,11 +266,9 @@
夜学 夜校
华乐 民乐
中樂 民乐
-住屋 住房
屋价 房价
的士 出租车
計程車 出租车
-巴士 公共汽车
公車 公共汽车
單車 自行车
節慶 节日
@@ -305,14 +283,14 @@
衛生 卫生
賓士 奔驰
平治 奔驰
-捷豹 美洲虎
-積架 美洲虎
+積架 捷豹
福斯 大众
福士 大众
雪鐵龍 雪铁龙
萬事得 马自达
馬自達 马自达
寶獅 标志
+拿破崙 拿破仑
布殊 布什
布希 布什
柯林頓 克林顿
@@ -326,6 +304,5 @@
沙芬 马拉特·萨芬
舒麥加 迈克尔·舒马赫
希特拉 希特勒
-戴安娜 狄安娜
-黛安娜 狄安娜
-希拉 赫拉 \ No newline at end of file
+黛安娜 戴安娜
+希拉 赫拉
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
index ab623455..6a872fa6 100644
--- a/includes/zhtable/toHK.manual
+++ b/includes/zhtable/toHK.manual
@@ -1,7 +1,7 @@
打印机 打印機
印表機 打印機
-字节 字節
-位元組 字節
+字节 位元組
+字節 位元組
打印 打印
列印 打印
硬件 硬件
@@ -83,10 +83,11 @@
坦桑尼亚 坦桑尼亞
坦尚尼亞 坦桑尼亞
埃塞俄比亚 埃塞俄比亞
+衣索匹亞 埃塞俄比亞
衣索比亞 埃塞俄比亞
基里巴斯 基里巴斯
吉里巴斯 基里巴斯
-獅子山 塞拉利昂
+狮子山 獅子山
塞普勒斯 塞浦路斯
塞舌尔 塞舌爾
塞席爾 塞舌爾
@@ -119,16 +120,14 @@
紐西蘭 新西蘭
格林纳达 格林納達
格瑞那達 格林納達
-格鲁吉亚 格魯吉亞
-喬治亞 格魯吉亞
+格鲁吉亚 喬治亞
+格魯吉亞 喬治亞
梵蒂冈 梵蒂岡
-教廷 梵蒂岡
毛里塔尼亚 毛里塔尼亞
茅利塔尼亞 毛里塔尼亞
毛里求斯 毛里裘斯
模里西斯 毛里裘斯
-沙特阿拉伯 沙地阿拉伯
-沙烏地阿拉伯 沙地阿拉伯
+沙烏地阿拉伯 沙特阿拉伯
波斯尼亚和黑塞哥维那 波斯尼亞黑塞哥維那
波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那
津巴布韦 津巴布韋
@@ -163,29 +162,22 @@
阿拉伯聯合大公國 阿拉伯聯合酋長國
马尔代夫 馬爾代夫
馬爾地夫 馬爾代夫
-马里 馬里
-馬利 馬里
+馬利共和國 馬里共和國
方便面 即食麵
快速面 即食麵
速食麵 即食麵
泡麵 即食麵
-土豆 薯仔
+土豆 馬鈴薯
华乐 中樂
民乐 中樂
-計程車 的士
+計程車 的士
出租车 的士
公車 巴士
-公共汽车 巴士
自行车 單車
-节日 節慶
犬只 狗隻
台球 桌球
撞球 桌球
冰淇淋 雪糕
-冰淇淋 雪糕
-卫生 衞生
-衛生 衞生
-老人 長者
賓士 平治
捷豹 積架
福斯 福士
@@ -195,12 +187,14 @@
马自达 萬事得
馬自達 萬事得
寶獅 標致
+拿破崙 拿破侖
布什 布殊
布希 布殊
克林顿 克林頓
柯林頓 克林頓
萨达姆 薩達姆
-海珊 薩達姆
+海珊 侯賽因
+侯赛因 侯賽因
大卫·贝克汉姆 大衛碧咸
迈克尔·欧文 米高奧雲
珍妮弗·卡普里亚蒂 卡佩雅蒂
@@ -208,4 +202,4 @@
迈克尔·舒马赫 舒麥加
希特勒 希特拉
狄安娜 戴安娜
-黛安娜 戴安娜 \ No newline at end of file
+黛安娜 戴安娜
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
index 5c90dbe3..a1639f7f 100644
--- a/includes/zhtable/toTW.manual
+++ b/includes/zhtable/toTW.manual
@@ -1,5 +1,3 @@
-内存 記憶體
-默认 預設
缺省 預設
串行 串列
以太网 乙太網
@@ -10,19 +8,13 @@
光盘 光碟
光驱 光碟機
全角 全形
-共享 共用
-兼容 相容
-前缀 首碼
-后缀 尾碼
加载 載入
半角 半形
变量 變數
噪声 雜訊
-因子 因數
-在线 線上
脱机 離線
-域名 功能變數名稱
声卡 音效卡
+老字号 老字號
字号 字型大小
字库 字型檔
字段 欄位
@@ -51,6 +43,7 @@
词组 片語
译码 解碼
软驱 軟碟機
+快闪存储器 快閃記憶體
闪存 快閃記憶體
鼠标 滑鼠
进制 進位
@@ -60,29 +53,22 @@
传感 感測
便携式 攜帶型
信息论 資訊理論
-循环 迴圈
写保护 防寫
分布式 分散式
分辨率 解析度
-程序 程式
服务器 伺服器
等于 等於
局域网 區域網
-上载 上傳
计算机 電腦
-宏 巨集
扫瞄仪 掃瞄器
宽带 寬頻
-窗口 視窗
数据库 資料庫
-公历 西曆
奶酪 乳酪
巨商 鉅賈
手电 手電筒
万历 萬曆
永历 永曆
词汇 辭彙
-保安 保全
习用 慣用
元音 母音
任意球 自由球
@@ -103,8 +89,6 @@
二極管 二極體
三极管 三極體
三極管 三極體
-数码 數位
-數碼 數位
软件 軟體
軟件 軟體
网络 網路
@@ -219,13 +203,12 @@
斯洛文尼亞 斯洛維尼亞
新西兰 紐西蘭
新西蘭 紐西蘭
-朝鲜 北韓
格林纳达 格瑞那達
格林納達 格瑞那達
格鲁吉亚 喬治亞
格魯吉亞 喬治亞
-梵蒂冈 教廷
-梵蒂岡 教廷
+佐治亚 喬治亞
+佐治亞 喬治亞
毛里塔尼亚 茅利塔尼亞
毛里塔尼亞 茅利塔尼亞
毛里求斯 模里西斯
@@ -267,12 +250,11 @@
阿塞拜疆 亞塞拜然
阿拉伯联合酋长国 阿拉伯聯合大公國
阿拉伯聯合酋長國 阿拉伯聯合大公國
-韩国 南韓
马尔代夫 馬爾地夫
馬爾代夫 馬爾地夫
马耳他 馬爾他
-马里 馬利
-馬里 馬利
+马里共和国 馬利共和國
+馬里共和國 馬利共和國
方便面 速食麵
快速面 速食麵
即食麵 速食麵
@@ -281,10 +263,7 @@
绑紧跳 笨豬跳
冷菜 冷盤
凉菜 冷盤
-的士 計程車
出租车 計程車
-巴士 公車
-公共汽车 公車
台球 撞球
桌球 撞球
雪糕 冰淇淋
@@ -297,13 +276,15 @@
雪铁龙 雪鐵龍
马自达 馬自達
萬事得 馬自達
+拿破仑 拿破崙
+拿破侖 拿破崙
布什 布希
布殊 布希
克林顿 柯林頓
克林頓 柯林頓
-萨达姆 海珊
-薩達姆 海珊
+侯赛因 海珊
+侯賽因 海珊
凡高 梵谷
狄安娜 黛安娜
戴安娜 黛安娜
-赫拉 希拉 \ No newline at end of file
+赫拉 希拉