From 183851b06bd6c52f3cae5375f433da720d410447 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 11 Oct 2006 18:12:39 +0000 Subject: MediaWiki 1.7.1 wiederhergestellt --- includes/.htaccess | 1 + includes/AjaxDispatcher.php | 83 + includes/AjaxFunctions.php | 157 + includes/Article.php | 2575 ++++++++ includes/AuthPlugin.php | 232 + includes/AutoLoader.php | 272 + includes/BagOStuff.php | 538 ++ includes/Block.php | 440 ++ includes/CacheManager.php | 159 + includes/CategoryPage.php | 315 + includes/Categoryfinder.php | 191 + includes/ChangesList.php | 653 +++ includes/CoreParserFunctions.php | 150 + includes/Credits.php | 187 + includes/Database.php | 2020 +++++++ includes/DatabaseFunctions.php | 414 ++ includes/DatabaseMysql.php | 6 + includes/DatabaseOracle.php | 692 +++ includes/DatabasePostgres.php | 609 ++ includes/DateFormatter.php | 288 + includes/DefaultSettings.php | 2189 +++++++ includes/Defines.php | 183 + includes/DifferenceEngine.php | 1751 ++++++ includes/DjVuImage.php | 214 + includes/EditPage.php | 1864 ++++++ includes/Exception.php | 193 + includes/Exif.php | 1124 ++++ includes/Export.php | 736 +++ includes/ExternalEdit.php | 77 + includes/ExternalStore.php | 70 + includes/ExternalStoreDB.php | 150 + includes/ExternalStoreHttp.php | 23 + includes/FakeTitle.php | 88 + includes/Feed.php | 310 + includes/FileStore.php | 377 ++ includes/GlobalFunctions.php | 2005 +++++++ includes/HTMLCacheUpdate.php | 230 + includes/HTMLForm.php | 177 + includes/HistoryBlob.php | 308 + includes/Hooks.php | 131 + includes/HttpFunctions.php | 91 + includes/Image.php | 2265 +++++++ includes/ImageFunctions.php | 223 + includes/ImageGallery.php | 211 + includes/ImagePage.php | 726 +++ includes/JobQueue.php | 267 + includes/Licenses.php | 171 + includes/LinkBatch.php | 184 + includes/LinkCache.php | 178 + includes/LinkFilter.php | 92 + includes/Linker.php | 1101 ++++ includes/LinksUpdate.php | 601 ++ includes/LoadBalancer.php | 666 +++ includes/LogPage.php | 246 + includes/MacBinary.php | 272 + includes/MagicWord.php | 448 ++ includes/Math.php | 269 + includes/MemcachedSessions.php | 74 + includes/MessageCache.php | 581 ++ includes/Metadata.php | 362 ++ includes/MimeMagic.php | 695 +++ includes/Namespace.php | 129 + includes/ObjectCache.php | 125 + includes/OutputPage.php | 1078 ++++ includes/PageHistory.php | 685 +++ includes/Parser.php | 4727 +++++++++++++++ includes/ParserCache.php | 127 + includes/ParserXML.php | 643 ++ includes/ProfilerSimple.php | 108 + includes/ProfilerSimpleUDP.php | 34 + includes/ProfilerStub.php | 26 + includes/Profiling.php | 353 ++ includes/ProtectionForm.php | 244 + includes/ProxyTools.php | 233 + includes/QueryPage.php | 483 ++ includes/RawPage.php | 203 + includes/RecentChange.php | 509 ++ includes/Revision.php | 799 +++ includes/Sanitizer.php | 1184 ++++ includes/SearchEngine.php | 345 ++ includes/SearchMySQL.php | 206 + includes/SearchMySQL4.php | 73 + includes/SearchPostgres.php | 156 + includes/SearchTsearch2.php | 123 + includes/SearchUpdate.php | 115 + includes/Setup.php | 330 ++ includes/SiteConfiguration.php | 121 + includes/SiteStatsUpdate.php | 82 + includes/Skin.php | 1499 +++++ includes/SkinTemplate.php | 1109 ++++ includes/SpecialAllmessages.php | 212 + includes/SpecialAllpages.php | 322 + includes/SpecialAncientpages.php | 65 + includes/SpecialBlockip.php | 239 + includes/SpecialBlockme.php | 40 + includes/SpecialBooksources.php | 109 + includes/SpecialBrokenRedirects.php | 88 + includes/SpecialCategories.php | 68 + includes/SpecialConfirmemail.php | 97 + includes/SpecialContributions.php | 444 ++ includes/SpecialDeadendpages.php | 63 + includes/SpecialDisambiguations.php | 81 + includes/SpecialDoubleRedirects.php | 107 + includes/SpecialEmailuser.php | 160 + includes/SpecialExport.php | 106 + includes/SpecialImagelist.php | 121 + includes/SpecialImport.php | 848 +++ includes/SpecialIpblocklist.php | 255 + includes/SpecialListredirects.php | 69 + includes/SpecialListusers.php | 235 + includes/SpecialLockdb.php | 118 + includes/SpecialLog.php | 427 ++ includes/SpecialLonelypages.php | 58 + includes/SpecialLongpages.php | 41 + includes/SpecialMIMEsearch.php | 155 + includes/SpecialMostcategories.php | 68 + includes/SpecialMostimages.php | 64 + includes/SpecialMostlinked.php | 98 + includes/SpecialMostlinkedcategories.php | 81 + includes/SpecialMostrevisions.php | 68 + includes/SpecialMovepage.php | 283 + includes/SpecialNewimages.php | 204 + includes/SpecialNewpages.php | 198 + includes/SpecialPage.php | 575 ++ includes/SpecialPopularpages.php | 59 + includes/SpecialPreferences.php | 937 +++ includes/SpecialPrefixindex.php | 149 + includes/SpecialRandompage.php | 58 + includes/SpecialRandomredirect.php | 54 + includes/SpecialRecentchanges.php | 709 +++ includes/SpecialRecentchangeslinked.php | 173 + includes/SpecialRevisiondelete.php | 258 + includes/SpecialSearch.php | 413 ++ includes/SpecialShortpages.php | 91 + includes/SpecialSpecialpages.php | 73 + includes/SpecialStatistics.php | 86 + includes/SpecialUncategorizedcategories.php | 39 + includes/SpecialUncategorizedimages.php | 55 + includes/SpecialUncategorizedpages.php | 59 + includes/SpecialUndelete.php | 737 +++ includes/SpecialUnlockdb.php | 105 + includes/SpecialUnusedcategories.php | 48 + includes/SpecialUnusedimages.php | 86 + includes/SpecialUnusedtemplates.php | 59 + includes/SpecialUnwatchedpages.php | 71 + includes/SpecialUpload.php | 1109 ++++ includes/SpecialUploadMogile.php | 135 + includes/SpecialUserlogin.php | 671 +++ includes/SpecialUserlogout.php | 27 + includes/SpecialUserrights.php | 183 + includes/SpecialVersion.php | 270 + includes/SpecialWantedcategories.php | 85 + includes/SpecialWantedpages.php | 133 + includes/SpecialWatchlist.php | 513 ++ includes/SpecialWhatlinkshere.php | 277 + includes/SquidUpdate.php | 279 + includes/StreamFile.php | 72 + includes/Title.php | 2307 ++++++++ includes/User.php | 1986 +++++++ includes/UserMailer.php | 414 ++ includes/Utf8Case.php | 1506 +++++ includes/WatchedItem.php | 190 + includes/WebRequest.php | 491 ++ includes/Wiki.php | 410 ++ includes/WikiError.php | 125 + includes/Xml.php | 279 + includes/XmlFunctions.php | 65 + includes/ZhClient.php | 149 + includes/ZhConversion.php | 8457 +++++++++++++++++++++++++++ includes/cbt/CBTCompiler.php | 369 ++ includes/cbt/CBTProcessor.php | 540 ++ includes/cbt/README | 108 + includes/memcached-client.php | 1060 ++++ includes/mime.info | 76 + includes/mime.types | 117 + includes/normal/CleanUpTest.php | 423 ++ includes/normal/Makefile | 72 + includes/normal/README | 55 + includes/normal/RandomTest.php | 107 + includes/normal/Utf8Test.php | 151 + includes/normal/UtfNormal.php | 792 +++ includes/normal/UtfNormalBench.php | 107 + includes/normal/UtfNormalData.inc | 13 + includes/normal/UtfNormalDataK.inc | 10 + includes/normal/UtfNormalGenerate.php | 235 + includes/normal/UtfNormalTest.php | 249 + includes/normal/UtfNormalUtil.php | 142 + includes/proxy_check.php | 55 + includes/templates/Userlogin.php | 215 + includes/zhtable/Makefile | 268 + includes/zhtable/README | 16 + includes/zhtable/printutf8.c | 99 + includes/zhtable/simp2trad.manual | 178 + includes/zhtable/toCN.manual | 331 ++ includes/zhtable/toHK.manual | 211 + includes/zhtable/toSG.manual | 15 + includes/zhtable/toTW.manual | 309 + includes/zhtable/trad2simp.manual | 15 + includes/zhtable/tradphrases.manual | 149 + 199 files changed, 84860 insertions(+) create mode 100644 includes/.htaccess create mode 100644 includes/AjaxDispatcher.php create mode 100644 includes/AjaxFunctions.php create mode 100644 includes/Article.php create mode 100644 includes/AuthPlugin.php create mode 100644 includes/AutoLoader.php create mode 100644 includes/BagOStuff.php create mode 100644 includes/Block.php create mode 100644 includes/CacheManager.php create mode 100644 includes/CategoryPage.php create mode 100644 includes/Categoryfinder.php create mode 100644 includes/ChangesList.php create mode 100644 includes/CoreParserFunctions.php create mode 100644 includes/Credits.php create mode 100644 includes/Database.php create mode 100644 includes/DatabaseFunctions.php create mode 100644 includes/DatabaseMysql.php create mode 100644 includes/DatabaseOracle.php create mode 100644 includes/DatabasePostgres.php create mode 100644 includes/DateFormatter.php create mode 100644 includes/DefaultSettings.php create mode 100644 includes/Defines.php create mode 100644 includes/DifferenceEngine.php create mode 100644 includes/DjVuImage.php create mode 100644 includes/EditPage.php create mode 100644 includes/Exception.php create mode 100644 includes/Exif.php create mode 100644 includes/Export.php create mode 100644 includes/ExternalEdit.php create mode 100644 includes/ExternalStore.php create mode 100644 includes/ExternalStoreDB.php create mode 100644 includes/ExternalStoreHttp.php create mode 100644 includes/FakeTitle.php create mode 100644 includes/Feed.php create mode 100644 includes/FileStore.php create mode 100644 includes/GlobalFunctions.php create mode 100644 includes/HTMLCacheUpdate.php create mode 100644 includes/HTMLForm.php create mode 100644 includes/HistoryBlob.php create mode 100644 includes/Hooks.php create mode 100644 includes/HttpFunctions.php create mode 100644 includes/Image.php create mode 100644 includes/ImageFunctions.php create mode 100644 includes/ImageGallery.php create mode 100644 includes/ImagePage.php create mode 100644 includes/JobQueue.php create mode 100644 includes/Licenses.php create mode 100644 includes/LinkBatch.php create mode 100644 includes/LinkCache.php create mode 100644 includes/LinkFilter.php create mode 100644 includes/Linker.php create mode 100644 includes/LinksUpdate.php create mode 100644 includes/LoadBalancer.php create mode 100644 includes/LogPage.php create mode 100644 includes/MacBinary.php create mode 100644 includes/MagicWord.php create mode 100644 includes/Math.php create mode 100644 includes/MemcachedSessions.php create mode 100644 includes/MessageCache.php create mode 100644 includes/Metadata.php create mode 100644 includes/MimeMagic.php create mode 100644 includes/Namespace.php create mode 100644 includes/ObjectCache.php create mode 100644 includes/OutputPage.php create mode 100644 includes/PageHistory.php create mode 100644 includes/Parser.php create mode 100644 includes/ParserCache.php create mode 100644 includes/ParserXML.php create mode 100644 includes/ProfilerSimple.php create mode 100644 includes/ProfilerSimpleUDP.php create mode 100644 includes/ProfilerStub.php create mode 100644 includes/Profiling.php create mode 100644 includes/ProtectionForm.php create mode 100644 includes/ProxyTools.php create mode 100644 includes/QueryPage.php create mode 100644 includes/RawPage.php create mode 100644 includes/RecentChange.php create mode 100644 includes/Revision.php create mode 100644 includes/Sanitizer.php create mode 100644 includes/SearchEngine.php create mode 100644 includes/SearchMySQL.php create mode 100644 includes/SearchMySQL4.php create mode 100644 includes/SearchPostgres.php create mode 100644 includes/SearchTsearch2.php create mode 100644 includes/SearchUpdate.php create mode 100644 includes/Setup.php create mode 100644 includes/SiteConfiguration.php create mode 100644 includes/SiteStatsUpdate.php create mode 100644 includes/Skin.php create mode 100644 includes/SkinTemplate.php create mode 100644 includes/SpecialAllmessages.php create mode 100644 includes/SpecialAllpages.php create mode 100644 includes/SpecialAncientpages.php create mode 100644 includes/SpecialBlockip.php create mode 100644 includes/SpecialBlockme.php create mode 100644 includes/SpecialBooksources.php create mode 100644 includes/SpecialBrokenRedirects.php create mode 100644 includes/SpecialCategories.php create mode 100644 includes/SpecialConfirmemail.php create mode 100644 includes/SpecialContributions.php create mode 100644 includes/SpecialDeadendpages.php create mode 100644 includes/SpecialDisambiguations.php create mode 100644 includes/SpecialDoubleRedirects.php create mode 100644 includes/SpecialEmailuser.php create mode 100644 includes/SpecialExport.php create mode 100644 includes/SpecialImagelist.php create mode 100644 includes/SpecialImport.php create mode 100644 includes/SpecialIpblocklist.php create mode 100644 includes/SpecialListredirects.php create mode 100644 includes/SpecialListusers.php create mode 100644 includes/SpecialLockdb.php create mode 100644 includes/SpecialLog.php create mode 100644 includes/SpecialLonelypages.php create mode 100644 includes/SpecialLongpages.php create mode 100644 includes/SpecialMIMEsearch.php create mode 100644 includes/SpecialMostcategories.php create mode 100644 includes/SpecialMostimages.php create mode 100644 includes/SpecialMostlinked.php create mode 100644 includes/SpecialMostlinkedcategories.php create mode 100644 includes/SpecialMostrevisions.php create mode 100644 includes/SpecialMovepage.php create mode 100644 includes/SpecialNewimages.php create mode 100644 includes/SpecialNewpages.php create mode 100644 includes/SpecialPage.php create mode 100644 includes/SpecialPopularpages.php create mode 100644 includes/SpecialPreferences.php create mode 100644 includes/SpecialPrefixindex.php create mode 100644 includes/SpecialRandompage.php create mode 100644 includes/SpecialRandomredirect.php create mode 100644 includes/SpecialRecentchanges.php create mode 100644 includes/SpecialRecentchangeslinked.php create mode 100644 includes/SpecialRevisiondelete.php create mode 100644 includes/SpecialSearch.php create mode 100644 includes/SpecialShortpages.php create mode 100644 includes/SpecialSpecialpages.php create mode 100644 includes/SpecialStatistics.php create mode 100644 includes/SpecialUncategorizedcategories.php create mode 100644 includes/SpecialUncategorizedimages.php create mode 100644 includes/SpecialUncategorizedpages.php create mode 100644 includes/SpecialUndelete.php create mode 100644 includes/SpecialUnlockdb.php create mode 100644 includes/SpecialUnusedcategories.php create mode 100644 includes/SpecialUnusedimages.php create mode 100644 includes/SpecialUnusedtemplates.php create mode 100644 includes/SpecialUnwatchedpages.php create mode 100644 includes/SpecialUpload.php create mode 100644 includes/SpecialUploadMogile.php create mode 100644 includes/SpecialUserlogin.php create mode 100644 includes/SpecialUserlogout.php create mode 100644 includes/SpecialUserrights.php create mode 100644 includes/SpecialVersion.php create mode 100644 includes/SpecialWantedcategories.php create mode 100644 includes/SpecialWantedpages.php create mode 100644 includes/SpecialWatchlist.php create mode 100644 includes/SpecialWhatlinkshere.php create mode 100644 includes/SquidUpdate.php create mode 100644 includes/StreamFile.php create mode 100644 includes/Title.php create mode 100644 includes/User.php create mode 100644 includes/UserMailer.php create mode 100644 includes/Utf8Case.php create mode 100644 includes/WatchedItem.php create mode 100644 includes/WebRequest.php create mode 100644 includes/Wiki.php create mode 100644 includes/WikiError.php create mode 100644 includes/Xml.php create mode 100644 includes/XmlFunctions.php create mode 100644 includes/ZhClient.php create mode 100644 includes/ZhConversion.php create mode 100644 includes/cbt/CBTCompiler.php create mode 100644 includes/cbt/CBTProcessor.php create mode 100644 includes/cbt/README create mode 100644 includes/memcached-client.php create mode 100644 includes/mime.info create mode 100644 includes/mime.types create mode 100644 includes/normal/CleanUpTest.php create mode 100644 includes/normal/Makefile create mode 100644 includes/normal/README create mode 100644 includes/normal/RandomTest.php create mode 100644 includes/normal/Utf8Test.php create mode 100644 includes/normal/UtfNormal.php create mode 100644 includes/normal/UtfNormalBench.php create mode 100644 includes/normal/UtfNormalData.inc create mode 100644 includes/normal/UtfNormalDataK.inc create mode 100644 includes/normal/UtfNormalGenerate.php create mode 100644 includes/normal/UtfNormalTest.php create mode 100644 includes/normal/UtfNormalUtil.php create mode 100644 includes/proxy_check.php create mode 100644 includes/templates/Userlogin.php create mode 100644 includes/zhtable/Makefile create mode 100644 includes/zhtable/README create mode 100644 includes/zhtable/printutf8.c create mode 100644 includes/zhtable/simp2trad.manual create mode 100644 includes/zhtable/toCN.manual create mode 100644 includes/zhtable/toHK.manual create mode 100644 includes/zhtable/toSG.manual create mode 100644 includes/zhtable/toTW.manual create mode 100644 includes/zhtable/trad2simp.manual create mode 100644 includes/zhtable/tradphrases.manual (limited to 'includes') diff --git a/includes/.htaccess b/includes/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/includes/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php new file mode 100644 index 00000000..2084c366 --- /dev/null +++ b/includes/AjaxDispatcher.php @@ -0,0 +1,83 @@ +mode = ""; + + if (! empty($_GET["rs"])) { + $this->mode = "get"; + } + + if (!empty($_POST["rs"])) { + $this->mode = "post"; + } + + if ($this->mode == "get") { + $this->func_name = $_GET["rs"]; + if (! empty($_GET["rsargs"])) { + $this->args = $_GET["rsargs"]; + } else { + $this->args = array(); + } + } else { + $this->func_name = $_POST["rs"]; + if (! empty($_POST["rsargs"])) { + $this->args = $_POST["rsargs"]; + } else { + $this->args = array(); + } + } + wfProfileOut( 'AjaxDispatcher::AjaxDispatcher' ); + } + + function performAction() { + global $wgAjaxCachePolicy, $wgAjaxExportList; + if ( empty( $this->mode ) ) { + return; + } + wfProfileIn( 'AjaxDispatcher::performAction' ); + + if (! in_array( $this->func_name, $wgAjaxExportList ) ) { + echo "-:{$this->func_name} not callable"; + } else { + echo "+:"; + $result = call_user_func_array($this->func_name, $this->args); + header( 'Content-Type: text/html; charset=utf-8', true ); + $wgAjaxCachePolicy->writeHeader(); + echo $result; + } + wfProfileOut( 'AjaxDispatcher::performAction' ); + exit; + } +} + +?> diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php new file mode 100644 index 00000000..4387a607 --- /dev/null +++ b/includes/AjaxFunctions.php @@ -0,0 +1,157 @@ +>6)+192).chr(($num&63)+128); + if ( $num<65536 ) + return chr(($num>>12)+224).chr((($num>>6)&63)+128).chr(($num&63)+128); + if ( $num<2097152 ) + return chr(($num>>18)+240).chr((($num>>12)&63)+128).chr((($num>>6)&63)+128) .chr(($num&63)+128); + return ''; +} + +class AjaxCachePolicy { + var $policy; + + function AjaxCachePolicy( $policy = null ) { + $this->policy = $policy; + } + + function setPolicy( $policy ) { + $this->policy = $policy; + } + + function writeHeader() { + header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); + if ( is_null( $this->policy ) ) { + // Bust cache in the head + header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past + // always modified + header ("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 + header ("Pragma: no-cache"); // HTTP/1.0 + } else { + header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->policy ) . " GMT"); + header ("Cache-Control: s-max-age={$this->policy},public,max-age={$this->policy}"); + } + } +} + + +function wfSajaxSearch( $term ) { + global $wgContLang, $wgAjaxCachePolicy, $wgOut; + $limit = 16; + + $l = new Linker; + + $term = str_replace( ' ', '_', $wgContLang->ucfirst( + $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) ) + ) ); + + if ( strlen( str_replace( '_', '', $term ) )<3 ) + return; + + $wgAjaxCachePolicy->setPolicy( 30*60 ); + + $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 = ""; + + $i=0; + while ( ( $row = $db->fetchObject( $res ) ) && ( ++$i <= $limit ) ) { + $nt = Title::newFromDBkey( $row->page_title ); + $r .= '
  • ' . $l->makeKnownLinkObj( $nt ) . "
  • \n"; + } + if ( $i > $limit ) { + $more = '' . $l->makeKnownLink( $wgContLang->specialPage( "Allpages" ), + wfMsg('moredotdotdot'), + "namespace=0&from=" . wfUrlEncode ( $term ) ) . + ''; + } else { + $more = ''; + } + + $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); + $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); + + $term = htmlspecialchars( $term ); + return '
    ' + . wfMsg( 'hideresults' ) . '
    ' + . '

    '.wfMsg('search') + . '

    '. $subtitle . '

    " . wfMsg( 'articletitles', $term ) . "

    " + . ''.$more; +} + +?> diff --git a/includes/Article.php b/includes/Article.php new file mode 100644 index 00000000..b1e1f620 --- /dev/null +++ b/includes/Article.php @@ -0,0 +1,2575 @@ +mTitle =& $title; + $this->mOldId = $oldId; + $this->clear(); + } + + /** + * Tell the page view functions that this view was redirected + * from another page on the wiki. + * @param $from Title object. + */ + function setRedirectedFrom( $from ) { + $this->mRedirectedFrom = $from; + } + + /** + * @return mixed false, Title of in-wiki target, or string with URL + */ + function followRedirect() { + $text = $this->getContent(); + $rt = Title::newFromRedirect( $text ); + + # process if title object is valid and not special:userlogout + if( $rt ) { + if( $rt->getInterwiki() != '' ) { + if( $rt->isLocal() ) { + // Offsite wikis need an HTTP redirect. + // + // This can be hard to reverse and may produce loops, + // so they may be disabled in the site configuration. + + $source = $this->mTitle->getFullURL( 'redirect=no' ); + return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); + } + } else { + if( $rt->getNamespace() == NS_SPECIAL ) { + // Gotta hand redirects to special pages differently: + // Fill the HTTP response "Location" header and ignore + // the rest of the page we're on. + // + // This can be hard to reverse, so they may be disabled. + + if( $rt->getNamespace() == NS_SPECIAL && $rt->getText() == 'Userlogout' ) { + // rolleyes + } else { + return $rt->getFullURL(); + } + } + return $rt; + } + } + + // No or invalid redirect + return false; + } + + /** + * get the title object of the article + */ + function getTitle() { + return $this->mTitle; + } + + /** + * Clear the object + * @private + */ + function clear() { + $this->mDataLoaded = false; + $this->mContentLoaded = false; + + $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded + $this->mRedirectedFrom = null; # Title object if set + $this->mUserText = + $this->mTimestamp = $this->mComment = ''; + $this->mGoodAdjustment = $this->mTotalAdjustment = 0; + $this->mTouched = '19700101000000'; + $this->mForUpdate = false; + $this->mIsRedirect = false; + $this->mRevIdFetched = 0; + $this->mRedirectUrl = false; + $this->mLatest = false; + } + + /** + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in Article::followContent() + * FIXME + * @todo There are still side-effects in this! + * In general, you should use the Revision class, not Article, + * to fetch text for purposes other than page views. + * + * @return Return the text of this revision + */ + function getContent() { + global $wgRequest, $wgUser, $wgOut; + + wfProfileIn( __METHOD__ ); + + if ( 0 == $this->getID() ) { + wfProfileOut( __METHOD__ ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + } else { + $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + } + + return "
    $ret
    "; + } else { + $this->loadContent(); + wfProfileOut( __METHOD__ ); + return $this->mContent; + } + } + + /** + * This function returns the text of a section, specified by a number ($section). + * A section is text under a heading like == Heading == or \Heading\, 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 + * @return string text of the requested section + * @deprecated + */ + function getSection($text,$section) { + global $wgParser; + return $wgParser->getSection( $text, $section ); + } + + /** + * @return int The oldid of the article that is to be shown, 0 for the + * current revision + */ + function getOldID() { + if ( is_null( $this->mOldId ) ) { + $this->mOldId = $this->getOldIDFromRequest(); + } + return $this->mOldId; + } + + /** + * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect + * + * @return int The old id for the request + */ + function getOldIDFromRequest() { + global $wgRequest; + $this->mRedirectUrl = false; + $oldid = $wgRequest->getVal( 'oldid' ); + if ( isset( $oldid ) ) { + $oldid = intval( $oldid ); + if ( $wgRequest->getVal( 'direction' ) == 'next' ) { + $nextid = $this->mTitle->getNextRevisionID( $oldid ); + if ( $nextid ) { + $oldid = $nextid; + } else { + $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); + } + } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { + $previd = $this->mTitle->getPreviousRevisionID( $oldid ); + if ( $previd ) { + $oldid = $previd; + } else { + # TODO + } + } + # unused: + # $lastid = $oldid; + } + + if ( !$oldid ) { + $oldid = 0; + } + return $oldid; + } + + /** + * Load the revision (including text) into this object + */ + function loadContent() { + if ( $this->mContentLoaded ) return; + + # Query variables :P + $oldid = $this->getOldID(); + + # Pre-fill content with error message so that if something + # fails we'll have something telling us what we intended. + + $t = $this->mTitle->getPrefixedText(); + + $this->mOldId = $oldid; + $this->fetchContent( $oldid ); + } + + + /** + * Fetch a page record with the given conditions + * @param Database $dbr + * @param array $conditions + * @private + */ + function pageData( &$dbr, $conditions ) { + $fields = array( + 'page_id', + 'page_namespace', + 'page_title', + 'page_restrictions', + 'page_counter', + 'page_is_redirect', + 'page_is_new', + 'page_random', + 'page_touched', + 'page_latest', + 'page_len' ) ; + wfRunHooks( 'ArticlePageDataBefore', array( &$this , &$fields ) ) ; + $row = $dbr->selectRow( 'page', + $fields, + $conditions, + 'Article::pageData' ); + wfRunHooks( 'ArticlePageDataAfter', array( &$this , &$row ) ) ; + return $row ; + } + + /** + * @param Database $dbr + * @param Title $title + */ + function pageDataFromTitle( &$dbr, $title ) { + return $this->pageData( $dbr, array( + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey() ) ); + } + + /** + * @param Database $dbr + * @param int $id + */ + function pageDataFromId( &$dbr, $id ) { + return $this->pageData( $dbr, array( 'page_id' => $id ) ); + } + + /** + * Set the general counter, title etc data loaded from + * some source. + * + * @param object $data + * @private + */ + function loadPageData( $data = 'fromdb' ) { + if ( $data === 'fromdb' ) { + $dbr =& $this->getDB(); + $data = $this->pageDataFromId( $dbr, $this->getId() ); + } + + $lc =& LinkCache::singleton(); + if ( $data ) { + $lc->addGoodLinkObj( $data->page_id, $this->mTitle ); + + $this->mTitle->mArticleID = $data->page_id; + $this->mTitle->loadRestrictions( $data->page_restrictions ); + $this->mTitle->mRestrictionsLoaded = true; + + $this->mCounter = $data->page_counter; + $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); + $this->mIsRedirect = $data->page_is_redirect; + $this->mLatest = $data->page_latest; + } else { + if ( is_object( $this->mTitle ) ) { + $lc->addBadLinkObj( $this->mTitle ); + } + $this->mTitle->mArticleID = 0; + } + + $this->mDataLoaded = true; + } + + /** + * Get text of an article from database + * Does *NOT* follow redirects. + * @param int $oldid 0 for whatever the latest revision is + * @return string + */ + function fetchContent( $oldid = 0 ) { + if ( $this->mContentLoaded ) { + return $this->mContent; + } + + $dbr =& $this->getDB(); + + # Pre-fill content with error message so that if something + # fails we'll have something telling us what we intended. + $t = $this->mTitle->getPrefixedText(); + if( $oldid ) { + $t .= ',oldid='.$oldid; + } + $this->mContent = wfMsg( 'missingarticle', $t ) ; + + if( $oldid ) { + $revision = Revision::newFromId( $oldid ); + if( is_null( $revision ) ) { + wfDebug( __METHOD__." failed to retrieve specified revision, id $oldid\n" ); + return false; + } + $data = $this->pageDataFromId( $dbr, $revision->getPage() ); + if( !$data ) { + wfDebug( __METHOD__." failed to get page data linked to revision id $oldid\n" ); + return false; + } + $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title ); + $this->loadPageData( $data ); + } else { + if( !$this->mDataLoaded ) { + $data = $this->pageDataFromTitle( $dbr, $this->mTitle ); + if( !$data ) { + wfDebug( __METHOD__." failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); + return false; + } + $this->loadPageData( $data ); + } + $revision = Revision::newFromId( $this->mLatest ); + if( is_null( $revision ) ) { + wfDebug( __METHOD__." failed to retrieve current page, rev_id {$data->page_latest}\n" ); + return false; + } + } + + // FIXME: Horrible, horrible! This content-loading interface just plain sucks. + // We should instead work with the Revision object when we need it... + $this->mContent = $revision->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : ""; + //$this->mContent = $revision->getText(); + + $this->mUser = $revision->getUser(); + $this->mUserText = $revision->getUserText(); + $this->mComment = $revision->getComment(); + $this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() ); + + $this->mRevIdFetched = $revision->getID(); + $this->mContentLoaded = true; + $this->mRevision =& $revision; + + wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ) ; + + return $this->mContent; + } + + /** + * Read/write accessor to select FOR UPDATE + * + * @param $x Mixed: FIXME + */ + function forUpdate( $x = NULL ) { + return wfSetVar( $this->mForUpdate, $x ); + } + + /** + * Get the database which should be used for reads + * + * @return Database + */ + function &getDB() { + $ret =& wfGetDB( DB_MASTER ); + return $ret; + } + + /** + * Get options for all SELECT statements + * + * @param $options Array: an optional options array which'll be appended to + * the default + * @return Array: options + */ + function getSelectOptions( $options = '' ) { + if ( $this->mForUpdate ) { + if ( is_array( $options ) ) { + $options[] = 'FOR UPDATE'; + } else { + $options = 'FOR UPDATE'; + } + } + return $options; + } + + /** + * @return int Page ID + */ + function getID() { + if( $this->mTitle ) { + return $this->mTitle->getArticleID(); + } else { + return 0; + } + } + + /** + * @return bool Whether or not the page exists in the database + */ + function exists() { + return $this->getId() != 0; + } + + /** + * @return int The view count for the page + */ + function getCount() { + if ( -1 == $this->mCounter ) { + $id = $this->getID(); + if ( $id == 0 ) { + $this->mCounter = 0; + } else { + $dbr =& wfGetDB( DB_SLAVE ); + $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ), + 'Article::getCount', $this->getSelectOptions() ); + } + } + return $this->mCounter; + } + + /** + * Determine whether a page would be suitable for being counted as an + * article in the site_stats table based on the title & its content + * + * @param $text String: text to analyze + * @return bool + */ + function isCountable( $text ) { + global $wgUseCommaCount, $wgContentNamespaces; + + $token = $wgUseCommaCount ? ',' : '[['; + return + array_search( $this->mTitle->getNamespace(), $wgContentNamespaces ) !== false + && ! $this->isRedirect( $text ) + && in_string( $token, $text ); + } + + /** + * Tests if the article text represents a redirect + * + * @param $text String: FIXME + * @return bool + */ + function isRedirect( $text = false ) { + if ( $text === false ) { + $this->loadContent(); + $titleObj = Title::newFromRedirect( $this->fetchContent() ); + } else { + $titleObj = Title::newFromRedirect( $text ); + } + return $titleObj !== NULL; + } + + /** + * Returns true if the currently-referenced revision is the current edit + * to this page (and it exists). + * @return bool + */ + function isCurrent() { + return $this->exists() && + isset( $this->mRevision ) && + $this->mRevision->isCurrent(); + } + + /** + * Loads everything except the text + * This isn't necessary for all uses, so it's only done if needed. + * @private + */ + function loadLastEdit() { + if ( -1 != $this->mUser ) + return; + + # New or non-existent articles have no user information + $id = $this->getID(); + if ( 0 == $id ) return; + + $this->mLastRevision = Revision::loadFromPageId( $this->getDB(), $id ); + if( !is_null( $this->mLastRevision ) ) { + $this->mUser = $this->mLastRevision->getUser(); + $this->mUserText = $this->mLastRevision->getUserText(); + $this->mTimestamp = $this->mLastRevision->getTimestamp(); + $this->mComment = $this->mLastRevision->getComment(); + $this->mMinorEdit = $this->mLastRevision->isMinor(); + $this->mRevIdFetched = $this->mLastRevision->getID(); + } + } + + function getTimestamp() { + // Check if the field has been filled by ParserCache::get() + if ( !$this->mTimestamp ) { + $this->loadLastEdit(); + } + return wfTimestamp(TS_MW, $this->mTimestamp); + } + + function getUser() { + $this->loadLastEdit(); + return $this->mUser; + } + + function getUserText() { + $this->loadLastEdit(); + return $this->mUserText; + } + + function getComment() { + $this->loadLastEdit(); + return $this->mComment; + } + + function getMinorEdit() { + $this->loadLastEdit(); + return $this->mMinorEdit; + } + + function getRevIdFetched() { + $this->loadLastEdit(); + return $this->mRevIdFetched; + } + + /** + * @todo Document, fixme $offset never used. + * @param $limit Integer: default 0. + * @param $offset Integer: default 0. + */ + function getContributors($limit = 0, $offset = 0) { + # XXX: this is expensive; cache this info somewhere. + + $title = $this->mTitle; + $contribs = array(); + $dbr =& wfGetDB( DB_SLAVE ); + $revTable = $dbr->tableName( 'revision' ); + $userTable = $dbr->tableName( 'user' ); + $encDBkey = $dbr->addQuotes( $title->getDBkey() ); + $ns = $title->getNamespace(); + $user = $this->getUser(); + $pageId = $this->getId(); + + $sql = "SELECT rev_user, rev_user_text, user_real_name, MAX(rev_timestamp) as timestamp + FROM $revTable LEFT JOIN $userTable ON rev_user = user_id + WHERE rev_page = $pageId + AND rev_user != $user + GROUP BY rev_user, rev_user_text, user_real_name + ORDER BY timestamp DESC"; + + if ($limit > 0) { $sql .= ' LIMIT '.$limit; } + $sql .= ' '. $this->getSelectOptions(); + + $res = $dbr->query($sql, __METHOD__); + + while ( $line = $dbr->fetchObject( $res ) ) { + $contribs[] = array($line->rev_user, $line->rev_user_text, $line->user_real_name); + } + + $dbr->freeResult($res); + return $contribs; + } + + /** + * This is the default action of the script: just view the page of + * the given title. + */ + function view() { + global $wgUser, $wgOut, $wgRequest, $wgContLang; + global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser; + global $wgUseTrackbacks, $wgNamespaceRobotPolicies; + $sk = $wgUser->getSkin(); + + wfProfileIn( __METHOD__ ); + + $parserCache =& ParserCache::singleton(); + $ns = $this->mTitle->getNamespace(); # shortcut + + # Get variables from query string + $oldid = $this->getOldID(); + + # getOldID may want us to redirect somewhere else + if ( $this->mRedirectUrl ) { + $wgOut->redirect( $this->mRedirectUrl ); + wfProfileOut( __METHOD__ ); + return; + } + + $diff = $wgRequest->getVal( 'diff' ); + $rcid = $wgRequest->getVal( 'rcid' ); + $rdfrom = $wgRequest->getVal( 'rdfrom' ); + + $wgOut->setArticleFlag( true ); + if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + $policy = $wgNamespaceRobotPolicies[$ns]; + } else { + $policy = 'index,follow'; + } + $wgOut->setRobotpolicy( $policy ); + + # If we got diff and oldid in the query, we want to see a + # diff page instead of the article. + + if ( !is_null( $diff ) ) { + require_once( 'DifferenceEngine.php' ); + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); + // DifferenceEngine directly fetched the revision: + $this->mRevIdFetched = $de->mNewid; + $de->showDiffPage(); + + if( $diff == 0 ) { + # Run view updates for current revision only + $this->viewUpdates(); + } + wfProfileOut( __METHOD__ ); + return; + } + + if ( empty( $oldid ) && $this->checkTouched() ) { + $wgOut->setETag($parserCache->getETag($this, $wgUser)); + + if( $wgOut->checkLastModified( $this->mTouched ) ){ + wfProfileOut( __METHOD__ ); + return; + } else if ( $this->tryFileCache() ) { + # tell wgOut that output is taken care of + $wgOut->disable(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + return; + } + } + + # Should the parser cache be used? + $pcache = $wgEnableParserCache && + intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 && + $this->exists() && + empty( $oldid ); + wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); + if ( $wgUser->getOption( 'stubthreshold' ) ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + $wasRedirected = false; + if ( isset( $this->mRedirectedFrom ) ) { + // This is an internally redirected page view. + // We'll need a backlink to the source page for navigation. + if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { + $sk = $wgUser->getSkin(); + $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); + $s = wfMsg( 'redirectedfrom', $redir ); + $wgOut->setSubtitle( $s ); + $wasRedirected = true; + } + } elseif ( !empty( $rdfrom ) ) { + // This is an externally redirected view, from some other wiki. + // If it was reported from a trusted site, supply a backlink. + global $wgRedirectSources; + if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { + $sk = $wgUser->getSkin(); + $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); + $s = wfMsg( 'redirectedfrom', $redir ); + $wgOut->setSubtitle( $s ); + $wasRedirected = true; + } + } + + $outputDone = false; + if ( $pcache ) { + if ( $wgOut->tryParserCache( $this, $wgUser ) ) { + $outputDone = true; + } + } + if ( !$outputDone ) { + $text = $this->getContent(); + if ( $text === false ) { + # Failed to load, replace text with error message + $t = $this->mTitle->getPrefixedText(); + if( $oldid ) { + $t .= ',oldid='.$oldid; + $text = wfMsg( 'missingarticle', $t ); + } else { + $text = wfMsg( 'noarticletext', $t ); + } + } + + # Another whitelist check in case oldid is altering the title + if ( !$this->mTitle->userCanRead() ) { + $wgOut->loginToUse(); + $wgOut->output(); + exit; + } + + # We're looking at an old revision + + if ( !empty( $oldid ) ) { + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + if( is_null( $this->mRevision ) ) { + // FIXME: This would be a nice place to load the 'no such page' text. + } else { + $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->setPageTitle( $this->mTitle->getPrefixedText() ); + return; + } else { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + // and we are allowed to see... + } + } + } + + } + } + if( !$outputDone ) { + /** + * @fixme: this hook doesn't work most of the time, as it doesn't + * trigger when the parser cache is used. + */ + wfRunHooks( 'ArticleViewHeader', array( &$this ) ) ; + $wgOut->setRevisionId( $this->getRevIdFetched() ); + # wrap user css and user js in pre and don't parse + # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found + if ( + $ns == NS_USER && + preg_match('/\\/[\\w]+\\.(css|js)$/', $this->mTitle->getDBkey()) + ) { + $wgOut->addWikiText( wfMsg('clearyourcache')); + $wgOut->addHTML( '
    '.htmlspecialchars($this->mContent)."\n
    " ); + } else if ( $rt = Title::newFromRedirect( $text ) ) { + # Display redirect + $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; + $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; + # Don't overwrite the subtitle if this was an old revision + if( !$wasRedirected && $this->isCurrent() ) { + $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) ); + } + $targetUrl = $rt->escapeLocalURL(); + # fixme unused $titleText : + $titleText = htmlspecialchars( $rt->getPrefixedText() ); + $link = $sk->makeLinkObj( $rt ); + + $wgOut->addHTML( '#REDIRECT' . + ''.$link.'' ); + + $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); + $wgOut->addParserOutputNoText( $parseout ); + } else if ( $pcache ) { + # Display content and save to parser cache + $wgOut->addPrimaryWikiText( $text, $this ); + } else { + # Display content, don't attempt to save to parser cache + # Don't show section-edit links on old revisions... this way lies madness. + if( !$this->isCurrent() ) { + $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + } + # Display content and don't save to parser cache + $wgOut->addPrimaryWikiText( $text, $this, false ); + + if( !$this->isCurrent() ) { + $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + } + } + } + /* title may have been set from the cache */ + $t = $wgOut->getPageTitle(); + if( empty( $t ) ) { + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + } + + # 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') ); + } + + # 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' ) ) { + $wgOut->addHTML( + "' + ); + } + + # Trackbacks + if ($wgUseTrackbacks) + $this->addTrackbacks(); + + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + } + + function addTrackbacks() { + global $wgOut, $wgUser; + + $dbr =& wfGetDB(DB_SLAVE); + $tbs = $dbr->select( + /* FROM */ 'trackbacks', + /* SELECT */ array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'), + /* WHERE */ array('tb_page' => $this->getID()) + ); + + if (!$dbr->numrows($tbs)) + return; + + $tbtext = ""; + while ($o = $dbr->fetchObject($tbs)) { + $rmvtxt = ""; + if ($wgUser->isAllowed( 'trackback' )) { + $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" + . $o->tb_id . "&token=" . $wgUser->editToken()); + $rmvtxt = wfMsg('trackbackremove', $delurl); + } + $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback', + $o->tb_title, + $o->tb_url, + $o->tb_ex, + $o->tb_name, + $rmvtxt); + } + $wgOut->addWikitext(wfMsg('trackbackbox', $tbtext)); + } + + function deletetrackback() { + global $wgUser, $wgRequest, $wgOut, $wgTitle; + + if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) { + $wgOut->addWikitext(wfMsg('sessionfailure')); + return; + } + + if ((!$wgUser->isAllowed('delete'))) { + $wgOut->sysopRequired(); + return; + } + + if (wfReadOnly()) { + $wgOut->readOnlyPage(); + return; + } + + $db =& wfGetDB(DB_MASTER); + $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); + $wgTitle->invalidateCache(); + $wgOut->addWikiText(wfMsg('trackbackdeleteok')); + } + + function render() { + global $wgOut; + + $wgOut->setArticleBodyOnly(true); + $this->view(); + } + + /** + * Handle action=purge + */ + function purge() { + global $wgUser, $wgRequest, $wgOut; + + if ( $wgUser->isLoggedIn() || $wgRequest->wasPosted() ) { + if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { + $this->doPurge(); + } + } else { + $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) ); + $action = $this->mTitle->escapeLocalURL( 'action=purge' ); + $button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) ); + $msg = str_replace( '$1', + "
    \n" . + "\n" . + "
    \n", $msg ); + + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( $msg ); + } + } + + /** + * Perform the actions of a page purging + */ + function doPurge() { + global $wgUseSquid; + // Invalidate the cache + $this->mTitle->invalidateCache(); + + if ( $wgUseSquid ) { + // Commit the transaction before the purge is sent + $dbw = wfGetDB( DB_MASTER ); + $dbw->immediateCommit(); + + // Send purge + $update = SquidUpdate::newSimplePurge( $this->mTitle ); + $update->doUpdate(); + } + $this->view(); + } + + /** + * Insert a new empty page record for this article. + * This *must* be followed up by creating a revision + * and running $this->updateToLatest( $rev_id ); + * or else the record will be left in a funky state. + * Best if all done inside a transaction. + * + * @param Database $dbw + * @param string $restrictions + * @return int The newly created page_id key + * @private + */ + function insertOn( &$dbw, $restrictions = '' ) { + wfProfileIn( __METHOD__ ); + + $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); + $dbw->insert( 'page', array( + 'page_id' => $page_id, + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'page_counter' => 0, + 'page_restrictions' => $restrictions, + 'page_is_redirect' => 0, # Will set this shortly... + 'page_is_new' => 1, + 'page_random' => wfRandom(), + 'page_touched' => $dbw->timestamp(), + 'page_latest' => 0, # Fill this in shortly... + 'page_len' => 0, # Fill this in shortly... + ), __METHOD__ ); + $newid = $dbw->insertId(); + + $this->mTitle->resetArticleId( $newid ); + + wfProfileOut( __METHOD__ ); + return $newid; + } + + /** + * Update the page record to point to a newly saved revision. + * + * @param Database $dbw + * @param Revision $revision For ID number, and text used to set + length and redirect status fields + * @param int $lastRevision If given, will not overwrite the page field + * when different from the currently set value. + * Giving 0 indicates the new page flag should + * be set on. + * @return bool true on success, false on failure + * @private + */ + function updateRevisionOn( &$dbw, $revision, $lastRevision = null ) { + wfProfileIn( __METHOD__ ); + + $conditions = array( 'page_id' => $this->getId() ); + if( !is_null( $lastRevision ) ) { + # An extra check against threads stepping on each other + $conditions['page_latest'] = $lastRevision; + } + + $text = $revision->getText(); + $dbw->update( 'page', + array( /* SET */ + 'page_latest' => $revision->getId(), + 'page_touched' => $dbw->timestamp(), + 'page_is_new' => ($lastRevision === 0) ? 1 : 0, + 'page_is_redirect' => Article::isRedirect( $text ) ? 1 : 0, + 'page_len' => strlen( $text ), + ), + $conditions, + __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return ( $dbw->affectedRows() != 0 ); + } + + /** + * If the given revision is newer than the currently set page_latest, + * update the page record. Otherwise, do nothing. + * + * @param Database $dbw + * @param Revision $revision + */ + function updateIfNewerOn( &$dbw, $revision ) { + wfProfileIn( __METHOD__ ); + + $row = $dbw->selectRow( + array( 'revision', 'page' ), + array( 'rev_id', 'rev_timestamp' ), + array( + 'page_id' => $this->getId(), + 'page_latest=rev_id' ), + __METHOD__ ); + if( $row ) { + if( wfTimestamp(TS_MW, $row->rev_timestamp) >= $revision->getTimestamp() ) { + wfProfileOut( __METHOD__ ); + return false; + } + $prev = $row->rev_id; + } else { + # No or missing previous revision; mark the page as new + $prev = 0; + } + + $ret = $this->updateRevisionOn( $dbw, $revision, $prev ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * @return string Complete article text, or null if error + */ + function replaceSection($section, $text, $summary = '', $edittime = NULL) { + wfProfileIn( __METHOD__ ); + + if( $section == '' ) { + // Whole-page edit; let the text through unmolested. + } else { + if( is_null( $edittime ) ) { + $rev = Revision::newFromTitle( $this->mTitle ); + } else { + $dbw =& wfGetDB( DB_MASTER ); + $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); + } + if( is_null( $rev ) ) { + wfDebug( "Article::replaceSection asked for bogus section (page: " . + $this->getId() . "; section: $section; edittime: $edittime)\n" ); + return null; + } + $oldtext = $rev->getText(); + + if($section=='new') { + if($summary) $subject="== {$summary} ==\n\n"; + $text=$oldtext."\n\n".$subject.$text; + } else { + global $wgParser; + $text = $wgParser->replaceSection( $oldtext, $section, $text ); + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * @deprecated use Article::doEdit() + */ + function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { + $flags = EDIT_NEW | EDIT_DEFER_UPDATES | + ( $isminor ? EDIT_MINOR : 0 ) | + ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ); + + # If this is a comment, add the summary as headline + if ( $comment && $summary != "" ) { + $text = "== {$summary} ==\n\n".$text; + } + + $this->doEdit( $text, $summary, $flags ); + + $dbw =& wfGetDB( DB_MASTER ); + if ($watchthis) { + if (!$this->mTitle->userIsWatching()) { + $dbw->begin(); + $this->doWatch(); + $dbw->commit(); + } + } else { + if ( $this->mTitle->userIsWatching() ) { + $dbw->begin(); + $this->doUnwatch(); + $dbw->commit(); + } + } + $this->doRedirect( $this->isRedirect( $text ) ); + } + + /** + * @deprecated use Article::doEdit() + */ + function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { + $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | + ( $minor ? EDIT_MINOR : 0 ) | + ( $forceBot ? EDIT_FORCE_BOT : 0 ); + + $good = $this->doEdit( $text, $summary, $flags ); + if ( $good ) { + $dbw =& wfGetDB( DB_MASTER ); + if ($watchthis) { + if (!$this->mTitle->userIsWatching()) { + $dbw->begin(); + $this->doWatch(); + $dbw->commit(); + } + } else { + if ( $this->mTitle->userIsWatching() ) { + $dbw->begin(); + $this->doUnwatch(); + $dbw->commit(); + } + } + + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor ); + } + return $good; + } + + /** + * Article::doEdit() + * + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * $wgUser must be set before calling this function. + * + * @param string $text New text + * @param string $summary Edit summary + * @param integer $flags bitfield: + * EDIT_NEW + * Article is known or assumed to be non-existent, create a new one + * EDIT_UPDATE + * Article is known or assumed to be pre-existing, update it + * EDIT_MINOR + * Mark this edit minor, if the user is allowed to do so + * EDIT_SUPPRESS_RC + * Do not log the change in recentchanges + * EDIT_FORCE_BOT + * Mark the edit a "bot" edit regardless of user rights + * EDIT_DEFER_UPDATES + * Defer some of the updates until the end of index.php + * + * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. + * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If + * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception + * to be thrown from the Database. These two conditions are also possible with auto-detection due + * to MediaWiki's performance-optimised locking strategy. + * + * @return bool success + */ + function doEdit( $text, $summary, $flags = 0 ) { + global $wgUser, $wgDBtransactions; + + wfProfileIn( __METHOD__ ); + $good = true; + + if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); + if ( $aid ) { + $flags |= EDIT_UPDATE; + } else { + $flags |= EDIT_NEW; + } + } + + if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text, + &$summary, $flags & EDIT_MINOR, + null, null, &$flags ) ) ) + { + wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + + # Silently ignore EDIT_MINOR if not allowed + $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); + $bot = $wgUser->isBot() || ( $flags & EDIT_FORCE_BOT ); + + $text = $this->preSaveTransform( $text ); + + $dbw =& wfGetDB( DB_MASTER ); + $now = wfTimestampNow(); + + if ( $flags & EDIT_UPDATE ) { + # Update article, but only if changed. + + # Make sure the revision is either completely inserted or not inserted at all + if( !$wgDBtransactions ) { + $userAbort = ignore_user_abort( true ); + } + + $oldtext = $this->getContent(); + $oldsize = strlen( $oldtext ); + $newsize = strlen( $text ); + $lastRevision = 0; + $revisionId = 0; + + if ( 0 != strcmp( $text, $oldtext ) ) { + $this->mGoodAdjustment = (int)$this->isCountable( $text ) + - (int)$this->isCountable( $oldtext ); + $this->mTotalAdjustment = 0; + + $lastRevision = $dbw->selectField( + 'page', 'page_latest', array( 'page_id' => $this->getId() ) ); + + if ( !$lastRevision ) { + # Article gone missing + wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + + $revision = new Revision( array( + 'page' => $this->getId(), + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $text + ) ); + + $dbw->begin(); + $revisionId = $revision->insertOn( $dbw ); + + # Update page + $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision ); + + if( !$ok ) { + /* Belated edit conflict! Run away!! */ + $good = false; + $dbw->rollback(); + } else { + # Update recentchanges + if( !( $flags & EDIT_SUPPRESS_RC ) ) { + $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary, + $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId ); + + # Mark as patrolled if the user can do so and has it set in their options + if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) { + RecentChange::markPatrolled( $rcid ); + } + } + $dbw->commit(); + } + } else { + // Keep the same revision ID, but do some updates on it + $revisionId = $this->getRevIdFetched(); + // Update page_touched, this is usually implicit in the page update + // Other cache updates are done in onArticleEdit() + $this->mTitle->invalidateCache(); + } + + if( !$wgDBtransactions ) { + ignore_user_abort( $userAbort ); + } + + if ( $good ) { + # 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 { + # Create new article + + # Set statistics members + # We work out if it's countable after PST to avoid counter drift + # when articles are created with {{subst:}} + $this->mGoodAdjustment = (int)$this->isCountable( $text ); + $this->mTotalAdjustment = 1; + + $dbw->begin(); + + # Add the page record; stake our claim on this title! + # This will fail with a database query exception if the article already exists + $newid = $this->insertOn( $dbw ); + + # Save the revision text... + $revision = new Revision( array( + 'page' => $newid, + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $text + ) ); + $revisionId = $revision->insertOn( $dbw ); + + $this->mTitle->resetArticleID( $newid ); + + # Update the page record with revision data + $this->updateRevisionOn( $dbw, $revision, 0 ); + + if( !( $flags & EDIT_SUPPRESS_RC ) ) { + $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, + '', strlen( $text ), $revisionId ); + # Mark as patrolled if the user can and has the option set + if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) { + RecentChange::markPatrolled( $rcid ); + } + } + $dbw->commit(); + + # Update links, etc. + $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true ); + + # Clear caches + Article::onArticleCreate( $this->mTitle ); + + wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, + $summary, $flags & EDIT_MINOR, + null, null, &$flags ) ); + } + + if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) { + wfDoUpdates(); + } + + wfRunHooks( 'ArticleSaveComplete', + array( &$this, &$wgUser, $text, + $summary, $flags & EDIT_MINOR, + null, null, &$flags ) ); + + wfProfileOut( __METHOD__ ); + return $good; + } + + /** + * @deprecated wrapper for doRedirect + */ + function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor ); + } + + /** + * Output a redirect back to the article. + * This is typically used after an edit. + * + * @param boolean $noRedir Add redirect=no + * @param string $sectionAnchor section to redirect to, including "#" + */ + function doRedirect( $noRedir = false, $sectionAnchor = '' ) { + global $wgOut; + if ( $noRedir ) { + $query = 'redirect=no'; + } else { + $query = ''; + } + $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor ); + } + + /** + * Mark this particular edit as patrolled + */ + function markpatrolled() { + global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser; + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + # Check RC patrol config. option + if( !$wgUseRCPatrol ) { + $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + return; + } + + # Check permissions + if( !$wgUser->isAllowed( 'patrol' ) ) { + $wgOut->permissionRequired( 'patrol' ); + return; + } + + $rcid = $wgRequest->getVal( 'rcid' ); + if ( !is_null ( $rcid ) ) { + if( wfRunHooks( 'MarkPatrolled', array( &$rcid, &$wgUser, false ) ) ) { + RecentChange::markPatrolled( $rcid ); + wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + $wgOut->setPagetitle( wfMsg( 'markedaspatrolled' ) ); + $wgOut->addWikiText( wfMsg( 'markedaspatrolledtext' ) ); + } + $rcTitle = Title::makeTitle( NS_SPECIAL, 'Recentchanges' ); + $wgOut->returnToMain( false, $rcTitle->getPrefixedText() ); + } + else { + $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + } + } + + /** + * User-interface handler for the "watch" action + */ + + function watch() { + + global $wgUser, $wgOut; + + if ( $wgUser->isAnon() ) { + $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); + return; + } + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + + if( $this->doWatch() ) { + $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + $link = $this->mTitle->getPrefixedText(); + $text = wfMsg( 'addedwatchtext', $link ); + $wgOut->addWikiText( $text ); + } + + $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); + } + + /** + * Add this page to $wgUser's watchlist + * @return bool true on successful watch operation + */ + function doWatch() { + global $wgUser; + if( $wgUser->isAnon() ) { + return false; + } + + if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { + $wgUser->addWatch( $this->mTitle ); + $wgUser->saveSettings(); + + return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); + } + + return false; + } + + /** + * User interface handler for the "unwatch" action. + */ + function unwatch() { + + global $wgUser, $wgOut; + + if ( $wgUser->isAnon() ) { + $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); + return; + } + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + + if( $this->doUnwatch() ) { + $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + $link = $this->mTitle->getPrefixedText(); + $text = wfMsg( 'removedwatchtext', $link ); + $wgOut->addWikiText( $text ); + } + + $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); + } + + /** + * Stop watching a page + * @return bool true on successful unwatch + */ + function doUnwatch() { + global $wgUser; + if( $wgUser->isAnon() ) { + return false; + } + + if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) { + $wgUser->removeWatch( $this->mTitle ); + $wgUser->saveSettings(); + + return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); + } + + return false; + } + + /** + * action=protect handler + */ + function protect() { + require_once 'ProtectionForm.php'; + $form = new ProtectionForm( $this ); + $form->show(); + } + + /** + * action=unprotect handler (alias) + */ + function unprotect() { + $this->protect(); + } + + /** + * Update the article's restriction field, and leave a log entry. + * + * @param array $limit set of restriction keys + * @param string $reason + * @return bool true on success + */ + function updateRestrictions( $limit = array(), $reason = '' ) { + global $wgUser, $wgRestrictionTypes, $wgContLang; + + $id = $this->mTitle->getArticleID(); + if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) { + return false; + } + + # FIXME: Same limitations as described in ProtectionForm.php (line 37); + # we expect a single selection, but the schema allows otherwise. + $current = array(); + foreach( $wgRestrictionTypes as $action ) + $current[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); + + $current = Article::flattenRestrictions( $current ); + $updated = Article::flattenRestrictions( $limit ); + + $changed = ( $current != $updated ); + $protect = ( $updated != '' ); + + # If nothing's changed, do nothing + if( $changed ) { + if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { + + $dbw =& wfGetDB( DB_MASTER ); + + # Prepare a null revision to be added to the history + $comment = $wgContLang->ucfirst( wfMsgForContent( $protect ? 'protectedarticle' : 'unprotectedarticle', $this->mTitle->getPrefixedText() ) ); + if( $reason ) + $comment .= ": $reason"; + if( $protect ) + $comment .= " [$updated]"; + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); + + # Update page record + $dbw->update( 'page', + array( /* SET */ + 'page_touched' => $dbw->timestamp(), + 'page_restrictions' => $updated, + 'page_latest' => $nullRevId + ), array( /* WHERE */ + 'page_id' => $id + ), 'Article::protect' + ); + wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); + + # Update the protection log + $log = new LogPage( 'protect' ); + if( $protect ) { + $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]" ) ); + } else { + $log->addEntry( 'unprotect', $this->mTitle, $reason ); + } + + } # End hook + } # End "changed" check + + return true; + } + + /** + * Take an array of page restrictions and flatten it to a string + * suitable for insertion into the page_restrictions field. + * @param array $limit + * @return string + * @private + */ + function flattenRestrictions( $limit ) { + if( !is_array( $limit ) ) { + throw new MWException( 'Article::flattenRestrictions given non-array restriction set' ); + } + $bits = array(); + ksort( $limit ); + foreach( $limit as $action => $restrictions ) { + if( $restrictions != '' ) { + $bits[] = "$action=$restrictions"; + } + } + return implode( ':', $bits ); + } + + /* + * UI entry point for page deletion + */ + function delete() { + global $wgUser, $wgOut, $wgRequest; + $confirm = $wgRequest->wasPosted() && + $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); + $reason = $wgRequest->getText( 'wpReason' ); + + # This code desperately needs to be totally rewritten + + # Check permissions + if( $wgUser->isAllowed( 'delete' ) ) { + if( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); + return; + } + } else { + $wgOut->permissionRequired( 'delete' ); + return; + } + + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + + $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); + + # Better double-check that it hasn't been deleted yet! + $dbw =& wfGetDB( DB_MASTER ); + $conds = $this->mTitle->pageCond(); + $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); + if ( $latest === false ) { + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + return; + } + + if( $confirm ) { + $this->doDelete( $reason ); + return; + } + + # determine whether this page has earlier revisions + # and insert a warning if it does + $maxRevisions = 20; + $authors = $this->getLastNAuthors( $maxRevisions, $latest ); + + if( count( $authors ) > 1 && !$confirm ) { + $skin=$wgUser->getSkin(); + $wgOut->addHTML( '' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '' ); + } + + # 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( $length < 500 && $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 ); + } + } + } + + return $this->confirmDelete( '', $reason ); + } + + /** + * Get the last N authors + * @param int $num Number of revisions to get + * @param string $revLatest The latest rev_id, selected from the master (optional) + * @return array Array of authors, duplicates not removed + */ + function getLastNAuthors( $num, $revLatest = 0 ) { + wfProfileIn( __METHOD__ ); + + // First try the slave + // If that doesn't have the latest revision, try the master + $continue = 2; + $db =& wfGetDB( DB_SLAVE ); + do { + $res = $db->select( array( 'page', 'revision' ), + array( 'rev_id', 'rev_user_text' ), + array( + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'rev_page = page_id' + ), __METHOD__, $this->getSelectOptions( array( + 'ORDER BY' => 'rev_timestamp DESC', + 'LIMIT' => $num + ) ) + ); + if ( !$res ) { + wfProfileOut( __METHOD__ ); + return array(); + } + $row = $db->fetchObject( $res ); + if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { + $db =& wfGetDB( DB_MASTER ); + $continue--; + } else { + $continue = 0; + } + } while ( $continue ); + + $authors = array( $row->rev_user_text ); + while ( $row = $db->fetchObject( $res ) ) { + $authors[] = $row->rev_user_text; + } + wfProfileOut( __METHOD__ ); + return $authors; + } + + /** + * Output deletion confirmation dialog + */ + function confirmDelete( $par, $reason ) { + global $wgOut, $wgUser; + + wfDebug( "Article::confirmDelete\n" ); + + $sub = htmlspecialchars( $this->mTitle->getPrefixedText() ); + $wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) ); + $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() ); + + $wgOut->addHTML( " +
    + + + + + + + + + +
    + + + +
      + +
    + +
    \n" ); + + $wgOut->returnToMain( false ); + } + + + /** + * Perform a deletion and output success or failure messages + */ + function doDelete( $reason ) { + global $wgOut, $wgUser; + wfDebug( __METHOD__."\n" ); + + if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { + if ( $this->doDeleteArticle( $reason ) ) { + $deleted = $this->mTitle->getPrefixedText(); + + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; + $text = wfMsg( 'deletedtext', $deleted, $loglink ); + + $wgOut->addWikiText( $text ); + $wgOut->returnToMain( false ); + wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason)); + } else { + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + } + } + } + + /** + * Back-end article deletion + * Deletes the article with database consistency, writes logs, purges caches + * Returns success + */ + function doDeleteArticle( $reason ) { + global $wgUseSquid, $wgDeferredUpdateList; + global $wgPostCommitUpdateList, $wgUseTrackbacks; + + wfDebug( __METHOD__."\n" ); + + $dbw =& wfGetDB( DB_MASTER ); + $ns = $this->mTitle->getNamespace(); + $t = $this->mTitle->getDBkey(); + $id = $this->mTitle->getArticleID(); + + if ( $t == '' || $id == 0 ) { + return false; + } + + $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); + array_push( $wgDeferredUpdateList, $u ); + + // For now, shunt the revision data into the archive table. + // Text is *not* removed from the text table; bulk storage + // is left intact to avoid breaking block-compression or + // immutable storage schemes. + // + // For backwards compatibility, note that some older archive + // table entries will have ar_text and ar_flags fields still. + // + // In the future, we may keep revisions and mark them with + // the rev_deleted field, which is reserved for this purpose. + $dbw->insertSelect( 'archive', array( 'page', 'revision' ), + array( + 'ar_namespace' => 'page_namespace', + 'ar_title' => 'page_title', + 'ar_comment' => 'rev_comment', + 'ar_user' => 'rev_user', + 'ar_user_text' => 'rev_user_text', + 'ar_timestamp' => 'rev_timestamp', + 'ar_minor_edit' => 'rev_minor_edit', + 'ar_rev_id' => 'rev_id', + 'ar_text_id' => 'rev_text_id', + ), array( + 'page_id' => $id, + 'page_id = rev_page' + ), __METHOD__ + ); + + # Now that it's safely backed up, delete it + $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); + $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); + + if ($wgUseTrackbacks) + $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); + + # Clean up recentchanges entries... + $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ ); + + # Finally, clean up the link tables + $t = $this->mTitle->getPrefixedDBkey(); + + # Clear caches + Article::onArticleDelete( $this->mTitle ); + + # Delete outgoing links + $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); + $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); + $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); + $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); + $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); + $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); + + # Log the deletion + $log = new LogPage( 'delete' ); + $log->addEntry( 'delete', $this->mTitle, $reason ); + + # Clear the cached article id so the interface doesn't act like we exist + $this->mTitle->resetArticleID( 0 ); + $this->mTitle->mArticleID = 0; + return true; + } + + /** + * Revert a modification + */ + function rollback() { + global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; + + if( $wgUser->isAllowed( 'rollback' ) ) { + if( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); + return; + } + } else { + $wgOut->permissionRequired( 'rollback' ); + return; + } + + if ( wfReadOnly() ) { + $wgOut->readOnlyPage( $this->getContent() ); + return; + } + if( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ), + array( $this->mTitle->getPrefixedText(), + $wgRequest->getVal( 'from' ) ) ) ) { + $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); + $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); + return; + } + $dbw =& wfGetDB( DB_MASTER ); + + # Enhanced rollback, marks edits rc_bot=1 + $bot = $wgRequest->getBool( 'bot' ); + + # Replace all this user's current edits with the next one down + $tt = $this->mTitle->getDBKey(); + $n = $this->mTitle->getNamespace(); + + # Get the last editor + $current = Revision::newFromTitle( $this->mTitle ); + if( is_null( $current ) ) { + # Something wrong... no page? + $wgOut->addHTML( wfMsg( 'notanarticle' ) ); + return; + } + + $from = str_replace( '_', ' ', $wgRequest->getVal( 'from' ) ); + if( $from != $current->getUserText() ) { + $wgOut->setPageTitle( wfMsg('rollbackfailed') ); + $wgOut->addWikiText( wfMsg( 'alreadyrolled', + htmlspecialchars( $this->mTitle->getPrefixedText()), + htmlspecialchars( $from ), + htmlspecialchars( $current->getUserText() ) ) ); + if( $current->getComment() != '') { + $wgOut->addHTML( + wfMsg( 'editcomment', + htmlspecialchars( $current->getComment() ) ) ); + } + return; + } + + # Get the last edit not by this guy + $user = intval( $current->getUser() ); + $user_text = $dbw->addQuotes( $current->getUserText() ); + $s = $dbw->selectRow( 'revision', + array( 'rev_id', 'rev_timestamp' ), + array( + 'rev_page' => $current->getPage(), + "rev_user <> {$user} OR rev_user_text <> {$user_text}" + ), __METHOD__, + array( + 'USE INDEX' => 'page_timestamp', + 'ORDER BY' => 'rev_timestamp DESC' ) + ); + if( $s === false ) { + # Something wrong + $wgOut->setPageTitle(wfMsg('rollbackfailed')); + $wgOut->addHTML( wfMsg( 'cantrollback' ) ); + return; + } + + $set = array(); + if ( $bot ) { + # Mark all reverted edits as bot + $set['rc_bot'] = 1; + } + if ( $wgUseRCPatrol ) { + # Mark all reverted edits as patrolled + $set['rc_patrolled'] = 1; + } + + if ( $set ) { + $dbw->update( 'recentchanges', $set, + array( /* WHERE */ + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $current->getUserText(), + "rc_timestamp > '{$s->rev_timestamp}'", + ), __METHOD__ + ); + } + + # Get the edit summary + $target = Revision::newFromId( $s->rev_id ); + $newComment = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); + $newComment = $wgRequest->getText( 'summary', $newComment ); + + # Save it! + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( '

    ' . htmlspecialchars( $newComment ) . "

    \n
    \n" ); + + $this->updateArticle( $target->getText(), $newComment, 1, $this->mTitle->userIsWatching(), $bot ); + + $wgOut->returnToMain( false ); + } + + + /** + * Do standard deferred updates after page view + * @private + */ + function viewUpdates() { + global $wgDeferredUpdateList; + + if ( 0 != $this->getID() ) { + global $wgDisableCounters; + if( !$wgDisableCounters ) { + Article::incViewCount( $this->getID() ); + $u = new SiteStatsUpdate( 1, 0, 0 ); + array_push( $wgDeferredUpdateList, $u ); + } + } + + # Update newtalk / watchlist notification status + global $wgUser; + $wgUser->clearNotification( $this->mTitle ); + } + + /** + * Do standard deferred updates after page edit. + * Update links tables, site stats, search index and message cache. + * Every 1000th edit, prune the recent changes table. + * + * @private + * @param $text New text of the article + * @param $summary Edit summary + * @param $minoredit Minor edit + * @param $timestamp_of_pagechange Timestamp associated with the page change + * @param $newid rev_id value of the new revision + * @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; + + wfProfileIn( __METHOD__ ); + + # Parse the text + $options = new ParserOptions; + $options->setTidy(true); + $poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid ); + + # Save it to the parser cache + $parserCache =& ParserCache::singleton(); + $parserCache->save( $poutput, $this, $wgUser ); + + # Update the links tables + $u = new LinksUpdate( $this->mTitle, $poutput ); + $u->doUpdate(); + + if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { + wfSeedRandom(); + if ( 0 == mt_rand( 0, 999 ) ) { + # Periodically flush old entries from the recentchanges table. + global $wgRCMaxAge; + + $dbw =& wfGetDB( DB_MASTER ); + $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); + $recentchanges = $dbw->tableName( 'recentchanges' ); + $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'"; + $dbw->query( $sql ); + } + } + + $id = $this->getID(); + $title = $this->mTitle->getPrefixedDBkey(); + $shortTitle = $this->mTitle->getDBkey(); + + if ( 0 == $id ) { + wfProfileOut( __METHOD__ ); + return; + } + + $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment ); + array_push( $wgDeferredUpdateList, $u ); + $u = new SearchUpdate( $id, $title, $text ); + array_push( $wgDeferredUpdateList, $u ); + + # If this is another user's talk page, update newtalk + # Don't do this if $changed = false otherwise some idiot can null-edit a + # load of user talk pages and piss people off + if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getName() && $changed ) { + if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { + $other = User::newFromName( $shortTitle ); + if( is_null( $other ) && User::isIP( $shortTitle ) ) { + // An anonymous user + $other = new User(); + $other->setName( $shortTitle ); + } + if( $other ) { + $other->setNewtalk( true ); + } + } + } + + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgMessageCache->replace( $shortTitle, $text ); + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Generate the navigation links when browsing through an article revisions + * It shows the information as: + * Revision as of \; view current revision + * \<- Previous version | Next Version -\> + * + * @private + * @param string $oldid Revision ID of this article revision + */ + function setOldSubtitle( $oldid=0 ) { + global $wgLang, $wgOut, $wgUser; + + $revision = Revision::newFromId( $oldid ); + + $current = ( $oldid == $this->mLatest ); + $td = $wgLang->timeanddate( $this->mTimestamp, true ); + $sk = $wgUser->getSkin(); + $lnk = $current + ? wfMsg( 'currentrevisionlink' ) + : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; + $prevlink = $prev + ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) + : wfMsg( 'previousrevision' ); + $prevdiff = $prev + ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=prev&oldid='.$oldid ) + : wfMsg( 'diff' ); + $nextlink = $current + ? wfMsg( 'nextrevision' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'nextrevision' ), 'direction=next&oldid='.$oldid ); + $nextdiff = $current + ? wfMsg( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); + + $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) + . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); + + $r = wfMsg( 'old-revision-navigation', $td, $lnk, $prevlink, $nextlink, $userlinks, $prevdiff, $nextdiff ); + $wgOut->setSubtitle( $r ); + } + + /** + * This function is called right before saving the wikitext, + * so we can do things like signatures and links-in-context. + * + * @param string $text + */ + function preSaveTransform( $text ) { + global $wgParser, $wgUser; + return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) ); + } + + /* Caching functions */ + + /** + * checkLastModified returns true if it has taken care of all + * output to the client that is necessary for this request. + * (that is, it has sent a cached version of the page) + */ + function tryFileCache() { + static $called = false; + if( $called ) { + wfDebug( "Article::tryFileCache(): called twice!?\n" ); + return; + } + $called = true; + if($this->isFileCacheable()) { + $touched = $this->mTouched; + $cache = new CacheManager( $this->mTitle ); + if($cache->isFileCacheGood( $touched )) { + wfDebug( "Article::tryFileCache(): about to load file\n" ); + $cache->loadFromFileCache(); + return true; + } else { + wfDebug( "Article::tryFileCache(): starting buffer\n" ); + ob_start( array(&$cache, 'saveToFileCache' ) ); + } + } else { + wfDebug( "Article::tryFileCache(): not cacheable\n" ); + } + } + + /** + * Check if the page can be cached + * @return bool + */ + function isFileCacheable() { + global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest; + extract( $wgRequest->getValues( 'action', 'oldid', 'diff', 'redirect', 'printable' ) ); + + return $wgUseFileCache + and (!$wgShowIPinHeader) + and ($this->getID() != 0) + and ($wgUser->isAnon()) + and (!$wgUser->getNewtalk()) + and ($this->mTitle->getNamespace() != NS_SPECIAL ) + and (empty( $action ) || $action == 'view') + and (!isset($oldid)) + and (!isset($diff)) + and (!isset($redirect)) + and (!isset($printable)) + and (!$this->mRedirectedFrom); + } + + /** + * Loads page_touched and returns a value indicating if it should be used + * + */ + function checkTouched() { + if( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return !$this->mIsRedirect; + } + + /** + * Get the page_touched field + */ + function getTouched() { + # Ensure that page data has been loaded + if( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mTouched; + } + + /** + * Get the page_latest field + */ + function getLatest() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mLatest; + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param string $text text submitted + * @param string $comment comment submitted + * @param bool $minor whereas it's a minor modification + */ + function quickEdit( $text, $comment = '', $minor = 0 ) { + wfProfileIn( __METHOD__ ); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->begin(); + $revision = new Revision( array( + 'page' => $this->getId(), + 'text' => $text, + 'comment' => $comment, + 'minor_edit' => $minor ? 1 : 0, + ) ); + # fixme : $revisionId never used + $revisionId = $revision->insertOn( $dbw ); + $this->updateRevisionOn( $dbw, $revision ); + $dbw->commit(); + + wfProfileOut( __METHOD__ ); + } + + /** + * Used to increment the view counter + * + * @static + * @param integer $id article id + */ + function incViewCount( $id ) { + $id = intval( $id ); + global $wgHitcounterUpdateFreq, $wgDBtype; + + $dbw =& wfGetDB( DB_MASTER ); + $pageTable = $dbw->tableName( 'page' ); + $hitcounterTable = $dbw->tableName( 'hitcounter' ); + $acchitsTable = $dbw->tableName( 'acchits' ); + + if( $wgHitcounterUpdateFreq <= 1 ){ // + $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" ); + return; + } + + # Not important enough to warrant an error page in case of failure + $oldignore = $dbw->ignoreErrors( true ); + + $dbw->query( "INSERT INTO $hitcounterTable (hc_id) VALUES ({$id})" ); + + $checkfreq = intval( $wgHitcounterUpdateFreq/25 + 1 ); + if( (rand() % $checkfreq != 0) or ($dbw->lastErrno() != 0) ){ + # Most of the time (or on SQL errors), skip row count check + $dbw->ignoreErrors( $oldignore ); + return; + } + + $res = $dbw->query("SELECT COUNT(*) as n FROM $hitcounterTable"); + $row = $dbw->fetchObject( $res ); + $rown = intval( $row->n ); + if( $rown >= $wgHitcounterUpdateFreq ){ + wfProfileIn( 'Article::incViewCount-collect' ); + $old_user_abort = ignore_user_abort( true ); + + if ($wgDBtype == 'mysql') + $dbw->query("LOCK TABLES $hitcounterTable WRITE"); + $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : ''; + $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype". + "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ". + 'GROUP BY hc_id'); + $dbw->query("DELETE FROM $hitcounterTable"); + if ($wgDBtype == 'mysql') + $dbw->query('UNLOCK TABLES'); + $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". + 'WHERE page_id = hc_id'); + $dbw->query("DROP TABLE $acchitsTable"); + + ignore_user_abort( $old_user_abort ); + wfProfileOut( 'Article::incViewCount-collect' ); + } + $dbw->ignoreErrors( $oldignore ); + } + + /**#@+ + * The onArticle*() functions are supposed to be a kind of hooks + * which should be called whenever any of the specified actions + * are done. + * + * This is a good place to put code to clear caches, for instance. + * + * This is called on page move and undelete, as well as edit + * @static + * @param $title_obj a title object + */ + + static function onArticleCreate($title) { + # The talk page isn't in the regular link tables, so we need to update manually: + if ( $title->isTalkPage() ) { + $other = $title->getSubjectPage(); + } else { + $other = $title->getTalkPage(); + } + $other->invalidateCache(); + $other->purgeSquid(); + + $title->touchLinks(); + $title->purgeSquid(); + } + + static function onArticleDelete( $title ) { + global $wgUseFileCache, $wgMessageCache; + + $title->touchLinks(); + $title->purgeSquid(); + + # File cache + if ( $wgUseFileCache ) { + $cm = new CacheManager( $title ); + @unlink( $cm->fileCacheName() ); + } + + if( $title->getNamespace() == NS_MEDIAWIKI) { + $wgMessageCache->replace( $title->getDBkey(), false ); + } + } + + /** + * Purge caches on page update etc + */ + static function onArticleEdit( $title ) { + global $wgDeferredUpdateList, $wgUseFileCache; + + $urls = array(); + + // Invalidate caches of articles which include this page + $update = new HTMLCacheUpdate( $title, 'templatelinks' ); + $wgDeferredUpdateList[] = $update; + + # Purge squid for this page only + $title->purgeSquid(); + + # Clear file cache + if ( $wgUseFileCache ) { + $cm = new CacheManager( $title ); + @unlink( $cm->fileCacheName() ); + } + } + + /**#@-*/ + + /** + * Info about this page + * Called for ?action=info when $wgAllowPageInfo is on. + * + * @public + */ + function info() { + global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser; + + if ( !$wgAllowPageInfo ) { + $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); + return; + } + + $page = $this->mTitle->getSubjectPage(); + + $wgOut->setPagetitle( $page->getPrefixedText() ); + $wgOut->setSubtitle( wfMsg( 'infosubtitle' )); + + # first, see if the page exists at all. + $exists = $page->getArticleId() != 0; + if( !$exists ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgOut->addHTML(wfMsgWeirdKey ( $this->mTitle->getText() ) ); + } else { + $wgOut->addHTML(wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ) ); + } + } else { + $dbr =& wfGetDB( DB_SLAVE ); + $wl_clause = array( + 'wl_title' => $page->getDBkey(), + 'wl_namespace' => $page->getNamespace() ); + $numwatchers = $dbr->selectField( + 'watchlist', + 'COUNT(*)', + $wl_clause, + __METHOD__, + $this->getSelectOptions() ); + + $pageInfo = $this->pageCountInfo( $page ); + $talkInfo = $this->pageCountInfo( $page->getTalkPage() ); + + $wgOut->addHTML( "' ); + + } + } + + /** + * Return the total number of edits and number of unique editors + * on a given page. If page does not exist, returns false. + * + * @param Title $title + * @return array + * @private + */ + function pageCountInfo( $title ) { + $id = $title->getArticleId(); + if( $id == 0 ) { + return false; + } + + $dbr =& wfGetDB( DB_SLAVE ); + + $rev_clause = array( 'rev_page' => $id ); + + $edits = $dbr->selectField( + 'revision', + 'COUNT(rev_page)', + $rev_clause, + __METHOD__, + $this->getSelectOptions() ); + + $authors = $dbr->selectField( + 'revision', + 'COUNT(DISTINCT rev_user_text)', + $rev_clause, + __METHOD__, + $this->getSelectOptions() ); + + return array( 'edits' => $edits, 'authors' => $authors ); + } + + /** + * Return a list of templates used by this article. + * Uses the templatelinks table + * + * @return array Array of Title objects + */ + function getUsedTemplates() { + $result = array(); + $id = $this->mTitle->getArticleID(); + if( $id == 0 ) { + return array(); + } + + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + 'Article:getUsedTemplates' ); + if ( false !== $res ) { + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title ); + } + } + } + $dbr->freeResult( $res ); + return $result; + } +} + +?> diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php new file mode 100644 index 00000000..1d955418 --- /dev/null +++ b/includes/AuthPlugin.php @@ -0,0 +1,232 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html + +/** + * Authentication plugin interface. Instantiate a subclass of AuthPlugin + * and set $wgAuth to it to authenticate against some external tool. + * + * The default behavior is not to do anything, and use the local user + * database for all authentication. A subclass can require that all + * 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... + * + * @package MediaWiki + */ +class AuthPlugin { + /** + * Check whether there exists a user account with the given name. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param $username String: username. + * @return bool + * @public + */ + function userExists( $username ) { + # Override this! + return false; + } + + /** + * Check if a username+password pair is a valid login. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param $username String: username. + * @param $password String: user password. + * @return bool + * @public + */ + function authenticate( $username, $password ) { + # Override this! + return false; + } + + /** + * Modify options in the login template. + * + * @param $template UserLoginTemplate object. + * @public + */ + function modifyUITemplate( &$template ) { + # Override this! + $template->set( 'usedomain', false ); + } + + /** + * Set the domain this plugin is supposed to use when authenticating. + * + * @param $domain String: authentication domain. + * @public + */ + function setDomain( $domain ) { + $this->domain = $domain; + } + + /** + * Check to see if the specific domain is a valid domain. + * + * @param $domain String: authentication domain. + * @return bool + * @public + */ + function validDomain( $domain ) { + # Override this! + return true; + } + + /** + * When a user logs in, optionally fill in preferences and such. + * For instance, you might pull the email address or real name from the + * external user database. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @param User $user + * @public + */ + function updateUser( &$user ) { + # Override this and do something + return true; + } + + + /** + * Return true if the wiki should create a new local account automatically + * when asked to login a user who doesn't exist locally but does in the + * external auth database. + * + * If you don't automatically create accounts, you must still create + * accounts in some way. It's not possible to authenticate without + * a local account. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + * @public + */ + function autoCreate() { + return false; + } + + /** + * Can users change their passwords? + * + * @return bool + */ + function allowPasswordChange() { + return true; + } + + /** + * Set the given password in the authentication database. + * Return true if successful. + * + * @param $password String: password. + * @return bool + * @public + */ + function setPassword( $password ) { + return true; + } + + /** + * Update user information in the external authentication database. + * Return true if successful. + * + * @param $user User object. + * @return bool + * @public + */ + function updateExternalDB( $user ) { + return true; + } + + /** + * Check to see if external accounts can be created. + * Return true if external accounts can be created. + * @return bool + * @public + */ + function canCreateAccounts() { + return false; + } + + /** + * Add a user to the external authentication database. + * Return true if successful. + * + * @param User $user + * @param string $password + * @return bool + * @public + */ + function addUser( $user, $password ) { + return true; + } + + + /** + * Return true to prevent logins that don't authenticate here from being + * checked against the local database's password fields. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + * @public + */ + function strict() { + 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. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @param $user User object. + * @public + */ + function initUser( &$user ) { + # Override this to do something. + } + + /** + * If you want to munge the case of an account name before the final + * check, now is your chance. + */ + function getCanonicalName( $username ) { + return $username; + } +} + +?> diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php new file mode 100644 index 00000000..7d09d5b6 --- /dev/null +++ b/includes/AutoLoader.php @@ -0,0 +1,272 @@ + 'includes/AjaxDispatcher.php', + 'AjaxCachePolicy' => 'includes/AjaxFunctions.php', + 'Article' => 'includes/Article.php', + 'AuthPlugin' => 'includes/AuthPlugin.php', + 'BagOStuff' => 'includes/BagOStuff.php', + 'HashBagOStuff' => 'includes/BagOStuff.php', + 'SqlBagOStuff' => 'includes/BagOStuff.php', + 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', + 'TurckBagOStuff' => 'includes/BagOStuff.php', + 'APCBagOStuff' => 'includes/BagOStuff.php', + 'eAccelBagOStuff' => 'includes/BagOStuff.php', + 'Block' => 'includes/Block.php', + 'CacheManager' => 'includes/CacheManager.php', + 'CategoryPage' => 'includes/CategoryPage.php', + 'Categoryfinder' => 'includes/Categoryfinder.php', + 'RCCacheEntry' => 'includes/ChangesList.php', + 'ChangesList' => 'includes/ChangesList.php', + 'OldChangesList' => 'includes/ChangesList.php', + 'EnhancedChangesList' => 'includes/ChangesList.php', + 'CoreParserFunctions' => 'includes/CoreParserFunctions.php', + 'DBObject' => 'includes/Database.php', + 'Database' => 'includes/Database.php', + 'DatabaseMysql' => 'includes/Database.php', + 'ResultWrapper' => 'includes/Database.php', + 'OracleBlob' => 'includes/DatabaseOracle.php', + 'DatabaseOracle' => 'includes/DatabaseOracle.php', + 'DatabasePostgres' => 'includes/DatabasePostgres.php', + 'DateFormatter' => 'includes/DateFormatter.php', + 'DifferenceEngine' => 'includes/DifferenceEngine.php', + '_DiffOp' => 'includes/DifferenceEngine.php', + '_DiffOp_Copy' => 'includes/DifferenceEngine.php', + '_DiffOp_Delete' => 'includes/DifferenceEngine.php', + '_DiffOp_Add' => 'includes/DifferenceEngine.php', + '_DiffOp_Change' => 'includes/DifferenceEngine.php', + '_DiffEngine' => 'includes/DifferenceEngine.php', + 'Diff' => 'includes/DifferenceEngine.php', + 'MappedDiff' => 'includes/DifferenceEngine.php', + 'DiffFormatter' => 'includes/DifferenceEngine.php', + 'DjVuImage' => 'includes/DjVuImage.php', + '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php', + 'WordLevelDiff' => 'includes/DifferenceEngine.php', + 'TableDiffFormatter' => 'includes/DifferenceEngine.php', + 'EditPage' => 'includes/EditPage.php', + 'MWException' => 'includes/Exception.php', + 'Exif' => 'includes/Exif.php', + 'FormatExif' => 'includes/Exif.php', + 'WikiExporter' => 'includes/Export.php', + 'XmlDumpWriter' => 'includes/Export.php', + 'DumpOutput' => 'includes/Export.php', + 'DumpFileOutput' => 'includes/Export.php', + 'DumpPipeOutput' => 'includes/Export.php', + 'DumpGZipOutput' => 'includes/Export.php', + 'DumpBZip2Output' => 'includes/Export.php', + 'Dump7ZipOutput' => 'includes/Export.php', + 'DumpFilter' => 'includes/Export.php', + 'DumpNotalkFilter' => 'includes/Export.php', + 'DumpNamespaceFilter' => 'includes/Export.php', + 'DumpLatestFilter' => 'includes/Export.php', + 'DumpMultiWriter' => 'includes/Export.php', + 'ExternalEdit' => 'includes/ExternalEdit.php', + 'ExternalStore' => 'includes/ExternalStore.php', + 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', + 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', + 'FakeTitle' => 'includes/FakeTitle.php', + 'FeedItem' => 'includes/Feed.php', + 'ChannelFeed' => 'includes/Feed.php', + 'RSSFeed' => 'includes/Feed.php', + 'AtomFeed' => 'includes/Feed.php', + 'FileStore' => 'includes/FileStore.php', + 'FSException' => 'includes/FileStore.php', + 'FSTransaction' => 'includes/FileStore.php', + 'ReplacerCallback' => 'includes/GlobalFunctions.php', + 'HTMLForm' => 'includes/HTMLForm.php', + 'HistoryBlob' => 'includes/HistoryBlob.php', + 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', + 'HistoryBlobStub' => 'includes/HistoryBlob.php', + 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', + 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', + 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', + 'Http' => 'includes/HttpFunctions.php', + 'Image' => 'includes/Image.php', + 'ThumbnailImage' => 'includes/Image.php', + 'ImageGallery' => 'includes/ImageGallery.php', + 'ImagePage' => 'includes/ImagePage.php', + 'ImageHistoryList' => 'includes/ImagePage.php', + 'ImageRemote' => 'includes/ImageRemote.php', + 'Job' => 'includes/JobQueue.php', + 'Licenses' => 'includes/Licenses.php', + 'License' => 'includes/Licenses.php', + 'LinkBatch' => 'includes/LinkBatch.php', + 'LinkCache' => 'includes/LinkCache.php', + 'LinkFilter' => 'includes/LinkFilter.php', + 'Linker' => 'includes/Linker.php', + 'LinksUpdate' => 'includes/LinksUpdate.php', + 'LoadBalancer' => 'includes/LoadBalancer.php', + 'LogPage' => 'includes/LogPage.php', + 'MacBinary' => 'includes/MacBinary.php', + 'MagicWord' => 'includes/MagicWord.php', + 'MathRenderer' => 'includes/Math.php', + 'MessageCache' => 'includes/MessageCache.php', + 'MimeMagic' => 'includes/MimeMagic.php', + 'Namespace' => 'includes/Namespace.php', + 'FakeMemCachedClient' => 'includes/ObjectCache.php', + 'OutputPage' => 'includes/OutputPage.php', + 'PageHistory' => 'includes/PageHistory.php', + 'Parser' => 'includes/Parser.php', + 'ParserOutput' => 'includes/Parser.php', + 'ParserOptions' => 'includes/Parser.php', + 'ParserCache' => 'includes/ParserCache.php', + 'element' => 'includes/ParserXML.php', + 'xml2php' => 'includes/ParserXML.php', + 'ParserXML' => 'includes/ParserXML.php', + 'ProfilerSimple' => 'includes/ProfilerSimple.php', + 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', + 'Profiler' => 'includes/Profiling.php', + 'ProxyTools' => 'includes/ProxyTools.php', + 'ProtectionForm' => 'includes/ProtectionForm.php', + 'QueryPage' => 'includes/QueryPage.php', + 'PageQueryPage' => 'includes/QueryPage.php', + 'RawPage' => 'includes/RawPage.php', + 'RecentChange' => 'includes/RecentChange.php', + 'Revision' => 'includes/Revision.php', + 'Sanitizer' => 'includes/Sanitizer.php', + 'SearchEngine' => 'includes/SearchEngine.php', + 'SearchResultSet' => 'includes/SearchEngine.php', + 'SearchResult' => 'includes/SearchEngine.php', + 'SearchEngineDummy' => 'includes/SearchEngine.php', + 'SearchMySQL' => 'includes/SearchMySQL.php', + 'MySQLSearchResultSet' => 'includes/SearchMySQL.php', + 'SearchMySQL4' => 'includes/SearchMySQL4.php', + 'SearchPostgres' => 'includes/SearchPostgres.php', + 'SearchUpdate' => 'includes/SearchUpdate.php', + 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', + 'SiteConfiguration' => 'includes/SiteConfiguration.php', + 'SiteStatsUpdate' => 'includes/SiteStatsUpdate.php', + 'Skin' => 'includes/Skin.php', + 'MediaWiki_I18N' => 'includes/SkinTemplate.php', + 'SkinTemplate' => 'includes/SkinTemplate.php', + 'QuickTemplate' => 'includes/SkinTemplate.php', + 'SpecialAllpages' => 'includes/SpecialAllpages.php', + 'AncientPagesPage' => 'includes/SpecialAncientpages.php', + 'IPBlockForm' => 'includes/SpecialBlockip.php', + 'BookSourceList' => 'includes/SpecialBooksources.php', + 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php', + 'CategoriesPage' => 'includes/SpecialCategories.php', + 'EmailConfirmation' => 'includes/SpecialConfirmemail.php', + 'ContribsFinder' => 'includes/SpecialContributions.php', + 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php', + 'DisambiguationsPage' => 'includes/SpecialDisambiguations.php', + 'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php', + 'EmailUserForm' => 'includes/SpecialEmailuser.php', + 'WikiRevision' => 'includes/SpecialImport.php', + 'WikiImporter' => 'includes/SpecialImport.php', + 'ImportStringSource' => 'includes/SpecialImport.php', + 'ImportStreamSource' => 'includes/SpecialImport.php', + 'IPUnblockForm' => 'includes/SpecialIpblocklist.php', + 'ListredirectsPage' => 'includes/SpecialListredirects.php', + 'ListUsersPage' => 'includes/SpecialListusers.php', + 'DBLockForm' => 'includes/SpecialLockdb.php', + 'LogReader' => 'includes/SpecialLog.php', + 'LogViewer' => 'includes/SpecialLog.php', + 'LonelyPagesPage' => 'includes/SpecialLonelypages.php', + 'LongPagesPage' => 'includes/SpecialLongpages.php', + 'MIMEsearchPage' => 'includes/SpecialMIMEsearch.php', + 'MostcategoriesPage' => 'includes/SpecialMostcategories.php', + 'MostimagesPage' => 'includes/SpecialMostimages.php', + 'MostlinkedPage' => 'includes/SpecialMostlinked.php', + 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php', + 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php', + 'MovePageForm' => 'includes/SpecialMovepage.php', + 'NewPagesPage' => 'includes/SpecialNewpages.php', + 'SpecialPage' => 'includes/SpecialPage.php', + 'UnlistedSpecialPage' => 'includes/SpecialPage.php', + 'IncludableSpecialPage' => 'includes/SpecialPage.php', + 'PopularPagesPage' => 'includes/SpecialPopularpages.php', + 'PreferencesForm' => 'includes/SpecialPreferences.php', + 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php', + 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php', + 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php', + 'SpecialSearch' => 'includes/SpecialSearch.php', + 'ShortPagesPage' => 'includes/SpecialShortpages.php', + 'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php', + 'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php', + 'PageArchive' => 'includes/SpecialUndelete.php', + 'UndeleteForm' => 'includes/SpecialUndelete.php', + 'DBUnlockForm' => 'includes/SpecialUnlockdb.php', + 'UnusedCategoriesPage' => 'includes/SpecialUnusedcategories.php', + 'UnusedimagesPage' => 'includes/SpecialUnusedimages.php', + 'UnusedtemplatesPage' => 'includes/SpecialUnusedtemplates.php', + 'UnwatchedpagesPage' => 'includes/SpecialUnwatchedpages.php', + 'UploadForm' => 'includes/SpecialUpload.php', + 'UploadFormMogile' => 'includes/SpecialUploadMogile.php', + 'LoginForm' => 'includes/SpecialUserlogin.php', + 'UserrightsForm' => 'includes/SpecialUserrights.php', + 'SpecialVersion' => 'includes/SpecialVersion.php', + 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php', + 'WantedPagesPage' => 'includes/SpecialWantedpages.php', + 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php', + 'SquidUpdate' => 'includes/SquidUpdate.php', + 'Title' => 'includes/Title.php', + 'User' => 'includes/User.php', + 'MailAddress' => 'includes/UserMailer.php', + 'EmailNotification' => 'includes/UserMailer.php', + 'WatchedItem' => 'includes/WatchedItem.php', + 'WebRequest' => 'includes/WebRequest.php', + 'FauxRequest' => 'includes/WebRequest.php', + 'MediaWiki' => 'includes/Wiki.php', + 'WikiError' => 'includes/WikiError.php', + 'WikiErrorMsg' => 'includes/WikiError.php', + 'WikiXmlError' => 'includes/WikiError.php', + 'Xml' => 'includes/Xml.php', + 'ZhClient' => 'includes/ZhClient.php', + 'memcached' => 'includes/memcached-client.php', + 'UtfNormal' => 'includes/normal/UtfNormal.php' + ); + if ( isset( $localClasses[$className] ) ) { + $filename = $localClasses[$className]; + } elseif ( isset( $wgAutoloadClasses[$className] ) ) { + $filename = $wgAutoloadClasses[$className]; + } else { + # Try a different capitalisation + # The case can sometimes be wrong when unserializing PHP 4 objects + $filename = false; + $lowerClass = strtolower( $className ); + foreach ( $localClasses as $class2 => $file2 ) { + if ( strtolower( $class2 ) == $lowerClass ) { + $filename = $file2; + } + } + if ( !$filename ) { + # Give up + return; + } + } + + # Make an absolute path, this improves performance by avoiding some stat calls + if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) { + global $IP; + $filename = "$IP/$filename"; + } + require( $filename ); +} + +function wfLoadAllExtensions() { + global $wgAutoloadClasses; + + # It is crucial that SpecialPage.php is included before any special page + # extensions are loaded. Otherwise the parent class will not be available + # when APC loads the early-bound extension class. Normally this is + # guaranteed by entering special pages via SpecialPage members such as + # executePath(), but here we have to take a more explicit measure. + + require_once( 'SpecialPage.php' ); + + foreach( $wgAutoloadClasses as $class => $file ) { + if ( ! class_exists( $class ) ) { + require( $file ); + } + } +} + +?> diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php new file mode 100644 index 00000000..182756ab --- /dev/null +++ b/includes/BagOStuff.php @@ -0,0 +1,538 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html +/** + * + * @package MediaWiki + */ + +/** + * Simple generic object store + * + * interface is intended to be more or less compatible with + * the PHP memcached client. + * + * backends for local hash array and SQL table included: + * $bag = new HashBagOStuff(); + * $bag = new MysqlBagOStuff($tablename); # connect to db first + * + * @package MediaWiki + */ +class BagOStuff { + var $debugmode; + + function BagOStuff() { + $this->set_debug( false ); + } + + function set_debug($bool) { + $this->debugmode = $bool; + } + + /* *** THE GUTS OF THE OPERATION *** */ + /* Override these with functional things in subclasses */ + + function get($key) { + /* stub */ + return false; + } + + function set($key, $value, $exptime=0) { + /* stub */ + return false; + } + + function delete($key, $time=0) { + /* stub */ + return false; + } + + function lock($key, $timeout = 0) { + /* stub */ + return true; + } + + function unlock($key) { + /* stub */ + return true; + } + + /* *** Emulated functions *** */ + /* Better performance can likely be got with custom written versions */ + function get_multi($keys) { + $out = array(); + foreach($keys as $key) + $out[$key] = $this->get($key); + return $out; + } + + function set_multi($hash, $exptime=0) { + foreach($hash as $key => $value) + $this->set($key, $value, $exptime); + } + + function add($key, $value, $exptime=0) { + if( $this->get($key) == false ) { + $this->set($key, $value, $exptime); + return true; + } + } + + function add_multi($hash, $exptime=0) { + foreach($hash as $key => $value) + $this->add($key, $value, $exptime); + } + + function delete_multi($keys, $time=0) { + foreach($keys as $key) + $this->delete($key, $time); + } + + function replace($key, $value, $exptime=0) { + if( $this->get($key) !== false ) + $this->set($key, $value, $exptime); + } + + function incr($key, $value=1) { + if ( !$this->lock($key) ) { + return false; + } + $value = intval($value); + if($value < 0) $value = 0; + + $n = false; + if( ($n = $this->get($key)) !== false ) { + $n += $value; + $this->set($key, $n); // exptime? + } + $this->unlock($key); + return $n; + } + + function decr($key, $value=1) { + if ( !$this->lock($key) ) { + return false; + } + $value = intval($value); + if($value < 0) $value = 0; + + $m = false; + if( ($n = $this->get($key)) !== false ) { + $m = $n - $value; + if($m < 0) $m = 0; + $this->set($key, $m); // exptime? + } + $this->unlock($key); + return $m; + } + + function _debug($text) { + if($this->debugmode) + wfDebug("BagOStuff debug: $text\n"); + } +} + + +/** + * Functional versions! + * @todo document + * @package MediaWiki + */ +class HashBagOStuff extends BagOStuff { + /* + This is a test of the interface, mainly. It stores + things in an associative array, which is not going to + persist between program runs. + */ + var $bag; + + function HashBagOStuff() { + $this->bag = array(); + } + + function _expire($key) { + $et = $this->bag[$key][1]; + if(($et == 0) || ($et > time())) + return false; + $this->delete($key); + return true; + } + + function get($key) { + if(!$this->bag[$key]) + return false; + if($this->_expire($key)) + return false; + return $this->bag[$key][0]; + } + + function set($key,$value,$exptime=0) { + if(($exptime != 0) && ($exptime < 3600*24*30)) + $exptime = time() + $exptime; + $this->bag[$key] = array( $value, $exptime ); + } + + function delete($key,$time=0) { + if(!$this->bag[$key]) + return false; + unset($this->bag[$key]); + return true; + } +} + +/* +CREATE TABLE objectcache ( + keyname char(255) binary not null default '', + value mediumblob, + exptime datetime, + unique key (keyname), + key (exptime) +); +*/ + +/** + * @todo document + * @abstract + * @package MediaWiki + */ +abstract class SqlBagOStuff extends BagOStuff { + var $table; + var $lastexpireall = 0; + + function SqlBagOStuff($tablename = 'objectcache') { + $this->table = $tablename; + } + + function get($key) { + /* expire old entries if any */ + $this->garbageCollect(); + + $res = $this->_query( + "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key); + if(!$res) { + $this->_debug("get: ** error: " . $this->_dberror($res) . " **"); + return false; + } + if($row=$this->_fetchobject($res)) { + $this->_debug("get: retrieved data; exp time is " . $row->exptime); + return $this->_unserialize($this->_blobdecode($row->value)); + } else { + $this->_debug('get: no matching rows'); + } + return false; + } + + function set($key,$value,$exptime=0) { + $exptime = intval($exptime); + if($exptime < 0) $exptime = 0; + if($exptime == 0) { + $exp = $this->_maxdatetime(); + } else { + if($exptime < 3600*24*30) + $exptime += time(); + $exp = $this->_fromunixtime($exptime); + } + $this->delete( $key ); + $this->_doinsert($this->getTableName(), array( + 'keyname' => $key, + 'value' => $this->_blobencode($this->_serialize($value)), + 'exptime' => $exp + )); + return true; /* ? */ + } + + function delete($key,$time=0) { + $this->_query( + "DELETE FROM $0 WHERE keyname='$1'", $key ); + return true; /* ? */ + } + + function getTableName() { + return $this->table; + } + + function _query($sql) { + $reps = func_get_args(); + $reps[0] = $this->getTableName(); + // ewwww + for($i=0;$i 0 ? $this->_strencode($reps[$i]) : $reps[$i], + $sql); + } + $res = $this->_doquery($sql); + if($res == false) { + $this->_debug('query failed: ' . $this->_dberror($res)); + } + return $res; + } + + function _strencode($str) { + /* Protect strings in SQL */ + return str_replace( "'", "''", $str ); + } + function _blobencode($str) { + return $str; + } + function _blobdecode($str) { + return $str; + } + + abstract function _doinsert($table, $vals); + abstract function _doquery($sql); + + function _freeresult($result) { + /* stub */ + return false; + } + + function _dberror($result) { + /* stub */ + return 'unknown error'; + } + + abstract function _maxdatetime(); + abstract function _fromunixtime($ts); + + function garbageCollect() { + /* Ignore 99% of requests */ + if ( !mt_rand( 0, 100 ) ) { + $nowtime = time(); + /* Avoid repeating the delete within a few seconds */ + if ( $nowtime > ($this->lastexpireall + 1) ) { + $this->lastexpireall = $nowtime; + $this->expireall(); + } + } + } + + function expireall() { + /* Remove any items that have expired */ + $now = $this->_fromunixtime( time() ); + $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" ); + } + + function deleteall() { + /* Clear *all* items from cache table */ + $this->_query( "DELETE FROM $0" ); + } + + /** + * Serialize an object and, if possible, compress the representation. + * On typical message and page data, this can provide a 3X decrease + * in storage requirements. + * + * @param mixed $data + * @return string + */ + function _serialize( &$data ) { + $serial = serialize( $data ); + if( function_exists( 'gzdeflate' ) ) { + return gzdeflate( $serial ); + } else { + return $serial; + } + } + + /** + * Unserialize and, if necessary, decompress an object. + * @param string $serial + * @return mixed + */ + function _unserialize( $serial ) { + if( function_exists( 'gzinflate' ) ) { + $decomp = @gzinflate( $serial ); + if( false !== $decomp ) { + $serial = $decomp; + } + } + $ret = unserialize( $serial ); + return $ret; + } +} + +/** + * @todo document + * @package MediaWiki + */ +class MediaWikiBagOStuff extends SqlBagOStuff { + var $tableInitialised = false; + + function _doquery($sql) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery'); + } + function _doinsert($t, $v) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert'); + } + function _fetchobject($result) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->fetchObject($result); + } + function _freeresult($result) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->freeResult($result); + } + function _dberror($result) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->lastError(); + } + function _maxdatetime() { + $dbw =& wfGetDB(DB_MASTER); + return $dbw->timestamp('9999-12-31 12:59:59'); + } + function _fromunixtime($ts) { + $dbw =& wfGetDB(DB_MASTER); + return $dbw->timestamp($ts); + } + function _strencode($s) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->strencode($s); + } + function _blobencode($s) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->encodeBlob($s); + } + function _blobdecode($s) { + $dbw =& wfGetDB( DB_MASTER ); + return $dbw->decodeBlob($s); + } + function getTableName() { + if ( !$this->tableInitialised ) { + $dbw =& wfGetDB( DB_MASTER ); + /* This is actually a hack, we should be able + to use Language classes here... or not */ + if (!$dbw) + throw new MWException("Could not connect to database"); + $this->table = $dbw->tableName( $this->table ); + $this->tableInitialised = true; + } + return $this->table; + } +} + +/** + * This is a wrapper for Turck MMCache's shared memory functions. + * + * You can store objects with mmcache_put() and mmcache_get(), but Turck seems + * to use a weird custom serializer that randomly segfaults. So we wrap calls + * with serialize()/unserialize(). + * + * The thing I noticed about the Turck serialized data was that unlike ordinary + * serialize(), it contained the names of methods, and judging by the amount of + * binary data, perhaps even the bytecode of the methods themselves. It may be + * that Turck's serializer is faster, so a possible future extension would be + * to use it for arrays but not for objects. + * + * @package MediaWiki + */ +class TurckBagOStuff extends BagOStuff { + function get($key) { + $val = mmcache_get( $key ); + if ( is_string( $val ) ) { + $val = unserialize( $val ); + } + return $val; + } + + function set($key, $value, $exptime=0) { + mmcache_put( $key, serialize( $value ), $exptime ); + return true; + } + + function delete($key, $time=0) { + mmcache_rm( $key ); + return true; + } + + function lock($key, $waitTimeout = 0 ) { + mmcache_lock( $key ); + return true; + } + + function unlock($key) { + mmcache_unlock( $key ); + return true; + } +} + +/** + * This is a wrapper for APC's shared memory functions + * + * @package MediaWiki + */ + +class APCBagOStuff extends BagOStuff { + function get($key) { + $val = apc_fetch($key); + return $val; + } + + function set($key, $value, $exptime=0) { + apc_store($key, $value, $exptime); + return true; + } + + function delete($key) { + apc_delete($key); + return true; + } +} + + +/** + * This is a wrapper for eAccelerator's shared memory functions. + * + * This is basically identical to the Turck MMCache version, + * mostly because eAccelerator is based on Turck MMCache. + * + * @package MediaWiki + */ +class eAccelBagOStuff extends BagOStuff { + function get($key) { + $val = eaccelerator_get( $key ); + if ( is_string( $val ) ) { + $val = unserialize( $val ); + } + return $val; + } + + function set($key, $value, $exptime=0) { + eaccelerator_put( $key, serialize( $value ), $exptime ); + return true; + } + + function delete($key, $time=0) { + eaccelerator_rm( $key ); + return true; + } + + function lock($key, $waitTimeout = 0 ) { + eaccelerator_lock( $key ); + return true; + } + + function unlock($key) { + eaccelerator_unlock( $key ); + return true; + } +} +?> diff --git a/includes/Block.php b/includes/Block.php new file mode 100644 index 00000000..26fa444d --- /dev/null +++ b/includes/Block.php @@ -0,0 +1,440 @@ +mAddress = $address; + $this->mUser = $user; + $this->mBy = $by; + $this->mReason = $reason; + $this->mTimestamp = wfTimestamp(TS_MW,$timestamp); + $this->mAuto = $auto; + if( empty( $expiry ) ) { + $this->mExpiry = $expiry; + } else { + $this->mExpiry = wfTimestamp( TS_MW, $expiry ); + } + + $this->mForUpdate = false; + $this->mFromMaster = false; + $this->mByName = false; + $this->initialiseRange(); + } + + /*static*/ function newFromDB( $address, $user = 0, $killExpired = true ) + { + $ban = new Block(); + $ban->load( $address, $user, $killExpired ); + return $ban; + } + + function clear() + { + $this->mAddress = $this->mReason = $this->mTimestamp = ''; + $this->mUser = $this->mBy = 0; + $this->mByName = false; + + } + + /** + * Get the DB object and set the reference parameter to the query options + */ + function &getDBOptions( &$options ) + { + global $wgAntiLockFlags; + if ( $this->mForUpdate || $this->mFromMaster ) { + $db =& wfGetDB( DB_MASTER ); + if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { + $options = ''; + } else { + $options = 'FOR UPDATE'; + } + } else { + $db =& wfGetDB( DB_SLAVE ); + $options = ''; + } + return $db; + } + + /** + * Get a ban from the DB, with either the given address or the given username + */ + function load( $address = '', $user = 0, $killExpired = true ) + { + $fname = 'Block::load'; + wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); + + $options = ''; + $db =& $this->getDBOptions( $options ); + + $ret = false; + $killed = false; + $ipblocks = $db->tableName( 'ipblocks' ); + + if ( 0 == $user && $address == '' ) { + # Invalid user specification, not blocked + $this->clear(); + return false; + } elseif ( $address == '' ) { + $sql = "SELECT * FROM $ipblocks WHERE ipb_user={$user} $options"; + } elseif ( $user == '' ) { + $sql = "SELECT * FROM $ipblocks WHERE ipb_address=" . $db->addQuotes( $address ) . " $options"; + } elseif ( $options == '' ) { + # If there are no options (e.g. FOR UPDATE), use a UNION + # so that the query can make efficient use of indices + $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . + "' UNION SELECT * FROM $ipblocks WHERE ipb_user={$user}"; + } else { + # If there are options, a UNION can not be used, use one + # SELECT instead. Will do a full table scan. + $sql = "SELECT * FROM $ipblocks WHERE (ipb_address='" . $db->strencode( $address ) . + "' OR ipb_user={$user}) $options"; + } + + $res = $db->query( $sql, $fname ); + if ( 0 != $db->numRows( $res ) ) { + # Get first block + $row = $db->fetchObject( $res ); + $this->initFromRow( $row ); + + if ( $killExpired ) { + # If requested, delete expired rows + do { + $killed = $this->deleteIfExpired(); + if ( $killed ) { + $row = $db->fetchObject( $res ); + if ( $row ) { + $this->initFromRow( $row ); + } + } + } while ( $killed && $row ); + + # If there were any left after the killing finished, return true + if ( !$row ) { + $ret = false; + $this->clear(); + } else { + $ret = true; + } + } else { + $ret = true; + } + } + $db->freeResult( $res ); + + # No blocks found yet? Try looking for range blocks + if ( !$ret && $address != '' ) { + $ret = $this->loadRange( $address, $killExpired ); + } + if ( !$ret ) { + $this->clear(); + } + + return $ret; + } + + /** + * Search the database for any range blocks matching the given address, and + * load the row if one is found. + */ + function loadRange( $address, $killExpired = true ) + { + $fname = 'Block::loadRange'; + + $iaddr = wfIP2Hex( $address ); + if ( $iaddr === false ) { + # Invalid address + return false; + } + + # Only scan ranges which start in this /16, this improves search speed + # Blocks should not cross a /16 boundary. + $range = substr( $iaddr, 0, 4 ); + + $options = ''; + $db =& $this->getDBOptions( $options ); + $ipblocks = $db->tableName( 'ipblocks' ); + $sql = "SELECT * FROM $ipblocks WHERE ipb_range_start LIKE '$range%' ". + "AND ipb_range_start <= '$iaddr' AND ipb_range_end >= '$iaddr' $options"; + $res = $db->query( $sql, $fname ); + $row = $db->fetchObject( $res ); + + $success = false; + if ( $row ) { + # Found a row, initialise this object + $this->initFromRow( $row ); + + # Is it expired? + if ( !$killExpired || !$this->deleteIfExpired() ) { + # No, return true + $success = true; + } + } + + $db->freeResult( $res ); + return $success; + } + + /** + * Determine if a given integer IPv4 address is in a given CIDR network + */ + function isAddressInRange( $addr, $range ) { + list( $network, $bits ) = wfParseCIDR( $range ); + if ( $network !== false && $addr >> ( 32 - $bits ) == $network >> ( 32 - $bits ) ) { + return true; + } else { + return false; + } + } + + function initFromRow( $row ) + { + $this->mAddress = $row->ipb_address; + $this->mReason = $row->ipb_reason; + $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp); + $this->mUser = $row->ipb_user; + $this->mBy = $row->ipb_by; + $this->mAuto = $row->ipb_auto; + $this->mId = $row->ipb_id; + $this->mExpiry = $row->ipb_expiry ? + wfTimestamp(TS_MW,$row->ipb_expiry) : + $row->ipb_expiry; + if ( isset( $row->user_name ) ) { + $this->mByName = $row->user_name; + } else { + $this->mByName = false; + } + $this->mRangeStart = $row->ipb_range_start; + $this->mRangeEnd = $row->ipb_range_end; + } + + function initialiseRange() + { + $this->mRangeStart = ''; + $this->mRangeEnd = ''; + if ( $this->mUser == 0 ) { + list( $network, $bits ) = wfParseCIDR( $this->mAddress ); + if ( $network !== false ) { + $this->mRangeStart = sprintf( '%08X', $network ); + $this->mRangeEnd = sprintf( '%08X', $network + (1 << (32 - $bits)) - 1 ); + } + } + } + + /** + * Callback with a Block object for every block + * @return integer number of blocks; + */ + /*static*/ function enumBlocks( $callback, $tag, $flags = 0 ) + { + global $wgAntiLockFlags; + + $block = new Block(); + if ( $flags & Block::EB_FOR_UPDATE ) { + $db =& wfGetDB( DB_MASTER ); + if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) { + $options = ''; + } else { + $options = 'FOR UPDATE'; + } + $block->forUpdate( true ); + } else { + $db =& wfGetDB( DB_SLAVE ); + $options = ''; + } + if ( $flags & Block::EB_RANGE_ONLY ) { + $cond = " AND ipb_range_start <> ''"; + } else { + $cond = ''; + } + + $now = wfTimestampNow(); + + extract( $db->tableNames( 'ipblocks', 'user' ) ); + + $sql = "SELECT $ipblocks.*,user_name FROM $ipblocks,$user " . + "WHERE user_id=ipb_by $cond ORDER BY ipb_timestamp DESC $options"; + $res = $db->query( $sql, 'Block::enumBlocks' ); + $num_rows = $db->numRows( $res ); + + while ( $row = $db->fetchObject( $res ) ) { + $block->initFromRow( $row ); + if ( ( $flags & Block::EB_RANGE_ONLY ) && $block->mRangeStart == '' ) { + continue; + } + + if ( !( $flags & Block::EB_KEEP_EXPIRED ) ) { + if ( $block->mExpiry && $now > $block->mExpiry ) { + $block->delete(); + } else { + call_user_func( $callback, $block, $tag ); + } + } else { + call_user_func( $callback, $block, $tag ); + } + } + wfFreeResult( $res ); + return $num_rows; + } + + function delete() + { + $fname = 'Block::delete'; + if (wfReadOnly()) { + return; + } + $dbw =& wfGetDB( DB_MASTER ); + + if ( $this->mAddress == '' ) { + $condition = array( 'ipb_id' => $this->mId ); + } else { + $condition = array( 'ipb_address' => $this->mAddress ); + } + return( $dbw->delete( 'ipblocks', $condition, $fname ) > 0 ? true : false ); + } + + function insert() + { + wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); + $dbw =& wfGetDB( DB_MASTER ); + $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); + $dbw->insert( 'ipblocks', + array( + 'ipb_id' => $ipb_id, + 'ipb_address' => $this->mAddress, + 'ipb_user' => $this->mUser, + 'ipb_by' => $this->mBy, + 'ipb_reason' => $this->mReason, + 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), + 'ipb_auto' => $this->mAuto, + 'ipb_expiry' => $this->mExpiry ? + $dbw->timestamp($this->mExpiry) : + $this->mExpiry, + 'ipb_range_start' => $this->mRangeStart, + 'ipb_range_end' => $this->mRangeEnd, + ), 'Block::insert' + ); + } + + function deleteIfExpired() + { + $fname = 'Block::deleteIfExpired'; + wfProfileIn( $fname ); + if ( $this->isExpired() ) { + wfDebug( "Block::deleteIfExpired() -- deleting\n" ); + $this->delete(); + $retVal = true; + } else { + wfDebug( "Block::deleteIfExpired() -- not expired\n" ); + $retVal = false; + } + wfProfileOut( $fname ); + return $retVal; + } + + function isExpired() + { + wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" ); + if ( !$this->mExpiry ) { + return false; + } else { + return wfTimestampNow() > $this->mExpiry; + } + } + + function isValid() + { + return $this->mAddress != ''; + } + + function updateTimestamp() + { + if ( $this->mAuto ) { + $this->mTimestamp = wfTimestamp(); + $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->update( 'ipblocks', + array( /* SET */ + 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), + 'ipb_expiry' => $dbw->timestamp($this->mExpiry), + ), array( /* WHERE */ + 'ipb_address' => $this->mAddress + ), 'Block::updateTimestamp' + ); + } + } + + /* + function getIntegerAddr() + { + return $this->mIntegerAddr; + } + + function getNetworkBits() + { + return $this->mNetworkBits; + }*/ + + function getByName() + { + if ( $this->mByName === false ) { + $this->mByName = User::whoIs( $this->mBy ); + } + return $this->mByName; + } + + function forUpdate( $x = NULL ) { + return wfSetVar( $this->mForUpdate, $x ); + } + + function fromMaster( $x = NULL ) { + return wfSetVar( $this->mFromMaster, $x ); + } + + /* static */ function getAutoblockExpiry( $timestamp ) + { + global $wgAutoblockExpiry; + return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); + } + + /* static */ function normaliseRange( $range ) + { + $parts = explode( '/', $range ); + if ( count( $parts ) == 2 ) { + $shift = 32 - $parts[1]; + $ipint = wfIP2Unsigned( $parts[0] ); + $ipint = $ipint >> $shift << $shift; + $newip = long2ip( $ipint ); + $range = "$newip/{$parts[1]}"; + } + return $range; + } + +} +?> diff --git a/includes/CacheManager.php b/includes/CacheManager.php new file mode 100644 index 00000000..b9e307f4 --- /dev/null +++ b/includes/CacheManager.php @@ -0,0 +1,159 @@ +mTitle =& $title; + $this->mFileCache = ''; + } + + function fileCacheName() { + global $wgFileCacheDirectory; + if( !$this->mFileCache ) { + $key = $this->mTitle->getPrefixedDbkey(); + $hash = md5( $key ); + $key = str_replace( '.', '%2E', urlencode( $key ) ); + + $hash1 = substr( $hash, 0, 1 ); + $hash2 = substr( $hash, 0, 2 ); + $this->mFileCache = "{$wgFileCacheDirectory}/{$hash1}/{$hash2}/{$key}.html"; + + if($this->useGzip()) + $this->mFileCache .= '.gz'; + + wfDebug( " fileCacheName() - {$this->mFileCache}\n" ); + } + return $this->mFileCache; + } + + function isFileCached() { + return file_exists( $this->fileCacheName() ); + } + + function fileCacheTime() { + return wfTimestamp( TS_MW, filemtime( $this->fileCacheName() ) ); + } + + function isFileCacheGood( $timestamp ) { + global $wgCacheEpoch; + + if( !$this->isFileCached() ) return false; + + $cachetime = $this->fileCacheTime(); + $good = (( $timestamp <= $cachetime ) && + ( $wgCacheEpoch <= $cachetime )); + + wfDebug(" isFileCacheGood() - cachetime $cachetime, touched {$timestamp} epoch {$wgCacheEpoch}, good $good\n"); + return $good; + } + + function useGzip() { + global $wgUseGzip; + return $wgUseGzip; + } + + /* In handy string packages */ + function fetchRawText() { + return file_get_contents( $this->fileCacheName() ); + } + + function fetchPageText() { + if( $this->useGzip() ) { + /* Why is there no gzfile_get_contents() or gzdecode()? */ + return implode( '', gzfile( $this->fileCacheName() ) ); + } else { + return $this->fetchRawText(); + } + } + + /* Working directory to/from output */ + function loadFromFileCache() { + global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode; + wfDebug(" loadFromFileCache()\n"); + + $filename=$this->fileCacheName(); + $wgOut->sendCacheControl(); + + header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); + header( "Content-language: $wgContLanguageCode" ); + + if( $this->useGzip() ) { + if( wfClientAcceptsGzip() ) { + header( 'Content-Encoding: gzip' ); + } else { + /* Send uncompressed */ + readgzfile( $filename ); + return; + } + } + readfile( $filename ); + } + + function checkCacheDirs() { + $filename = $this->fileCacheName(); + $mydir2=substr($filename,0,strrpos($filename,'/')); # subdirectory level 2 + $mydir1=substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1 + + if(!file_exists($mydir1)) { mkdir($mydir1,0775); } # create if necessary + if(!file_exists($mydir2)) { mkdir($mydir2,0775); } + } + + function saveToFileCache( $origtext ) { + $text = $origtext; + if(strcmp($text,'') == 0) return ''; + + wfDebug(" saveToFileCache()\n", false); + + $this->checkCacheDirs(); + + $f = fopen( $this->fileCacheName(), 'w' ); + if($f) { + $now = wfTimestampNow(); + if( $this->useGzip() ) { + $rawtext = str_replace( '', + '\n", + $text ); + $text = gzencode( $rawtext ); + } else { + $text = str_replace( '', + '\n", + $text ); + } + fwrite( $f, $text ); + fclose( $f ); + if( $this->useGzip() ) { + if( wfClientAcceptsGzip() ) { + header( 'Content-Encoding: gzip' ); + return $text; + } else { + return $rawtext; + } + } else { + return $text; + } + } + return $text; + } + +} + +?> diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php new file mode 100644 index 00000000..53d69971 --- /dev/null +++ b/includes/CategoryPage.php @@ -0,0 +1,315 @@ +mTitle->getNamespace() ) { + $this->openShowCategory(); + } + + Article::view(); + + # If the article we've just shown is in the "Image" namespace, + # follow it with the history list and link list for the image + # it describes. + + if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { + $this->closeShowCategory(); + } + } + + function openShowCategory() { + # For overloading + } + + function closeShowCategory() { + global $wgOut, $wgRequest; + $from = $wgRequest->getVal( 'from' ); + $until = $wgRequest->getVal( 'until' ); + + $wgOut->addHTML( $this->doCategoryMagic( $from, $until ) ); + } + + /** + * Format the category data list. + * + * @param string $from -- return only sort keys from this item on + * @param string $until -- don't return keys after this point. + * @return string HTML output + * @private + */ + function doCategoryMagic( $from = '', $until = '' ) { + global $wgOut; + global $wgContLang,$wgUser, $wgCategoryMagicGallery, $wgCategoryPagingLimit; + $fname = 'CategoryPage::doCategoryMagic'; + wfProfileIn( $fname ); + + $articles = array(); + $articles_start_char = array(); + $children = array(); + $children_start_char = array(); + + $showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; + if( $showGallery ) { + $ig = new ImageGallery(); + $ig->setParsing(); + } + + $dbr =& wfGetDB( DB_SLAVE ); + if( $from != '' ) { + $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $from ); + $flip = false; + } elseif( $until != '' ) { + $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $until ); + $flip = true; + } else { + $pageCondition = '1 = 1'; + $flip = false; + } + $limit = $wgCategoryPagingLimit; + $res = $dbr->select( + array( 'page', 'categorylinks' ), + array( 'page_title', 'page_namespace', 'page_len', 'cl_sortkey' ), + array( $pageCondition, + 'cl_from = page_id', + 'cl_to' => $this->mTitle->getDBKey()), + #'page_is_redirect' => 0), + #+ $pageCondition, + $fname, + array( 'ORDER BY' => $flip ? 'cl_sortkey DESC' : 'cl_sortkey', + 'LIMIT' => $limit + 1 ) ); + + $sk =& $wgUser->getSkin(); + $r = "
    \n"; + $count = 0; + $nextPage = null; + while( $x = $dbr->fetchObject ( $res ) ) { + if( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $nextPage = $x->cl_sortkey; + break; + } + + $title = Title::makeTitle( $x->page_namespace, $x->page_title ); + + if( $title->getNamespace() == NS_CATEGORY ) { + // Subcategory; strip the 'Category' namespace from the link text. + array_push( $children, $sk->makeKnownLinkObj( $title, $wgContLang->convertHtml( $title->getText() ) ) ); + + // If there's a link from Category:A to Category:B, the sortkey of the resulting + // entry in the categorylinks table is Category:A, not A, which it SHOULD be. + // Workaround: If sortkey == "Category:".$title, than use $title for sorting, + // else use sortkey... + $sortkey=''; + if( $title->getPrefixedText() == $x->cl_sortkey ) { + $sortkey=$wgContLang->firstChar( $x->page_title ); + } else { + $sortkey=$wgContLang->firstChar( $x->cl_sortkey ); + } + array_push( $children_start_char, $wgContLang->convert( $sortkey ) ) ; + } elseif( $showGallery && $title->getNamespace() == NS_IMAGE ) { + // Show thumbnails of categorized images, in a separate chunk + if( $flip ) { + $ig->insert( Image::newFromTitle( $title ) ); + } else { + $ig->add( Image::newFromTitle( $title ) ); + } + } else { + // Page in this category + array_push( $articles, $sk->makeSizeLinkObj( $x->page_len, $title, $wgContLang->convert( $title->getPrefixedText() ) ) ) ; + array_push( $articles_start_char, $wgContLang->convert( $wgContLang->firstChar( $x->cl_sortkey ) ) ); + } + } + $dbr->freeResult( $res ); + + if( $flip ) { + $children = array_reverse( $children ); + $children_start_char = array_reverse( $children_start_char ); + $articles = array_reverse( $articles ); + $articles_start_char = array_reverse( $articles_start_char ); + } + + if( $until != '' ) { + $r .= $this->pagingLinks( $this->mTitle, $nextPage, $until, $limit ); + } elseif( $nextPage != '' || $from != '' ) { + $r .= $this->pagingLinks( $this->mTitle, $from, $nextPage, $limit ); + } + + # Don't show subcategories section if there are none. + if( count( $children ) > 0 ) { + # Showing subcategories + $r .= '

    ' . wfMsg( 'subcategories' ) . "

    \n"; + $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), count( $children) ); + $r .= $this->formatList( $children, $children_start_char ); + } + + # Showing articles in this category + $ti = htmlspecialchars( $this->mTitle->getText() ); + $r .= '

    ' . wfMsg( 'category_header', $ti ) . "

    \n"; + $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), count( $articles) ); + $r .= $this->formatList( $articles, $articles_start_char ); + + if( $showGallery && ! $ig->isEmpty() ) { + $r.= $ig->toHTML(); + } + + if( $until != '' ) { + $r .= $this->pagingLinks( $this->mTitle, $nextPage, $until, $limit ); + } elseif( $nextPage != '' || $from != '' ) { + $r .= $this->pagingLinks( $this->mTitle, $from, $nextPage, $limit ); + } + + wfProfileOut( $fname ); + return $r; + } + + /** + * Format a list of articles chunked by letter, either as a + * bullet list or a columnar format, depending on the length. + * + * @param array $articles + * @param array $articles_start_char + * @param int $cutoff + * @return string + * @private + */ + function formatList( $articles, $articles_start_char, $cutoff = 6 ) { + if ( count ( $articles ) > $cutoff ) { + return $this->columnList( $articles, $articles_start_char ); + } elseif ( count($articles) > 0) { + // for short lists of articles in categories. + return $this->shortList( $articles, $articles_start_char ); + } + return ''; + } + + /** + * Format a list of articles chunked by letter in a three-column + * list, ordered vertically. + * + * @param array $articles + * @param array $articles_start_char + * @return string + * @private + */ + function columnList( $articles, $articles_start_char ) { + // divide list into three equal chunks + $chunk = (int) (count ( $articles ) / 3); + + // get and display header + $r = ''; + + $prev_start_char = 'none'; + + // loop through the chunks + for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0; + $chunkIndex < 3; + $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1) + { + $r .= "\n"; + + + } + $r .= '
    \n"; + $atColumnTop = true; + + // output all articles in category + for ($index = $startChunk ; + $index < $endChunk && $index < count($articles); + $index++ ) + { + // check for change of starting letter or begining of chunk + if ( ($index == $startChunk) || + ($articles_start_char[$index] != $articles_start_char[$index - 1]) ) + + { + if( $atColumnTop ) { + $atColumnTop = false; + } else { + $r .= "\n"; + } + $cont_msg = ""; + if ( $articles_start_char[$index] == $prev_start_char ) + $cont_msg = wfMsgHtml('listingcontinuesabbrev'); + $r .= "

    " . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg

    \n
      "; + $prev_start_char = $articles_start_char[$index]; + } + + $r .= "
    • {$articles[$index]}
    • "; + } + if( !$atColumnTop ) { + $r .= "
    \n"; + } + $r .= "
    '; + return $r; + } + + /** + * Format a list of articles chunked by letter in a bullet list. + * @param array $articles + * @param array $articles_start_char + * @return string + * @private + */ + function shortList( $articles, $articles_start_char ) { + $r = '

    ' . htmlspecialchars( $articles_start_char[0] ) . "

    \n"; + $r .= '
    • '.$articles[0].'
    • '; + for ($index = 1; $index < count($articles); $index++ ) + { + if ($articles_start_char[$index] != $articles_start_char[$index - 1]) + { + $r .= "

    " . htmlspecialchars( $articles_start_char[$index] ) . "

    \n
      "; + } + + $r .= "
    • {$articles[$index]}
    • "; + } + $r .= '
    '; + return $r; + } + + /** + * @param Title $title + * @param string $first + * @param string $last + * @param int $limit + * @param array $query - additional query options to pass + * @return string + * @private + */ + function pagingLinks( $title, $first, $last, $limit, $query = array() ) { + global $wgUser, $wgLang; + $sk =& $wgUser->getSkin(); + $limitText = $wgLang->formatNum( $limit ); + + $prevLink = htmlspecialchars( wfMsg( 'prevn', $limitText ) ); + if( $first != '' ) { + $prevLink = $sk->makeLinkObj( $title, $prevLink, + wfArrayToCGI( $query + array( 'until' => $first ) ) ); + } + $nextLink = htmlspecialchars( wfMsg( 'nextn', $limitText ) ); + if( $last != '' ) { + $nextLink = $sk->makeLinkObj( $title, $nextLink, + wfArrayToCGI( $query + array( 'from' => $last ) ) ); + } + + return "($prevLink) ($nextLink)"; + } +} + + +?> diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php new file mode 100644 index 00000000..a8cdf3ce --- /dev/null +++ b/includes/Categoryfinder.php @@ -0,0 +1,191 @@ +seed ( + array ( 12345 ) , + array ( "Category 1","Category 2" ) , + "AND" + ) ; + $a = $cf->run() ; + print implode ( "," , $a ) ; + +*/ + + +class Categoryfinder { + + var $articles = array () ; # The original article IDs passed to the seed function + var $deadend = array () ; # Array of DBKEY category names for categories that don't have a page + var $parents = array () ; # Array of [ID => array()] + var $next = array () ; # Array of article/category IDs + var $targets = array () ; # Array of DBKEY category names + var $name2id = array () ; + var $mode ; # "AND" or "OR" + var $dbr ; # Read-DB slave + + /** + * Constructor (currently empty). + */ + function Categoryfinder () { + } + + /** + * Initializes the instance. Do this prior to calling run(). + * @param $article_ids Array of article IDs + * @param $categories FIXME + * @param $mode String: FIXME, default 'AND'. + */ + function seed ( $article_ids , $categories , $mode = "AND" ) { + $this->articles = $article_ids ; + $this->next = $article_ids ; + $this->mode = $mode ; + + # Set the list of target categories; convert them to DBKEY form first + $this->targets = array () ; + foreach ( $categories AS $c ) { + $ct = Title::newFromText ( $c , NS_CATEGORY ) ; + $c = $ct->getDBkey () ; + $this->targets[$c] = $c ; + } + } + + /** + * Iterates through the parent tree starting with the seed values, + * then checks the articles if they match the conditions + @return array of page_ids (those given to seed() that match the conditions) + */ + function run () { + $this->dbr =& wfGetDB( DB_SLAVE ); + while ( count ( $this->next ) > 0 ) { + $this->scan_next_layer () ; + } + + # Now check if this applies to the individual articles + $ret = array () ; + foreach ( $this->articles AS $article ) { + $conds = $this->targets ; + if ( $this->check ( $article , $conds ) ) { + # Matches the conditions + $ret[] = $article ; + } + } + return $ret ; + } + + /** + * This functions recurses through the parent representation, trying to match the conditions + @param $id The article/category to check + @param $conds The array of categories to match + @return bool Does this match the conditions? + */ + function check ( $id , &$conds ) { + # Shortcut (runtime paranoia): No contitions=all matched + if ( count ( $conds ) == 0 ) return true ; + + if ( !isset ( $this->parents[$id] ) ) return false ; + + # iterate through the parents + foreach ( $this->parents[$id] AS $p ) { + $pname = $p->cl_to ; + + # Is this a condition? + if ( isset ( $conds[$pname] ) ) { + # This key is in the category list! + if ( $this->mode == "OR" ) { + # One found, that's enough! + $conds = array () ; + return true ; + } else { + # Assuming "AND" as default + unset ( $conds[$pname] ) ; + if ( count ( $conds ) == 0 ) { + # All conditions met, done + return true ; + } + } + } + + # Not done yet, try sub-parents + if ( !isset ( $this->name2id[$pname] ) ) { + # No sub-parent + continue ; + } + $done = $this->check ( $this->name2id[$pname] , $conds ) ; + if ( $done OR count ( $conds ) == 0 ) { + # Subparents have done it! + return true ; + } + } + return false ; + } + + /** + * Scans a "parent layer" of the articles/categories in $this->next + */ + function scan_next_layer () { + $fname = "Categoryfinder::scan_next_layer" ; + + # Find all parents of the article currently in $this->next + $layer = array () ; + $res = $this->dbr->select( + /* FROM */ 'categorylinks', + /* SELECT */ '*', + /* WHERE */ array( 'cl_from' => $this->next ), + $fname."-1" + ); + while ( $o = $this->dbr->fetchObject( $res ) ) { + $k = $o->cl_to ; + + # Update parent tree + if ( !isset ( $this->parents[$o->cl_from] ) ) { + $this->parents[$o->cl_from] = array () ; + } + $this->parents[$o->cl_from][$k] = $o ; + + # Ignore those we already have + if ( in_array ( $k , $this->deadend ) ) continue ; + if ( isset ( $this->name2id[$k] ) ) continue ; + + # Hey, new category! + $layer[$k] = $k ; + } + $this->dbr->freeResult( $res ) ; + + $this->next = array() ; + + # Find the IDs of all category pages in $layer, if they exist + if ( count ( $layer ) > 0 ) { + $res = $this->dbr->select( + /* FROM */ 'page', + /* SELECT */ 'page_id,page_title', + /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ), + $fname."-2" + ); + while ( $o = $this->dbr->fetchObject( $res ) ) { + $id = $o->page_id ; + $name = $o->page_title ; + $this->name2id[$name] = $id ; + $this->next[] = $id ; + unset ( $layer[$name] ) ; + } + $this->dbr->freeResult( $res ) ; + } + + # Mark dead ends + foreach ( $layer AS $v ) { + $this->deadend[$v] = $v ; + } + } + +} # END OF CLASS "Categoryfinder" + +?> diff --git a/includes/ChangesList.php b/includes/ChangesList.php new file mode 100644 index 00000000..b2c1abe2 --- /dev/null +++ b/includes/ChangesList.php @@ -0,0 +1,653 @@ +mAttribs = $rc->mAttribs; + $rc2->mExtra = $rc->mExtra; + return $rc2; + } +} ; + +/** + * @package MediaWiki + */ +class ChangesList { + # Called by history lists and recent changes + # + + /** @todo document */ + function ChangesList( &$skin ) { + $this->skin =& $skin; + $this->preCacheMessages(); + } + + /** + * Fetch an appropriate changes list class for the specified user + * Some users might want to use an enhanced list format, for instance + * + * @param $user User to fetch the list class for + * @return ChangesList derivative + */ + function newFromUser( &$user ) { + $sk =& $user->getSkin(); + $list = NULL; + if( wfRunHooks( 'FetchChangesList', array( &$user, &$skin, &$list ) ) ) { + return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); + } else { + return $list; + } + } + + /** + * 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 ) ) { + foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. + 'blocklink changes history boteditletter' ) as $msg ) { + $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); + } + } + } + + + /** + * Returns the appropriate flags for new page, minor change and patrolling + */ + function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { + $f = $new ? '' . $this->message['newpageletter'] . '' + : $nothing; + $f .= $minor ? '' . $this->message['minoreditletter'] . '' + : $nothing; + $f .= $bot ? '' . $this->message['boteditletter'] . '' : $nothing; + $f .= $patrolled ? '!' : $nothing; + return $f; + } + + /** + * Returns text for the start of the tabular part of RC + */ + function beginRecentChangesList() { + $this->rc_cache = array(); + $this->rcMoveIndex = 0; + $this->rcCacheIndex = 0; + $this->lastdate = ''; + $this->rclistOpen = false; + return ''; + } + + /** + * Returns text for the end of RC + */ + function endRecentChangesList() { + if( $this->rclistOpen ) { + return "\n"; + } else { + return ''; + } + } + + + function insertMove( &$s, $rc ) { + # Diff + $s .= '(' . $this->message['diff'] . ') ('; + # Hist + $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'], 'action=history' ) . + ') . . '; + + # "[[x]] moved to [[y]]" + $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; + $s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), + $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); + } + + function insertDateHeader(&$s, $rc_timestamp) { + global $wgLang; + + # Make date header if necessary + $date = $wgLang->date( $rc_timestamp, true, true ); + $s = ''; + if( $date != $this->lastdate ) { + if( '' != $this->lastdate ) { + $s .= "\n"; + } + $s .= '

    '.$date."

    \n
      "; + $this->lastdate = $date; + $this->rclistOpen = true; + } + } + + function insertLog(&$s, $title, $logtype) { + $logname = LogPage::logName( $logtype ); + $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')'; + } + + + function insertDiffHist(&$s, &$rc, $unpatrolled) { + # Diff link + if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { + $diffLink = $this->message['diff']; + } else { + $rcidparam = $unpatrolled + ? array( 'rcid' => $rc->mAttribs['rc_id'] ) + : array(); + $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], + wfArrayToCGI( array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'diff' => $rc->mAttribs['rc_this_oldid'], + 'oldid' => $rc->mAttribs['rc_last_oldid'] ), + $rcidparam ), + '', '', ' tabindex="'.$rc->counter.'"'); + } + $s .= '('.$diffLink.') ('; + + # History link + $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'], + wfArrayToCGI( array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'action' => 'history' ) ) ); + $s .= ') . . '; + } + + function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) { + # Article link + # If it's a new article, there is no diff link, but if it hasn't been + # patrolled yet, we need to give users a way to do so + $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) + ? 'rcid='.$rc->mAttribs['rc_id'] + : ''; + $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + if($watched) $articlelink = ''.$articlelink.''; + global $wgContLang; + $articlelink .= $wgContLang->getDirMark(); + + $s .= ' '.$articlelink; + } + + function insertTimestamp(&$s, &$rc) { + global $wgLang; + # Timestamp + $s .= '; ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + } + + /** Insert links to user page, user talk page and eventually a blocking link */ + function insertUserRelatedLinks(&$s, &$rc) { + $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + } + + /** insert a formatted comment */ + function insertComment(&$s, &$rc) { + # Add comment + if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { + $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + } + } + + /** + * Check whether to enable recent changes patrol features + * @return bool + */ + function usePatrol() { + global $wgUseRCPatrol, $wgUser; + return( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ); + } + + +} + + +/** + * Generate a list of changes using the good old system (no javascript) + */ +class OldChangesList extends ChangesList { + /** + * Format a line using the old system (aka without any javascript). + */ + function recentChangesLine( &$rc, $watched = false ) { + global $wgContLang; + + $fname = 'ChangesList::recentChangesLineOld'; + wfProfileIn( $fname ); + + + # Extract DB fields into local scope + extract( $rc->mAttribs ); + $curIdEq = 'curid=' . $rc_cur_id; + + # Should patrol-related stuff be shown? + $unpatrolled = $this->usePatrol() && $rc_patrolled == 0; + + $this->insertDateHeader($s,$rc_timestamp); + + $s .= '
    • '; + + // moved pages + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $this->insertMove( $s, $rc ); + // log entries + } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { + $this->insertLog($s, $rc->getTitle(), $matches[1]); + // all other stuff + } else { + wfProfileIn($fname.'-page'); + + $this->insertDiffHist($s, $rc, $unpatrolled); + + # M, N, b and ! (minor, new, bot and unpatrolled) + $s .= ' ' . $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot ); + $this->insertArticleLink($s, $rc, $unpatrolled, $watched); + + wfProfileOut($fname.'-page'); + } + + wfProfileIn( $fname.'-rest' ); + + $this->insertTimestamp($s,$rc); + $this->insertUserRelatedLinks($s,$rc); + $this->insertComment($s, $rc); + + if($rc->numberofWatchingusers > 0) { + $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers)); + } + + $s .= "
    • \n"; + + wfProfileOut( $fname.'-rest' ); + + wfProfileOut( $fname ); + return $s; + } +} + + +/** + * Generate a list of changes using an Enhanced system (use javascript). + */ +class EnhancedChangesList extends ChangesList { + /** + * Format a line for enhanced recentchange (aka with javascript and block of lines). + */ + function recentChangesLine( &$baseRC, $watched = false ) { + global $wgLang, $wgContLang; + + # Create a specialised object + $rc = RCCacheEntry::newFromParent( $baseRC ); + + # Extract fields from DB into the function scope (rc_xxxx variables) + extract( $rc->mAttribs ); + $curIdEq = 'curid=' . $rc_cur_id; + + # If it's a new day, add the headline and flush the cache + $date = $wgLang->date( $rc_timestamp, true); + $ret = ''; + if( $date != $this->lastdate ) { + # Process current cache + $ret = $this->recentChangesBlock(); + $this->rc_cache = array(); + $ret .= "

      {$date}

      \n"; + $this->lastdate = $date; + } + + # Should patrol-related stuff be shown? + if( $this->usePatrol() ) { + $rc->unpatrolled = !$rc_patrolled; + } else { + $rc->unpatrolled = false; + } + + # Make article link + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; + $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), + $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); + } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { + # Log updates, etc + $logtype = $matches[1]; + $logname = LogPage::logName( $logtype ); + $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) { + # Unpatrolled new page, give rc_id in query + $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); + } else { + $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); + } + + $time = $wgContLang->time( $rc_timestamp, true, true ); + $rc->watched = $watched; + $rc->link = $clink; + $rc->timestamp = $time; + $rc->numberofWatchingusers = $baseRC->numberofWatchingusers; + + # Make "cur" and "diff" links + if( $rc->unpatrolled ) { + $rcIdQuery = "&rcid={$rc_id}"; + } else { + $rcIdQuery = ''; + } + $querycur = $curIdEq."&diff=0&oldid=$rc_this_oldid"; + $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery"; + $aprops = ' tabindex="'.$baseRC->counter.'"'; + $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops ); + if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $rc_type != RC_NEW ) { + $curLink = $this->message['cur']; + } + $diffLink = $this->message['diff']; + } else { + $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], $querydiff, '' ,'', $aprops ); + } + + # Make "last" link + if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $lastLink = $this->message['last']; + } else { + $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'], + $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery ); + } + + $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); + + $rc->lastlink = $lastLink; + $rc->curlink = $curLink; + $rc->difflink = $diffLink; + + $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); + + # Put accumulated information into the cache, for later display + # Page moves go on their own line + $title = $rc->getTitle(); + $secureName = $title->getPrefixedDBkey(); + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + # Use an @ character to prevent collision with page names + $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc); + } else { + if( !isset ( $this->rc_cache[$secureName] ) ) { + $this->rc_cache[$secureName] = array(); + } + array_push( $this->rc_cache[$secureName], $rc ); + } + return $ret; + } + + /** + * Enhanced RC group + */ + function recentChangesBlockGroup( $block ) { + $r = ''; + + # Collate list of users + $isnew = false; + $unpatrolled = false; + $userlinks = array(); + foreach( $block as $rcObj ) { + $oldid = $rcObj->mAttribs['rc_last_oldid']; + $newid = $rcObj->mAttribs['rc_this_oldid']; + if( $rcObj->mAttribs['rc_new'] ) { + $isnew = true; + } + $u = $rcObj->userlink; + if( !isset( $userlinks[$u] ) ) { + $userlinks[$u] = 0; + } + if( $rcObj->unpatrolled ) { + $unpatrolled = true; + } + $bot = $rcObj->mAttribs['rc_bot']; + $userlinks[$u]++; + } + + # Sort the list and convert to text + krsort( $userlinks ); + asort( $userlinks ); + $users = array(); + foreach( $userlinks as $userlink => $count) { + $text = $userlink; + if( $count > 1 ) { + $text .= ' ('.$count.'×)'; + } + array_push( $users, $text ); + } + + $users = ' ['.implode('; ',$users).']'; + + # Arrow + $rci = 'RCI'.$this->rcCacheIndex; + $rcl = 'RCL'.$this->rcCacheIndex; + $rcm = 'RCM'.$this->rcCacheIndex; + $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')"; + $tl = '' . $this->sideArrow() . ''; + $tl .= ''; + $r .= $tl; + + # Main line + $r .= ''; + $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); + + # Timestamp + $r .= ' '.$block[0]->timestamp.' '; + $r .= ''; + + # Article link + $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); + + $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id']; + $currentRevision = $block[0]->mAttribs['rc_this_oldid']; + if( $block[0]->mAttribs['rc_type'] != RC_LOG ) { + # Changes + $r .= ' ('.count($block).' '; + if( $isnew ) { + $r .= $this->message['changes']; + } else { + $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), + $this->message['changes'], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); + } + $r .= '; '; + + # History + $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), + $this->message['history'], $curIdEq.'&action=history' ); + $r .= ')'; + } + + $r .= $users; + + if($block[0]->numberofWatchingusers > 0) { + global $wgContLang; + $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($block[0]->numberofWatchingusers)); + } + $r .= "
      \n"; + + # Sub-entries + $r .= '\n"; + + $this->rcCacheIndex++; + return $r; + } + + function maybeWatchedLink( $link, $watched=false ) { + if( $watched ) { + // FIXME: css style might be more appropriate + return '' . $link . ''; + } else { + return $link; + } + } + + /** + * Generate HTML for an arrow or placeholder graphic + * @param string $dir one of '', 'd', 'l', 'r' + * @param string $alt text + * @return string HTML tag + * @access private + */ + function arrow( $dir, $alt='' ) { + global $wgStylePath; + $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' ); + $encAlt = htmlspecialchars( $alt ); + return "\"$encAlt\""; + } + + /** + * Generate HTML for a right- or left-facing arrow, + * depending on language direction. + * @return string HTML tag + * @access private + */ + function sideArrow() { + global $wgContLang; + $dir = $wgContLang->isRTL() ? 'l' : 'r'; + return $this->arrow( $dir, '+' ); + } + + /** + * Generate HTML for a down-facing arrow + * depending on language direction. + * @return string HTML tag + * @access private + */ + function downArrow() { + return $this->arrow( 'd', '-' ); + } + + /** + * Generate HTML for a spacer image + * @return string HTML tag + * @access private + */ + function spacerArrow() { + return $this->arrow( '', ' ' ); + } + + /** + * Enhanced RC ungrouped line. + * @return string a HTML formated line (generated using $r) + */ + function recentChangesBlockLine( $rcObj ) { + global $wgContLang; + + # Get rc_xxxx variables + extract( $rcObj->mAttribs ); + $curIdEq = 'curid='.$rc_cur_id; + + $r = ''; + + # Spacer image + $r .= $this->spacerArrow(); + + # Flag and Timestamp + $r .= ''; + + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $r .= '   '; + } else { + $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); + } + $r .= ' '.$rcObj->timestamp.' '; + + # Article link + $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); + + # Diff + $r .= ' ('. $rcObj->difflink .'; '; + + # Hist + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ); + + # User/talk + $r .= ') . . '.$rcObj->userlink . $rcObj->usertalklink; + + # Comment + if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { + $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); + } + + if( $rcObj->numberofWatchingusers > 0 ) { + $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rcObj->numberofWatchingusers)); + } + + $r .= "
      \n"; + return $r; + } + + /** + * If enhanced RC is in use, this function takes the previously cached + * RC lines, arranges them, and outputs the HTML + */ + function recentChangesBlock() { + if( count ( $this->rc_cache ) == 0 ) { + return ''; + } + $blockOut = ''; + foreach( $this->rc_cache as $secureName => $block ) { + if( count( $block ) < 2 ) { + $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) ); + } else { + $blockOut .= $this->recentChangesBlockGroup( $block ); + } + } + + return '
      '.$blockOut.'
      '; + } + + /** + * Returns text for the end of RC + * If enhanced RC is in use, returns pretty much all the text + */ + function endRecentChangesList() { + return $this->recentChangesBlock() . parent::endRecentChangesList(); + } + +} +?> diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php new file mode 100644 index 00000000..d6578abf --- /dev/null +++ b/includes/CoreParserFunctions.php @@ -0,0 +1,150 @@ +getNsText( intval( $part1 ) ); + $found = true; + } else { + $param = str_replace( ' ', '_', strtolower( $part1 ) ); + $index = Namespace::getCanonicalIndex( strtolower( $param ) ); + if ( !is_null( $index ) ) { + $text = $wgContLang->getNsText( $index ); + $found = true; + } + } + if ( $found ) { + return $text; + } else { + return array( 'found' => false ); + } + } + + static function urlencode( $parser, $s = '' ) { + return urlencode( $s ); + } + + static function lcfirst( $parser, $s = '' ) { + global $wgContLang; + return $wgContLang->lcfirst( $s ); + } + + static function ucfirst( $parser, $s = '' ) { + global $wgContLang; + return $wgContLang->ucfirst( $s ); + } + + static function lc( $parser, $s = '' ) { + global $wgContLang; + return $wgContLang->lc( $s ); + } + + static function uc( $parser, $s = '' ) { + global $wgContLang; + return $wgContLang->uc( $s ); + } + + static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); } + static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); } + static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); } + static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); } + + static function urlFunction( $func, $s = '', $arg = null ) { + $found = false; + $title = Title::newFromText( $s ); + # Due to order of execution of a lot of bits, the values might be encoded + # before arriving here; if that's true, then the title can't be created + # and the variable will fail. If we can't get a decent title from the first + # attempt, url-decode and try for a second. + if( is_null( $title ) ) + $title = Title::newFromUrl( urldecode( $s ) ); + if ( !is_null( $title ) ) { + if ( !is_null( $arg ) ) { + $text = $title->$func( $arg ); + } else { + $text = $title->$func(); + } + $found = true; + } + if ( $found ) { + return $text; + } else { + return array( 'found' => false ); + } + } + + function formatNum( $parser, $num = '' ) { + return $parser->getFunctionLang()->formatNum( $num ); + } + + function grammar( $parser, $case = '', $word = '' ) { + return $parser->getFunctionLang()->convertGrammar( $word, $case ); + } + + function plural( $parser, $text = '', $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null ) { + return $parser->getFunctionLang()->convertPlural( $text, $arg0, $arg1, $arg2, $arg3, $arg4 ); + } + + function displaytitle( $parser, $param = '' ) { + $parserOptions = new ParserOptions; + $local_parser = clone $parser; + $t2 = $local_parser->parse ( $param, $parser->mTitle, $parserOptions, false ); + $parser->mOutput->mHTMLtitle = $t2->GetText(); + + # Add subtitle + $t = $parser->mTitle->getPrefixedText(); + $parser->mOutput->mSubtitle .= wfMsg('displaytitle', $t); + return ''; + } + + function isRaw( $param ) { + static $mwRaw; + if ( !$mwRaw ) { + $mwRaw =& MagicWord::get( MAG_RAWSUFFIX ); + } + if ( is_null( $param ) ) { + return false; + } else { + return $mwRaw->match( $param ); + } + } + + function statisticsFunction( $func, $raw = null ) { + if ( self::isRaw( $raw ) ) { + return call_user_func( $func ); + } else { + global $wgContLang; + return $wgContLang->formatNum( call_user_func( $func ) ); + } + } + + function numberofpages( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfPages', $raw ); } + function numberofusers( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfUsers', $raw ); } + function numberofarticles( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfArticles', $raw ); } + function numberoffiles( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfFiles', $raw ); } + function numberofadmins( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfAdmins', $raw ); } + + function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { + $count = wfPagesInNs( intval( $namespace ) ); + if ( self::isRaw( $raw ) ) { + global $wgContLang; + return $wgContLang->formatNum( $count ); + } else { + return $count; + } + } + + function language( $parser, $arg = '' ) { + global $wgContLang; + $lang = $wgContLang->getLanguageName( strtolower( $arg ) ); + return $lang != '' ? $lang : $arg; + } +} + +?> diff --git a/includes/Credits.php b/includes/Credits.php new file mode 100644 index 00000000..ff33de74 --- /dev/null +++ b/includes/Credits.php @@ -0,0 +1,187 @@ +. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @author + * @package MediaWiki + */ + +/** + * This is largely cadged from PageHistory::history + */ +function showCreditsPage($article) { + global $wgOut; + + $fname = 'showCreditsPage'; + + wfProfileIn( $fname ); + + $wgOut->setPageTitle( $article->mTitle->getPrefixedText() ); + $wgOut->setSubtitle( wfMsg( 'creditspage' ) ); + $wgOut->setArticleFlag( false ); + $wgOut->setArticleRelated( true ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + if( $article->mTitle->getArticleID() == 0 ) { + $s = wfMsg( 'nocredits' ); + } else { + $s = getCredits($article, -1); + } + + $wgOut->addHTML( $s ); + + wfProfileOut( $fname ); +} + +function getCredits($article, $cnt, $showIfMax=true) { + $fname = 'getCredits'; + wfProfileIn( $fname ); + $s = ''; + + if (isset($cnt) && $cnt != 0) { + $s = getAuthorCredits($article); + if ($cnt > 1 || $cnt < 0) { + $s .= ' ' . getContributorCredits($article, $cnt - 1, $showIfMax); + } + } + + wfProfileOut( $fname ); + return $s; +} + +/** + * + */ +function getAuthorCredits($article) { + global $wgLang, $wgAllowRealName; + + $last_author = $article->getUser(); + + if ($last_author == 0) { + $author_credit = wfMsg('anonymous'); + } else { + if($wgAllowRealName) { $real_name = User::whoIsReal($last_author); } + $user_name = User::whoIs($last_author); + + if (!empty($real_name)) { + $author_credit = creditLink($user_name, $real_name); + } else { + $author_credit = wfMsg('siteuser', creditLink($user_name)); + } + } + + $timestamp = $article->getTimestamp(); + if ($timestamp) { + $d = $wgLang->timeanddate($article->getTimestamp(), true); + } else { + $d = ''; + } + return wfMsg('lastmodifiedby', $d, $author_credit); +} + +/** + * + */ +function getContributorCredits($article, $cnt, $showIfMax) { + + global $wgLang, $wgAllowRealName; + + $contributors = $article->getContributors(); + + $others_link = ''; + + # Hmm... too many to fit! + + if ($cnt > 0 && count($contributors) > $cnt) { + $others_link = creditOthersLink($article); + if (!$showIfMax) { + return wfMsg('othercontribs', $others_link); + } else { + $contributors = array_slice($contributors, 0, $cnt); + } + } + + $real_names = array(); + $user_names = array(); + + $anon = ''; + + # Sift for real versus user names + + foreach ($contributors as $user_parts) { + if ($user_parts[0] != 0) { + if ($wgAllowRealName && !empty($user_parts[2])) { + $real_names[] = creditLink($user_parts[1], $user_parts[2]); + } else { + $user_names[] = creditLink($user_parts[1]); + } + } else { + $anon = wfMsg('anonymous'); + } + } + + # Two strings: real names, and user names + + $real = $wgLang->listToText($real_names); + $user = $wgLang->listToText($user_names); + + # "ThisSite user(s) A, B and C" + + if (!empty($user)) { + $user = wfMsg('siteusers', $user); + } + + # This is the big list, all mooshed together. We sift for blank strings + + $fulllist = array(); + + foreach (array($real, $user, $anon, $others_link) as $s) { + if (!empty($s)) { + array_push($fulllist, $s); + } + } + + # Make the list into text... + + $creds = $wgLang->listToText($fulllist); + + # "Based on work by ..." + + return (empty($creds)) ? '' : wfMsg('othercontribs', $creds); +} + +/** + * + */ +function creditLink($user_name, $link_text = '') { + global $wgUser, $wgContLang; + $skin = $wgUser->getSkin(); + return $skin->makeLink($wgContLang->getNsText(NS_USER) . ':' . $user_name, + htmlspecialchars( (empty($link_text)) ? $user_name : $link_text )); +} + +/** + * + */ +function creditOthersLink($article) { + global $wgUser; + $skin = $wgUser->getSkin(); + return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits'); +} + +?> diff --git a/includes/Database.php b/includes/Database.php new file mode 100644 index 00000000..f8e579b4 --- /dev/null +++ b/includes/Database.php @@ -0,0 +1,2020 @@ +mData = $data; + } + + function isLOB() { + return false; + } + + function data() { + return $this->mData; + } +}; + +/****************************************************************************** + * Error classes + *****************************************************************************/ + +/** + * Database error base class + */ +class DBError extends MWException { + public $db; + + /** + * Construct a database error + * @param Database $db The database object which threw the error + * @param string $error A simple error message to be used for debugging + */ + function __construct( Database &$db, $error ) { + $this->db =& $db; + parent::__construct( $error ); + } +} + +class DBConnectionError extends DBError { + public $error; + + function __construct( Database &$db, $error = 'unknown error' ) { + $msg = 'DB connection error'; + if ( trim( $error ) != '' ) { + $msg .= ": $error"; + } + $this->error = $error; + parent::__construct( $db, $msg ); + } + + function useOutputPage() { + // Not likely to work + return false; + } + + function useMessageCache() { + // Not likely to work + return false; + } + + function getText() { + return $this->getMessage() . "\n"; + } + + function getPageTitle() { + global $wgSitename; + return "$wgSitename has a problem"; + } + + function getHTML() { + global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding, $wgOutputEncoding; + global $wgSitename, $wgServer, $wgMessageCache, $wgLogo; + + # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky. + # Hard coding strings instead. + + $noconnect = "

      Sorry! This site is experiencing technical difficulties.

      Try waiting a few minutes and reloading.

      (Can't contact the database server: $1)

      "; + $mainpage = 'Main Page'; + $searchdisabled = <<$wgSitename search is disabled for performance reasons. You can search via Google in the meantime. +Note that their indexes of $wgSitename content may be out of date.

      ', +EOT; + + $googlesearch = " + +
      + +
      + +\"Google\" + + + + +
      WWW $wgServer
      + + +
      +
      +
      +"; + $cachederror = "The following is a cached copy of the requested page, and may not be up to date. "; + + # No database access + if ( is_object( $wgMessageCache ) ) { + $wgMessageCache->disable(); + } + + if ( trim( $this->error ) == '' ) { + $this->error = $this->db->getProperty('mServer'); + } + + $text = str_replace( '$1', $this->error, $noconnect ); + $text .= wfGetSiteNotice(); + + if($wgUseFileCache) { + if($wgTitle) { + $t =& $wgTitle; + } else { + if($title) { + $t = Title::newFromURL( $title ); + } elseif (@/**/$_REQUEST['search']) { + $search = $_REQUEST['search']; + return $searchdisabled . + str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ), + $wgInputEncoding ), $googlesearch ); + } else { + $t = Title::newFromText( $mainpage ); + } + } + + $cache = new CacheManager( $t ); + if( $cache->isFileCached() ) { + $msg = '

      '.$msg."
      \n" . + $cachederror . "

      \n"; + + $tag = '
      '; + $text = str_replace( + $tag, + $tag . $msg, + $cache->fetchPageText() ); + } + } + + return $text; + } +} + +class DBQueryError extends DBError { + public $error, $errno, $sql, $fname; + + function __construct( Database &$db, $error, $errno, $sql, $fname ) { + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + + parent::__construct( $db, $message ); + $this->error = $error; + $this->errno = $errno; + $this->sql = $sql; + $this->fname = $fname; + } + + function getText() { + if ( $this->useMessageCache() ) { + return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), + htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; + } else { + return $this->getMessage(); + } + } + + function getSQL() { + global $wgShowSQLErrors; + if( !$wgShowSQLErrors ) { + return $this->msg( 'sqlhidden', 'SQL hidden' ); + } else { + return $this->sql; + } + } + + function getPageTitle() { + return $this->msg( 'databaseerror', 'Database error' ); + } + + function getHTML() { + if ( $this->useMessageCache() ) { + return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), + htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ); + } else { + return nl2br( htmlspecialchars( $this->getMessage() ) ); + } + } +} + +class DBUnexpectedError extends DBError {} + +/******************************************************************************/ + +/** + * Database abstraction object + * @package MediaWiki + */ +class Database { + +#------------------------------------------------------------------------------ +# Variables +#------------------------------------------------------------------------------ + + protected $mLastQuery = ''; + + protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; + protected $mOut, $mOpened = false; + + protected $mFailFunction; + protected $mTablePrefix; + protected $mFlags; + protected $mTrxLevel = 0; + protected $mErrorCount = 0; + protected $mLBInfo = array(); + +#------------------------------------------------------------------------------ +# Accessors +#------------------------------------------------------------------------------ + # These optionally set a variable and return the previous state + + /** + * Fail function, takes a Database as a parameter + * Set to false for default, 1 for ignore errors + */ + function failFunction( $function = NULL ) { + return wfSetVar( $this->mFailFunction, $function ); + } + + /** + * Output page, used for reporting errors + * FALSE means discard output + */ + function setOutputPage( $out ) { + $this->mOut = $out; + } + + /** + * Boolean, controls output of large amounts of debug information + */ + function debug( $debug = NULL ) { + return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); + } + + /** + * Turns buffering of SQL result sets on (true) or off (false). + * Default is "on" and it should not be changed without good reasons. + */ + function bufferResults( $buffer = NULL ) { + if ( is_null( $buffer ) ) { + return !(bool)( $this->mFlags & DBO_NOBUFFER ); + } else { + return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer ); + } + } + + /** + * Turns on (false) or off (true) the automatic generation and sending + * of a "we're sorry, but there has been a database error" page on + * database errors. Default is on (false). When turned off, the + * code should use wfLastErrno() and wfLastError() to handle the + * situation as appropriate. + */ + function ignoreErrors( $ignoreErrors = NULL ) { + return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); + } + + /** + * The current depth of nested transactions + * @param $level Integer: , default NULL. + */ + function trxLevel( $level = NULL ) { + return wfSetVar( $this->mTrxLevel, $level ); + } + + /** + * Number of errors logged, only useful when errors are ignored + */ + function errorCount( $count = NULL ) { + return wfSetVar( $this->mErrorCount, $count ); + } + + /** + * Properties passed down from the server info array of the load balancer + */ + function getLBInfo( $name = NULL ) { + if ( is_null( $name ) ) { + return $this->mLBInfo; + } else { + if ( array_key_exists( $name, $this->mLBInfo ) ) { + return $this->mLBInfo[$name]; + } else { + return NULL; + } + } + } + + function setLBInfo( $name, $value = NULL ) { + if ( is_null( $value ) ) { + $this->mLBInfo = $name; + } else { + $this->mLBInfo[$name] = $value; + } + } + + /**#@+ + * Get function + */ + function lastQuery() { return $this->mLastQuery; } + function isOpen() { return $this->mOpened; } + /**#@-*/ + + function setFlag( $flag ) { + $this->mFlags |= $flag; + } + + function clearFlag( $flag ) { + $this->mFlags &= ~$flag; + } + + function getFlag( $flag ) { + return !!($this->mFlags & $flag); + } + + /** + * General read-only accessor + */ + function getProperty( $name ) { + return $this->$name; + } + +#------------------------------------------------------------------------------ +# Other functions +#------------------------------------------------------------------------------ + + /**@{{ + * @param string $server database server host + * @param string $user database user name + * @param string $password database user password + * @param string $dbname database name + */ + + /** + * @param failFunction + * @param $flags + * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php + */ + function __construct( $server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) { + + global $wgOut, $wgDBprefix, $wgCommandLineMode; + # Can't get a reference if it hasn't been set yet + if ( !isset( $wgOut ) ) { + $wgOut = NULL; + } + $this->mOut =& $wgOut; + + $this->mFailFunction = $failFunction; + $this->mFlags = $flags; + + if ( $this->mFlags & DBO_DEFAULT ) { + if ( $wgCommandLineMode ) { + $this->mFlags &= ~DBO_TRX; + } else { + $this->mFlags |= DBO_TRX; + } + } + + /* + // Faster read-only access + if ( wfReadOnly() ) { + $this->mFlags |= DBO_PERSISTENT; + $this->mFlags &= ~DBO_TRX; + }*/ + + /** Get the default table prefix*/ + if ( $tablePrefix == 'get from global' ) { + $this->mTablePrefix = $wgDBprefix; + } else { + $this->mTablePrefix = $tablePrefix; + } + + if ( $server ) { + $this->open( $server, $user, $password, $dbName ); + } + } + + /** + * @static + * @param failFunction + * @param $flags + */ + static function newFromParams( $server, $user, $password, $dbName, + $failFunction = false, $flags = 0 ) + { + return new Database( $server, $user, $password, $dbName, $failFunction, $flags ); + } + + /** + * Usually aborts on failure + * If the failFunction is set to a non-zero integer, returns success + */ + function open( $server, $user, $password, $dbName ) { + global $wguname; + + # Test for missing mysql.so + # First try to load it + if (!@extension_loaded('mysql')) { + @dl('mysql.so'); + } + + # Fail now + # Otherwise we get a suppressed fatal error, which is very hard to track down + if ( !function_exists( 'mysql_connect' ) ) { + throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); + } + + $this->close(); + $this->mServer = $server; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + + $success = false; + + if ( $this->mFlags & DBO_PERSISTENT ) { + @/**/$this->mConn = mysql_pconnect( $server, $user, $password ); + } else { + # Create a new connection... + @/**/$this->mConn = mysql_connect( $server, $user, $password, true ); + } + + if ( $dbName != '' ) { + if ( $this->mConn !== false ) { + $success = @/**/mysql_select_db( $dbName, $this->mConn ); + if ( !$success ) { + $error = "Error selecting database $dbName on server {$this->mServer} " . + "from client host {$wguname['nodename']}\n"; + wfDebug( $error ); + } + } else { + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); + $success = false; + } + } else { + # Delay USE query + $success = (bool)$this->mConn; + } + + if ( !$success ) { + $this->reportConnectionError(); + } + + global $wgDBmysql5; + if( $wgDBmysql5 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + $this->query( 'SET NAMES utf8' ); + } + + $this->mOpened = $success; + return $success; + } + /**@}}*/ + + /** + * Closes a database connection. + * if it is open : commits any open transactions + * + * @return bool operation success. true if already closed. + */ + function close() + { + $this->mOpened = false; + if ( $this->mConn ) { + if ( $this->trxLevel() ) { + $this->immediateCommit(); + } + return mysql_close( $this->mConn ); + } else { + return true; + } + } + + /** + * @param string $error fallback error message, used if none is given by MySQL + */ + function reportConnectionError( $error = 'Unknown error' ) { + $myError = $this->lastError(); + if ( $myError ) { + $error = $myError; + } + + if ( $this->mFailFunction ) { + # Legacy error handling method + if ( !is_int( $this->mFailFunction ) ) { + $ff = $this->mFailFunction; + $ff( $this, $error ); + } + } else { + # New method + wfLogDBError( "Connection error: $error\n" ); + throw new DBConnectionError( $this, $error ); + } + } + + /** + * Usually aborts on failure + * If errors are explicitly ignored, returns success + */ + function query( $sql, $fname = '', $tempIgnore = false ) { + global $wgProfiling; + + if ( $wgProfiling ) { + # generalizeSQL will probably cut down the query to reasonable + # logging size most of the time. The substr is really just a sanity check. + + # Who's been wasting my precious column space? -- TS + #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + + if ( is_null( $this->getLBInfo( 'master' ) ) ) { + $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'Database::query'; + } else { + $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'Database::query-master'; + } + wfProfileIn( $totalProf ); + wfProfileIn( $queryProf ); + } + + $this->mLastQuery = $sql; + + # Add a comment for easy SHOW PROCESSLIST interpretation + if ( $fname ) { + $commentedSql = preg_replace("/\s/", " /* $fname */ ", $sql, 1); + } else { + $commentedSql = $sql; + } + + # If DBO_TRX is set, start a transaction + if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' + ) { + $this->begin(); + } + + if ( $this->debug() ) { + $sqlx = substr( $commentedSql, 0, 500 ); + $sqlx = strtr( $sqlx, "\t\n", ' ' ); + wfDebug( "SQL: $sqlx\n" ); + } + + # Do the query and handle errors + $ret = $this->doQuery( $commentedSql ); + + # Try reconnecting if the connection was lost + if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) { + # Transaction is gone, like it or not + $this->mTrxLevel = 0; + wfDebug( "Connection lost, reconnecting...\n" ); + if ( $this->ping() ) { + wfDebug( "Reconnected\n" ); + $ret = $this->doQuery( $commentedSql ); + } else { + wfDebug( "Failed\n" ); + } + } + + if ( false === $ret ) { + $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); + } + + if ( $wgProfiling ) { + wfProfileOut( $queryProf ); + wfProfileOut( $totalProf ); + } + return $ret; + } + + /** + * The DBMS-dependent part of query() + * @param string $sql SQL query. + */ + function doQuery( $sql ) { + if( $this->bufferResults() ) { + $ret = mysql_query( $sql, $this->mConn ); + } else { + $ret = mysql_unbuffered_query( $sql, $this->mConn ); + } + return $ret; + } + + /** + * @param $error + * @param $errno + * @param $sql + * @param string $fname + * @param bool $tempIgnore + */ + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + global $wgCommandLineMode, $wgFullyInitialised, $wgColorErrors; + # Ignore errors during error handling to avoid infinite recursion + $ignore = $this->ignoreErrors( true ); + ++$this->mErrorCount; + + if( $ignore || $tempIgnore ) { + wfDebug("SQL ERROR (ignored): $error\n"); + $this->ignoreErrors( $ignore ); + } else { + $sql1line = str_replace( "\n", "\\n", $sql ); + wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n"); + wfDebug("SQL ERROR: " . $error . "\n"); + throw new DBQueryError( $this, $error, $errno, $sql, $fname ); + } + } + + + /** + * Intended to be compatible with the PEAR::DB wrapper functions. + * http://pear.php.net/manual/en/package.database.db.intro-execute.php + * + * ? = scalar value, quoted as necessary + * ! = raw SQL bit (a function for instance) + * & = filename; reads the file and inserts as a blob + * (we don't use this though...) + */ + function prepare( $sql, $func = 'Database::prepare' ) { + /* MySQL doesn't support prepared statements (yet), so just + pack up the query for reference. We'll manually replace + the bits later. */ + return array( 'query' => $sql, 'func' => $func ); + } + + function freePrepared( $prepared ) { + /* No-op for MySQL */ + } + + /** + * Execute a prepared query with the various arguments + * @param string $prepared the prepared sql + * @param mixed $args Either an array here, or put scalars as varargs + */ + function execute( $prepared, $args = null ) { + if( !is_array( $args ) ) { + # Pull the var args + $args = func_get_args(); + array_shift( $args ); + } + $sql = $this->fillPrepared( $prepared['query'], $args ); + return $this->query( $sql, $prepared['func'] ); + } + + /** + * Prepare & execute an SQL statement, quoting and inserting arguments + * in the appropriate places. + * @param string $query + * @param string $args ... + */ + function safeQuery( $query, $args = null ) { + $prepared = $this->prepare( $query, 'Database::safeQuery' ); + if( !is_array( $args ) ) { + # Pull the var args + $args = func_get_args(); + array_shift( $args ); + } + $retval = $this->execute( $prepared, $args ); + $this->freePrepared( $prepared ); + return $retval; + } + + /** + * For faking prepared SQL statements on DBs that don't support + * it directly. + * @param string $preparedSql - a 'preparable' SQL statement + * @param array $args - array of arguments to fill it with + * @return string executable SQL + */ + function fillPrepared( $preparedQuery, $args ) { + reset( $args ); + $this->preparedArgs =& $args; + return preg_replace_callback( '/(\\\\[?!&]|[?!&])/', + array( &$this, 'fillPreparedArg' ), $preparedQuery ); + } + + /** + * preg_callback func for fillPrepared() + * The arguments should be in $this->preparedArgs and must not be touched + * while we're doing this. + * + * @param array $matches + * @return string + * @private + */ + function fillPreparedArg( $matches ) { + switch( $matches[1] ) { + case '\\?': return '?'; + case '\\!': return '!'; + case '\\&': return '&'; + } + list( $n, $arg ) = each( $this->preparedArgs ); + switch( $matches[1] ) { + case '?': return $this->addQuotes( $arg ); + case '!': return $arg; + case '&': + # return $this->addQuotes( file_get_contents( $arg ) ); + throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' ); + default: + throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' ); + } + } + + /**#@+ + * @param mixed $res A SQL result + */ + /** + * Free a result object + */ + function freeResult( $res ) { + if ( !@/**/mysql_free_result( $res ) ) { + throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); + } + } + + /** + * Fetch the next row from the given result object, in object form + */ + function fetchObject( $res ) { + @/**/$row = mysql_fetch_object( $res ); + if( mysql_errno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( mysql_error() ) ); + } + return $row; + } + + /** + * Fetch the next row from the given result object + * Returns an array + */ + function fetchRow( $res ) { + @/**/$row = mysql_fetch_array( $res ); + if (mysql_errno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( mysql_error() ) ); + } + return $row; + } + + /** + * Get the number of rows in a result object + */ + function numRows( $res ) { + @/**/$n = mysql_num_rows( $res ); + if( mysql_errno() ) { + throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( mysql_error() ) ); + } + return $n; + } + + /** + * Get the number of fields in a result object + * See documentation for mysql_num_fields() + */ + function numFields( $res ) { return mysql_num_fields( $res ); } + + /** + * Get a field name in a result object + * See documentation for mysql_field_name(): + * http://www.php.net/mysql_field_name + */ + function fieldName( $res, $n ) { return mysql_field_name( $res, $n ); } + + /** + * Get the inserted value of an auto-increment row + * + * The value inserted should be fetched from nextSequenceValue() + * + * Example: + * $id = $dbw->nextSequenceValue('page_page_id_seq'); + * $dbw->insert('page',array('page_id' => $id)); + * $id = $dbw->insertId(); + */ + function insertId() { return mysql_insert_id( $this->mConn ); } + + /** + * Change the position of the cursor in a result object + * See mysql_data_seek() + */ + function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); } + + /** + * Get the last error number + * See mysql_errno() + */ + function lastErrno() { + if ( $this->mConn ) { + return mysql_errno( $this->mConn ); + } else { + return mysql_errno(); + } + } + + /** + * Get a description of the last error + * See mysql_error() for more details + */ + function lastError() { + if ( $this->mConn ) { + # Even if it's non-zero, it can still be invalid + wfSuppressWarnings(); + $error = mysql_error( $this->mConn ); + if ( !$error ) { + $error = mysql_error(); + } + wfRestoreWarnings(); + } else { + $error = mysql_error(); + } + if( $error ) { + $error .= ' (' . $this->mServer . ')'; + } + return $error; + } + /** + * Get the number of rows affected by the last write query + * See mysql_affected_rows() for more details + */ + function affectedRows() { return mysql_affected_rows( $this->mConn ); } + /**#@-*/ // end of template : @param $result + + /** + * Simple UPDATE wrapper + * Usually aborts on failure + * If errors are explicitly ignored, returns success + * + * This function exists for historical reasons, Database::update() has a more standard + * calling convention and feature set + */ + function set( $table, $var, $value, $cond, $fname = 'Database::set' ) + { + $table = $this->tableName( $table ); + $sql = "UPDATE $table SET $var = '" . + $this->strencode( $value ) . "' WHERE ($cond)"; + return (bool)$this->query( $sql, $fname ); + } + + /** + * Simple SELECT wrapper, returns a single field, input must be encoded + * Usually aborts on failure + * If errors are explicitly ignored, returns FALSE on failure + */ + function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) { + if ( !is_array( $options ) ) { + $options = array( $options ); + } + $options['LIMIT'] = 1; + + $res = $this->select( $table, $var, $cond, $fname, $options ); + if ( $res === false || !$this->numRows( $res ) ) { + return false; + } + $row = $this->fetchRow( $res ); + if ( $row !== false ) { + $this->freeResult( $res ); + return $row[0]; + } else { + return false; + } + } + + /** + * Returns an optional USE INDEX clause to go after the table, and a + * string to go at the end of the query + * + * @private + * + * @param array $options an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array + */ + function makeSelectOptions( $options ) { + $tailOpts = ''; + $startOpts = ''; + + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } + } + + if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + + if (isset($options['LIMIT'])) { + $tailOpts .= $this->limitResult('', $options['LIMIT'], + isset($options['OFFSET']) ? $options['OFFSET'] : false); + } + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + + # Various MySQL extensions + if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY'; + if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT'; + if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT'; + if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT'; + if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS'; + if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE'; + if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE'; + + if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { + $useIndex = $this->useIndexClause( $options['USE INDEX'] ); + } else { + $useIndex = ''; + } + + return array( $startOpts, $useIndex, $tailOpts ); + } + + /** + * SELECT wrapper + */ + function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() ) + { + if( is_array( $vars ) ) { + $vars = implode( ',', $vars ); + } + if( !is_array( $options ) ) { + $options = array( $options ); + } + if( is_array( $table ) ) { + if ( @is_array( $options['USE INDEX'] ) ) + $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] ); + else + $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); + } elseif ($table!='') { + $from = ' FROM ' . $this->tableName( $table ); + } else { + $from = ''; + } + + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $options ); + + if( !empty( $conds ) ) { + if ( is_array( $conds ) ) { + $conds = $this->makeList( $conds, LIST_AND ); + } + $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $tailOpts"; + } else { + $sql = "SELECT $startOpts $vars $from $useIndex $tailOpts"; + } + + return $this->query( $sql, $fname ); + } + + /** + * Single row SELECT wrapper + * Aborts or returns FALSE on error + * + * $vars: the selected variables + * $conds: a condition map, terms are ANDed together. + * Items with numeric keys are taken to be literal conditions + * Takes an array of selected variables, and a condition map, which is ANDed + * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" => + * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where + * $obj- >page_id is the ID of the Astronomy article + * + * @todo migrate documentation to phpdocumentor format + */ + function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) { + $options['LIMIT'] = 1; + $res = $this->select( $table, $vars, $conds, $fname, $options ); + if ( $res === false ) + return false; + if ( !$this->numRows($res) ) { + $this->freeResult($res); + return false; + } + $obj = $this->fetchObject( $res ); + $this->freeResult( $res ); + return $obj; + + } + + /** + * Removes most variables from an SQL query and replaces them with X or N for numbers. + * It's only slightly flawed. Don't use for anything important. + * + * @param string $sql A SQL Query + * @static + */ + static function generalizeSQL( $sql ) { + # This does the same as the regexp below would do, but in such a way + # as to avoid crashing php on some large strings. + # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql); + + $sql = str_replace ( "\\\\", '', $sql); + $sql = str_replace ( "\\'", '', $sql); + $sql = str_replace ( "\\\"", '', $sql); + $sql = preg_replace ("/'.*'/s", "'X'", $sql); + $sql = preg_replace ('/".*"/s', "'X'", $sql); + + # All newlines, tabs, etc replaced by single space + $sql = preg_replace ( "/\s+/", ' ', $sql); + + # All numbers => N + $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql); + + return $sql; + } + + /** + * Determines whether a field exists in a table + * Usually aborts on failure + * If errors are explicitly ignored, returns NULL on failure + */ + function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { + $table = $this->tableName( $table ); + $res = $this->query( 'DESCRIBE '.$table, $fname ); + if ( !$res ) { + return NULL; + } + + $found = false; + + while ( $row = $this->fetchObject( $res ) ) { + if ( $row->Field == $field ) { + $found = true; + break; + } + } + return $found; + } + + /** + * Determines whether an index exists + * Usually aborts on failure + * If errors are explicitly ignored, returns NULL on failure + */ + function indexExists( $table, $index, $fname = 'Database::indexExists' ) { + $info = $this->indexInfo( $table, $index, $fname ); + if ( is_null( $info ) ) { + return NULL; + } else { + return $info !== false; + } + } + + + /** + * Get information about an index into an object + * Returns false if the index does not exist + */ + function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { + # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. + # SHOW INDEX should work for 3.x and up: + # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html + $table = $this->tableName( $table ); + $sql = 'SHOW INDEX FROM '.$table; + $res = $this->query( $sql, $fname ); + if ( !$res ) { + return NULL; + } + + while ( $row = $this->fetchObject( $res ) ) { + if ( $row->Key_name == $index ) { + return $row; + } + } + return false; + } + + /** + * Query whether a given table exists + */ + function tableExists( $table ) { + $table = $this->tableName( $table ); + $old = $this->ignoreErrors( true ); + $res = $this->query( "SELECT 1 FROM $table LIMIT 1" ); + $this->ignoreErrors( $old ); + if( $res ) { + $this->freeResult( $res ); + return true; + } else { + return false; + } + } + + /** + * mysql_fetch_field() wrapper + * Returns false if the field doesn't exist + * + * @param $table + * @param $field + */ + function fieldInfo( $table, $field ) { + $table = $this->tableName( $table ); + $res = $this->query( "SELECT * FROM $table LIMIT 1" ); + $n = mysql_num_fields( $res ); + for( $i = 0; $i < $n; $i++ ) { + $meta = mysql_fetch_field( $res, $i ); + if( $field == $meta->name ) { + return $meta; + } + } + return false; + } + + /** + * mysql_field_type() wrapper + */ + function fieldType( $res, $index ) { + return mysql_field_type( $res, $index ); + } + + /** + * Determines if a given index is unique + */ + function indexUnique( $table, $index ) { + $indexInfo = $this->indexInfo( $table, $index ); + if ( !$indexInfo ) { + return NULL; + } + return !$indexInfo->Non_unique; + } + + /** + * INSERT wrapper, inserts an array into a table + * + * $a may be a single associative array, or an array of these with numeric keys, for + * multi-row insert. + * + * Usually aborts on failure + * If errors are explicitly ignored, returns success + */ + function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + # No rows to insert, easy just return now + if ( !count( $a ) ) { + return true; + } + + $table = $this->tableName( $table ); + if ( !is_array( $options ) ) { + $options = array( $options ); + } + if ( isset( $a[0] ) && is_array( $a[0] ) ) { + $multi = true; + $keys = array_keys( $a[0] ); + } else { + $multi = false; + $keys = array_keys( $a ); + } + + $sql = 'INSERT ' . implode( ' ', $options ) . + " INTO $table (" . implode( ',', $keys ) . ') VALUES '; + + if ( $multi ) { + $first = true; + foreach ( $a as $row ) { + if ( $first ) { + $first = false; + } else { + $sql .= ','; + } + $sql .= '(' . $this->makeList( $row ) . ')'; + } + } else { + $sql .= '(' . $this->makeList( $a ) . ')'; + } + return (bool)$this->query( $sql, $fname ); + } + + /** + * Make UPDATE options for the Database::update function + * + * @private + * @param array $options The options passed to Database::update + * @return string + */ + function makeUpdateOptions( $options ) { + if( !is_array( $options ) ) { + $options = array( $options ); + } + $opts = array(); + if ( in_array( 'LOW_PRIORITY', $options ) ) + $opts[] = $this->lowPriorityOption(); + if ( in_array( 'IGNORE', $options ) ) + $opts[] = 'IGNORE'; + return implode(' ', $opts); + } + + /** + * UPDATE wrapper, takes a condition array and a SET array + * + * @param string $table The table to UPDATE + * @param array $values An array of values to SET + * @param array $conds An array of conditions (WHERE). Use '*' to update all rows. + * @param string $fname The Class::Function calling this function + * (for the log) + * @param array $options An array of UPDATE options, can be one or + * more of IGNORE, LOW_PRIORITY + */ + function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { + $table = $this->tableName( $table ); + $opts = $this->makeUpdateOptions( $options ); + $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); + if ( $conds != '*' ) { + $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); + } + $this->query( $sql, $fname ); + } + + /** + * Makes a wfStrencoded list from an array + * $mode: + * LIST_COMMA - comma separated, no field names + * LIST_AND - ANDed WHERE clause (without the WHERE) + * LIST_OR - ORed WHERE clause (without the WHERE) + * LIST_SET - comma separated with field names, like a SET clause + * LIST_NAMES - comma separated field names + */ + function makeList( $a, $mode = LIST_COMMA ) { + if ( !is_array( $a ) ) { + throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); + } + + $first = true; + $list = ''; + foreach ( $a as $field => $value ) { + if ( !$first ) { + if ( $mode == LIST_AND ) { + $list .= ' AND '; + } elseif($mode == LIST_OR) { + $list .= ' OR '; + } else { + $list .= ','; + } + } else { + $first = false; + } + if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { + $list .= "($value)"; + } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) { + $list .= $field." IN (".$this->makeList($value).") "; + } else { + if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); + } + } + return $list; + } + + /** + * Change the current database + */ + function selectDB( $db ) { + $this->mDBname = $db; + return mysql_select_db( $db, $this->mConn ); + } + + /** + * Format a table name ready for use in constructing an SQL query + * + * This does two important things: it quotes table names which as necessary, + * and it adds a table prefix if there is one. + * + * All functions of this object which require a table name call this function + * themselves. Pass the canonical name to such functions. This is only needed + * when calling query() directly. + * + * @param string $name database table name + */ + function tableName( $name ) { + global $wgSharedDB; + # Skip quoted literals + if ( $name{0} != '`' ) { + if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) { + $name = "{$this->mTablePrefix}$name"; + } + if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) { + $name = "`$wgSharedDB`.`$name`"; + } else { + # Standard quoting + $name = "`$name`"; + } + } + return $name; + } + + /** + * Fetch a number of table names into an array + * This is handy when you need to construct SQL for joins + * + * Example: + * extract($dbr->tableNames('user','watchlist')); + * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user + * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; + */ + function tableNames() { + $inArray = func_get_args(); + $retVal = array(); + foreach ( $inArray as $name ) { + $retVal[$name] = $this->tableName( $name ); + } + return $retVal; + } + + /** + * @private + */ + function tableNamesWithUseIndex( $tables, $use_index ) { + $ret = array(); + + foreach ( $tables as $table ) + if ( @$use_index[$table] !== null ) + $ret[] = $this->tableName( $table ) . ' ' . $this->useIndexClause( implode( ',', (array)$use_index[$table] ) ); + else + $ret[] = $this->tableName( $table ); + + return implode( ',', $ret ); + } + + /** + * Wrapper for addslashes() + * @param string $s String to be slashed. + * @return string slashed string. + */ + function strencode( $s ) { + return mysql_real_escape_string( $s, $this->mConn ); + } + + /** + * If it's a string, adds quotes and backslashes + * Otherwise returns as-is + */ + function addQuotes( $s ) { + if ( is_null( $s ) ) { + return 'NULL'; + } else { + # This will also quote numeric values. This should be harmless, + # and protects against weird problems that occur when they really + # _are_ strings such as article titles and string->number->string + # conversion is not 1:1. + return "'" . $this->strencode( $s ) . "'"; + } + } + + /** + * Escape string for safe LIKE usage + */ + function escapeLike( $s ) { + $s=$this->strencode( $s ); + $s=str_replace(array('%','_'),array('\%','\_'),$s); + return $s; + } + + /** + * Returns an appropriately quoted sequence value for inserting a new row. + * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL + * subclass will return an integer, and save the value for insertId() + */ + function nextSequenceValue( $seqName ) { + return NULL; + } + + /** + * USE INDEX clause + * PostgreSQL doesn't have them and returns "" + */ + function useIndexClause( $index ) { + return "FORCE INDEX ($index)"; + } + + /** + * REPLACE query wrapper + * PostgreSQL simulates this with a DELETE followed by INSERT + * $row is the row to insert, an associative array + * $uniqueIndexes is an array of indexes. Each element may be either a + * field name or an array of field names + * + * It may be more efficient to leave off unique indexes which are unlikely to collide. + * However if you do this, you run the risk of encountering errors which wouldn't have + * occurred in MySQL + * + * @todo migrate comment to phodocumentor format + */ + function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { + $table = $this->tableName( $table ); + + # Single row case + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } + + $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES '; + $first = true; + foreach ( $rows as $row ) { + if ( $first ) { + $first = false; + } else { + $sql .= ','; + } + $sql .= '(' . $this->makeList( $row ) . ')'; + } + return $this->query( $sql, $fname ); + } + + /** + * DELETE where the condition is a join + * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects + * + * For safety, an empty $conds will not delete everything. If you want to delete all rows where the + * join condition matches, set $conds='*' + * + * DO NOT put the join condition in $conds + * + * @param string $delTable The table to delete from. + * @param string $joinTable The other table. + * @param string $delVar The variable to join on, in the first table. + * @param string $joinVar The variable to join on, in the second table. + * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause + */ + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; + if ( $conds != '*' ) { + $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + } + + return $this->query( $sql, $fname ); + } + + /** + * Returns the size of a text field, or -1 for "unlimited" + */ + function textFieldSize( $table, $field ) { + $table = $this->tableName( $table ); + $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";"; + $res = $this->query( $sql, 'Database::textFieldSize' ); + $row = $this->fetchObject( $res ); + $this->freeResult( $res ); + + if ( preg_match( "/\((.*)\)/", $row->Type, $m ) ) { + $size = $m[1]; + } else { + $size = -1; + } + return $size; + } + + /** + * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise + */ + function lowPriorityOption() { + return 'LOW_PRIORITY'; + } + + /** + * DELETE query wrapper + * + * Use $conds == "*" to delete all rows + */ + function delete( $table, $conds, $fname = 'Database::delete' ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' ); + } + $table = $this->tableName( $table ); + $sql = "DELETE FROM $table"; + if ( $conds != '*' ) { + $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + } + return $this->query( $sql, $fname ); + } + + /** + * INSERT SELECT wrapper + * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) + * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() + * $conds may be "*" to copy the whole table + * srcTable may be an array of tables. + */ + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', + $insertOptions = array(), $selectOptions = array() ) + { + $destTable = $this->tableName( $destTable ); + if ( is_array( $insertOptions ) ) { + $insertOptions = implode( ' ', $insertOptions ); + } + if( !is_array( $selectOptions ) ) { + $selectOptions = array( $selectOptions ); + } + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); + if( is_array( $srcTable ) ) { + $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); + } else { + $srcTable = $this->tableName( $srcTable ); + } + $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . + " SELECT $startOpts " . implode( ',', $varMap ) . + " FROM $srcTable $useIndex "; + if ( $conds != '*' ) { + $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + } + $sql .= " $tailOpts"; + return $this->query( $sql, $fname ); + } + + /** + * Construct a LIMIT query with optional offset + * This is used for query pages + * $sql string SQL query we will append the limit too + * $limit integer the SQL limit + * $offset integer the SQL offset (default false) + */ + function limitResult($sql, $limit, $offset=false) { + if( !is_numeric($limit) ) { + throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); + } + return " $sql LIMIT " + . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" ) + . "{$limit} "; + } + function limitResultForUpdate($sql, $num) { + return $this->limitResult($sql, $num, 0); + } + + /** + * Returns an SQL expression for a simple conditional. + * Uses IF on MySQL. + * + * @param string $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false + * @return string SQL fragment + */ + function conditional( $cond, $trueVal, $falseVal ) { + return " IF($cond, $trueVal, $falseVal) "; + } + + /** + * Determines if the last failure was due to a deadlock + */ + function wasDeadlock() { + return $this->lastErrno() == 1213; + } + + /** + * Perform a deadlock-prone transaction. + * + * This function invokes a callback function to perform a set of write + * queries. If a deadlock occurs during the processing, the transaction + * will be rolled back and the callback function will be called again. + * + * Usage: + * $dbw->deadlockLoop( callback, ... ); + * + * Extra arguments are passed through to the specified callback function. + * + * Returns whatever the callback function returned on its successful, + * iteration, or false on error, for example if the retry limit was + * reached. + */ + function deadlockLoop() { + $myFname = 'Database::deadlockLoop'; + + $this->begin(); + $args = func_get_args(); + $function = array_shift( $args ); + $oldIgnore = $this->ignoreErrors( true ); + $tries = DEADLOCK_TRIES; + if ( is_array( $function ) ) { + $fname = $function[0]; + } else { + $fname = $function; + } + do { + $retVal = call_user_func_array( $function, $args ); + $error = $this->lastError(); + $errno = $this->lastErrno(); + $sql = $this->lastQuery(); + + if ( $errno ) { + if ( $this->wasDeadlock() ) { + # Retry + usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) ); + } else { + $this->reportQueryError( $error, $errno, $sql, $fname ); + } + } + } while( $this->wasDeadlock() && --$tries > 0 ); + $this->ignoreErrors( $oldIgnore ); + if ( $tries <= 0 ) { + $this->query( 'ROLLBACK', $myFname ); + $this->reportQueryError( $error, $errno, $sql, $fname ); + return false; + } else { + $this->query( 'COMMIT', $myFname ); + return $retVal; + } + } + + /** + * Do a SELECT MASTER_POS_WAIT() + * + * @param string $file the binlog file + * @param string $pos the binlog position + * @param integer $timeout the maximum number of seconds to wait for synchronisation + */ + function masterPosWait( $file, $pos, $timeout ) { + $fname = 'Database::masterPosWait'; + wfProfileIn( $fname ); + + + # Commit any open transactions + $this->immediateCommit(); + + # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set + $encFile = $this->strencode( $file ); + $sql = "SELECT MASTER_POS_WAIT('$encFile', $pos, $timeout)"; + $res = $this->doQuery( $sql ); + if ( $res && $row = $this->fetchRow( $res ) ) { + $this->freeResult( $res ); + wfProfileOut( $fname ); + return $row[0]; + } else { + wfProfileOut( $fname ); + return false; + } + } + + /** + * Get the position of the master from SHOW SLAVE STATUS + */ + function getSlavePos() { + $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' ); + $row = $this->fetchObject( $res ); + if ( $row ) { + return array( $row->Master_Log_File, $row->Read_Master_Log_Pos ); + } else { + return array( false, false ); + } + } + + /** + * Get the position of the master from SHOW MASTER STATUS + */ + function getMasterPos() { + $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' ); + $row = $this->fetchObject( $res ); + if ( $row ) { + return array( $row->File, $row->Position ); + } else { + return array( false, false ); + } + } + + /** + * Begin a transaction, committing any previously open transaction + */ + function begin( $fname = 'Database::begin' ) { + $this->query( 'BEGIN', $fname ); + $this->mTrxLevel = 1; + } + + /** + * End a transaction + */ + function commit( $fname = 'Database::commit' ) { + $this->query( 'COMMIT', $fname ); + $this->mTrxLevel = 0; + } + + /** + * Rollback a transaction + */ + function rollback( $fname = 'Database::rollback' ) { + $this->query( 'ROLLBACK', $fname ); + $this->mTrxLevel = 0; + } + + /** + * Begin a transaction, committing any previously open transaction + * @deprecated use begin() + */ + function immediateBegin( $fname = 'Database::immediateBegin' ) { + $this->begin(); + } + + /** + * Commit transaction, if one is open + * @deprecated use commit() + */ + function immediateCommit( $fname = 'Database::immediateCommit' ) { + $this->commit(); + } + + /** + * Return MW-style timestamp used for MySQL schema + */ + function timestamp( $ts=0 ) { + return wfTimestamp(TS_MW,$ts); + } + + /** + * Local database timestamp format or null + */ + function timestampOrNull( $ts = null ) { + if( is_null( $ts ) ) { + return null; + } else { + return $this->timestamp( $ts ); + } + } + + /** + * @todo document + */ + function resultObject( $result ) { + if( empty( $result ) ) { + return NULL; + } else { + return new ResultWrapper( $this, $result ); + } + } + + /** + * Return aggregated value alias + */ + function aggregateValue ($valuedata,$valuename='value') { + return $valuename; + } + + /** + * @return string wikitext of a link to the server software's web site + */ + function getSoftwareLink() { + return "[http://www.mysql.com/ MySQL]"; + } + + /** + * @return string Version information from the database + */ + function getServerVersion() { + return mysql_get_server_info(); + } + + /** + * Ping the server and try to reconnect if it there is no connection + */ + function ping() { + if( function_exists( 'mysql_ping' ) ) { + return mysql_ping( $this->mConn ); + } else { + wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" ); + return true; + } + } + + /** + * Get slave lag. + * At the moment, this will only work if the DB user has the PROCESS privilege + */ + function getLag() { + $res = $this->query( 'SHOW PROCESSLIST' ); + # Find slave SQL thread. Assumed to be the second one running, which is a bit + # dubious, but unfortunately there's no easy rigorous way + $slaveThreads = 0; + while ( $row = $this->fetchObject( $res ) ) { + /* This should work for most situations - when default db + * for thread is not specified, it had no events executed, + * and therefore it doesn't know yet how lagged it is. + * + * Relay log I/O thread does not select databases. + */ + if ( $row->User == 'system user' && + $row->State != 'Waiting for master to send event' && + $row->State != 'Connecting to master' && + $row->State != 'Queueing master event to the relay log' && + $row->State != 'Waiting for master update' && + $row->State != 'Requesting binlog dump' + ) { + # This is it, return the time (except -ve) + if ( $row->Time > 0x7fffffff ) { + return false; + } else { + return $row->Time; + } + } + } + return false; + } + + /** + * Get status information from SHOW STATUS in an associative array + */ + function getStatus($which="%") { + $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); + $status = array(); + while ( $row = $this->fetchObject( $res ) ) { + $status[$row->Variable_name] = $row->Value; + } + return $status; + } + + /** + * Return the maximum number of items allowed in a list, or 0 for unlimited. + */ + function maxListLen() { + return 0; + } + + function encodeBlob($b) { + return $b; + } + + function decodeBlob($b) { + return $b; + } + + /** + * Read and execute SQL commands from a file. + * Returns true on success, error string on failure + */ + function sourceFile( $filename ) { + $fp = fopen( $filename, 'r' ); + if ( false === $fp ) { + return "Could not open \"{$fname}\".\n"; + } + + $cmd = ""; + $done = false; + $dollarquote = false; + + while ( ! feof( $fp ) ) { + $line = trim( fgets( $fp, 1024 ) ); + $sl = strlen( $line ) - 1; + + if ( $sl < 0 ) { continue; } + if ( '-' == $line{0} && '-' == $line{1} ) { continue; } + + ## Allow dollar quoting for function declarations + if (substr($line,0,4) == '$mw$') { + if ($dollarquote) { + $dollarquote = false; + $done = true; + } + else { + $dollarquote = true; + } + } + else if (!$dollarquote) { + if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) { + $done = true; + $line = substr( $line, 0, $sl ); + } + } + + if ( '' != $cmd ) { $cmd .= ' '; } + $cmd .= "$line\n"; + + if ( $done ) { + $cmd = str_replace(';;', ";", $cmd); + $cmd = $this->replaceVars( $cmd ); + $res = $this->query( $cmd, 'dbsource', true ); + + if ( false === $res ) { + $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; + } + + $cmd = ''; + $done = false; + } + } + fclose( $fp ); + return true; + } + + /** + * Replace variables in sourced SQL + */ + protected function replaceVars( $ins ) { + $varnames = array( + 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser', + 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword', + 'wgDBadminuser', 'wgDBadminpassword', + ); + + // Ordinary variables + foreach ( $varnames as $var ) { + if( isset( $GLOBALS[$var] ) ) { + $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check? + $ins = str_replace( '{$' . $var . '}', $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/', $val, $ins ); + } + } + + // Table prefixes + $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/', + array( &$this, 'tableNameCallback' ), $ins ); + return $ins; + } + + /** + * Table name callback + * @private + */ + protected function tableNameCallback( $matches ) { + return $this->tableName( $matches[1] ); + } + +} + +/** + * Database abstraction object for mySQL + * Inherit all methods and properties of Database::Database() + * + * @package MediaWiki + * @see Database + */ +class DatabaseMysql extends Database { + # Inherit all +} + + +/** + * Result wrapper for grabbing data queried by someone else + * + * @package MediaWiki + */ +class ResultWrapper { + var $db, $result; + + /** + * @todo document + */ + function ResultWrapper( &$database, $result ) { + $this->db =& $database; + $this->result =& $result; + } + + /** + * @todo document + */ + function numRows() { + return $this->db->numRows( $this->result ); + } + + /** + * @todo document + */ + function fetchObject() { + return $this->db->fetchObject( $this->result ); + } + + /** + * @todo document + */ + function fetchRow() { + return $this->db->fetchRow( $this->result ); + } + + /** + * @todo document + */ + function free() { + $this->db->freeResult( $this->result ); + unset( $this->result ); + unset( $this->db ); + } + + function seek( $row ) { + $this->db->dataSeek( $this->result, $row ); + } + +} + +?> diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php new file mode 100644 index 00000000..74b35a31 --- /dev/null +++ b/includes/DatabaseFunctions.php @@ -0,0 +1,414 @@ +query( $sql, $fname ); + } else { + return false; + } +} + +/** + * + * @param $sql String: SQL query + * @param $dbi + * @param $fname String: name of the php function calling + * @return Array: first row from the database + */ +function wfSingleQuery( $sql, $dbi, $fname = '' ) { + $db =& wfGetDB( $dbi ); + $res = $db->query($sql, $fname ); + $row = $db->fetchRow( $res ); + $ret = $row[0]; + $db->freeResult( $res ); + return $ret; +} + +/* + * @todo document function + */ +function &wfGetDB( $db = DB_LAST, $groups = array() ) { + global $wgLoadBalancer; + $ret =& $wgLoadBalancer->getConnection( $db, true, $groups ); + return $ret; +} + +/** + * Turns on (false) or off (true) the automatic generation and sending + * of a "we're sorry, but there has been a database error" page on + * database errors. Default is on (false). When turned off, the + * code should use wfLastErrno() and wfLastError() to handle the + * situation as appropriate. + * + * @param $newstate + * @param $dbi + * @return Returns the previous state. + */ +function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->ignoreErrors( $newstate ); + } else { + return NULL; + } +} + +/**#@+ + * @param $res Database result handler + * @param $dbi +*/ + +/** + * Free a database result + * @return Bool: whether result is sucessful or not. + */ +function wfFreeResult( $res, $dbi = DB_LAST ) +{ + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + $db->freeResult( $res ); + return true; + } else { + return false; + } +} + +/** + * Get an object from a database result + * @return object|false object we requested + */ +function wfFetchObject( $res, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->fetchObject( $res, $dbi = DB_LAST ); + } else { + return false; + } +} + +/** + * Get a row from a database result + * @return object|false row we requested + */ +function wfFetchRow( $res, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->fetchRow ( $res, $dbi = DB_LAST ); + } else { + return false; + } +} + +/** + * Get a number of rows from a database result + * @return integer|false number of rows + */ +function wfNumRows( $res, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->numRows( $res, $dbi = DB_LAST ); + } else { + return false; + } +} + +/** + * Get the number of fields from a database result + * @return integer|false number of fields + */ +function wfNumFields( $res, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->numFields( $res ); + } else { + return false; + } +} + +/** + * Return name of a field in a result + * @param $res Mixed: Ressource link see Database::fieldName() + * @param $n Integer: id of the field + * @param $dbi Default DB_LAST + * @return string|false name of field + */ +function wfFieldName( $res, $n, $dbi = DB_LAST ) +{ + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->fieldName( $res, $n, $dbi = DB_LAST ); + } else { + return false; + } +} +/**#@-*/ + +/** + * @todo document function + */ +function wfInsertId( $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->insertId(); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfDataSeek( $res, $row, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->dataSeek( $res, $row ); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfLastErrno( $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->lastErrno(); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfLastError( $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->lastError(); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfAffectedRows( $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->affectedRows(); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfLastDBquery( $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->lastQuery(); + } else { + return false; + } +} + +/** + * @see Database::Set() + * @todo document function + * @param $table + * @param $var + * @param $value + * @param $cond + * @param $dbi Default DB_MASTER + */ +function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER ) +{ + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->set( $table, $var, $value, $cond ); + } else { + return false; + } +} + + +/** + * @see Database::selectField() + * @todo document function + * @param $table + * @param $var + * @param $cond Default '' + * @param $dbi Default DB_LAST + */ +function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST ) +{ + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->selectField( $table, $var, $cond ); + } else { + return false; + } +} + +/** + * @see Database::fieldExists() + * @todo document function + * @param $table + * @param $field + * @param $dbi Default DB_LAST + * @return Result of Database::fieldExists() or false. + */ +function wfFieldExists( $table, $field, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->fieldExists( $table, $field ); + } else { + return false; + } +} + +/** + * @see Database::indexExists() + * @todo document function + * @param $table String + * @param $index + * @param $dbi Default DB_LAST + * @return Result of Database::indexExists() or false. + */ +function wfIndexExists( $table, $index, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->indexExists( $table, $index ); + } else { + return false; + } +} + +/** + * @see Database::insert() + * @todo document function + * @param $table String + * @param $array Array + * @param $fname String, default 'wfInsertArray'. + * @param $dbi Default DB_MASTER + * @return result of Database::insert() or false. + */ +function wfInsertArray( $table, $array, $fname = 'wfInsertArray', $dbi = DB_MASTER ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->insert( $table, $array, $fname ); + } else { + return false; + } +} + +/** + * @see Database::getArray() + * @todo document function + * @param $table String + * @param $vars + * @param $conds + * @param $fname String, default 'wfGetArray'. + * @param $dbi Default DB_LAST + * @return result of Database::getArray() or false. + */ +function wfGetArray( $table, $vars, $conds, $fname = 'wfGetArray', $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->getArray( $table, $vars, $conds, $fname ); + } else { + return false; + } +} + +/** + * @see Database::update() + * @param $table String + * @param $values + * @param $conds + * @param $fname String, default 'wfUpdateArray' + * @param $dbi Default DB_MASTER + * @return Result of Database::update()) or false; + * @todo document function + */ +function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi = DB_MASTER ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + $db->update( $table, $values, $conds, $fname ); + return true; + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfTableName( $name, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->tableName( $name ); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfStrencode( $s, $dbi = DB_LAST ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->strencode( $s ); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->nextSequenceValue( $seqName ); + } else { + return false; + } +} + +/** + * @todo document function + */ +function wfUseIndexClause( $index, $dbi = DB_SLAVE ) { + $db =& wfGetDB( $dbi ); + if ( $db !== false ) { + return $db->useIndexClause( $index ); + } else { + return false; + } +} +?> diff --git a/includes/DatabaseMysql.php b/includes/DatabaseMysql.php new file mode 100644 index 00000000..79e917b3 --- /dev/null +++ b/includes/DatabaseMysql.php @@ -0,0 +1,6 @@ + diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php new file mode 100644 index 00000000..d5d7379d --- /dev/null +++ b/includes/DatabaseOracle.php @@ -0,0 +1,692 @@ +mData; + } +}; + +/** + * + * @package MediaWiki + */ +class DatabaseOracle extends Database { + var $mInsertId = NULL; + var $mLastResult = NULL; + var $mFetchCache = array(); + var $mFetchID = array(); + var $mNcols = array(); + var $mFieldNames = array(), $mFieldTypes = array(); + var $mAffectedRows = array(); + var $mErr; + + function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + { + Database::Database( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); + } + + /* static */ function newFromParams( $server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + { + return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); + } + + /** + * Usually aborts on failure + * If the failFunction is set to a non-zero integer, returns success + */ + function open( $server, $user, $password, $dbName ) { + if ( !function_exists( 'oci_connect' ) ) { + throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n" ); + } + $this->close(); + $this->mServer = $server; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + + $success = false; + + $hstring=""; + $this->mConn = oci_new_connect($user, $password, $dbName, "AL32UTF8"); + if ( $this->mConn === false ) { + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " + . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( $this->lastError()."\n" ); + } else { + $this->mOpened = true; + } + return $this->mConn; + } + + /** + * Closes a database connection, if it is open + * Returns success, true if already closed + */ + function close() { + $this->mOpened = false; + if ($this->mConn) { + return oci_close($this->mConn); + } else { + return true; + } + } + + function parseStatement($sql) { + $this->mErr = $this->mLastResult = false; + if (($stmt = oci_parse($this->mConn, $sql)) === false) { + $this->lastError(); + return $this->mLastResult = false; + } + $this->mAffectedRows[$stmt] = 0; + return $this->mLastResult = $stmt; + } + + function doQuery($sql) { + if (($stmt = $this->parseStatement($sql)) === false) + return false; + return $this->executeStatement($stmt); + } + + function executeStatement($stmt) { + if (!oci_execute($stmt, OCI_DEFAULT)) { + $this->lastError(); + oci_free_statement($stmt); + return false; + } + $this->mAffectedRows[$stmt] = oci_num_rows($stmt); + $this->mFetchCache[$stmt] = array(); + $this->mFetchID[$stmt] = 0; + $this->mNcols[$stmt] = oci_num_fields($stmt); + if ($this->mNcols[$stmt] == 0) + return $this->mLastResult; + for ($i = 1; $i <= $this->mNcols[$stmt]; $i++) { + $this->mFieldNames[$stmt][$i] = oci_field_name($stmt, $i); + $this->mFieldTypes[$stmt][$i] = oci_field_type($stmt, $i); + } + while (($o = oci_fetch_array($stmt)) !== false) { + foreach ($o as $key => $value) { + if (is_object($value)) { + $o[$key] = $value->load(); + } + } + $this->mFetchCache[$stmt][] = $o; + } + return $this->mLastResult; + } + + function queryIgnore( $sql, $fname = '' ) { + return $this->query( $sql, $fname, true ); + } + + function freeResult( $res ) { + if (!oci_free_statement($res)) { + throw new DBUnexpectedError( $this, "Unable to free Oracle result\n" ); + } + unset($this->mFetchID[$res]); + unset($this->mFetchCache[$res]); + unset($this->mNcols[$res]); + unset($this->mFieldNames[$res]); + unset($this->mFieldTypes[$res]); + } + + function fetchAssoc($res) { + if ($this->mFetchID[$res] >= count($this->mFetchCache[$res])) + return false; + + for ($i = 1; $i <= $this->mNcols[$res]; $i++) { + $name = $this->mFieldNames[$res][$i]; + $type = $this->mFieldTypes[$res][$i]; + if (isset($this->mFetchCache[$res][$this->mFetchID[$res]][$name])) + $value = $this->mFetchCache[$res][$this->mFetchID[$res]][$name]; + else $value = NULL; + $key = strtolower($name); + wfdebug("'$key' => '$value'\n"); + $ret[$key] = $value; + } + $this->mFetchID[$res]++; + return $ret; + } + + function fetchRow($res) { + $r = $this->fetchAssoc($res); + if (!$r) + return false; + $i = 0; + $ret = array(); + foreach ($r as $key => $value) { + wfdebug("ret[$i]=[$value]\n"); + $ret[$i++] = $value; + } + return $ret; + } + + function fetchObject($res) { + $row = $this->fetchAssoc($res); + if (!$row) + return false; + $ret = new stdClass; + foreach ($row as $key => $value) + $ret->$key = $value; + return $ret; + } + + function numRows($res) { + return count($this->mFetchCache[$res]); + } + function numFields( $res ) { return pg_num_fields( $res ); } + function fieldName( $res, $n ) { return pg_field_name( $res, $n ); } + + /** + * This must be called after nextSequenceVal + */ + function insertId() { + return $this->mInsertId; + } + + function dataSeek($res, $row) { + $this->mFetchID[$res] = $row; + } + + function lastError() { + if ($this->mErr === false) { + if ($this->mLastResult !== false) $what = $this->mLastResult; + else if ($this->mConn !== false) $what = $this->mConn; + else $what = false; + $err = ($what !== false) ? oci_error($what) : oci_error(); + if ($err === false) + $this->mErr = 'no error'; + else + $this->mErr = $err['message']; + } + return str_replace("\n", '
      ', $this->mErr); + } + function lastErrno() { + return 0; + } + + function affectedRows() { + return $this->mAffectedRows[$this->mLastResult]; + } + + /** + * Returns information about an index + * If errors are explicitly ignored, returns NULL on failure + */ + function indexInfo ($table, $index, $fname = 'Database::indexInfo' ) { + $table = $this->tableName($table, true); + if ($index == 'PRIMARY') + $index = "${table}_pk"; + $sql = "SELECT uniqueness FROM all_indexes WHERE table_name='" . + $table . "' AND index_name='" . + $this->strencode(strtoupper($index)) . "'"; + $res = $this->query($sql, $fname); + if (!$res) + return NULL; + if (($row = $this->fetchObject($res)) == NULL) + return false; + $this->freeResult($res); + $row->Non_unique = !$row->uniqueness; + return $row; + } + + function indexUnique ($table, $index, $fname = 'indexUnique') { + if (!($i = $this->indexInfo($table, $index, $fname))) + return $i; + return $i->uniqueness == 'UNIQUE'; + } + + function fieldInfo( $table, $field ) { + $o = new stdClass; + $o->multiple_key = true; /* XXX */ + return $o; + } + + function getColumnInformation($table, $field) { + $table = $this->tableName($table, true); + $field = strtoupper($field); + + $res = $this->doQuery("SELECT * FROM all_tab_columns " . + "WHERE table_name='".$table."' " . + "AND column_name='".$field."'"); + if (!$res) + return false; + $o = $this->fetchObject($res); + $this->freeResult($res); + return $o; + } + + function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { + $column = $this->getColumnInformation($table, $field); + if (!$column) + return false; + return true; + } + + function tableName($name, $forddl = false) { + # First run any transformations from the parent object + $name = parent::tableName( $name ); + + # Replace backticks into empty + # Note: "foo" and foo are not the same in Oracle! + $name = str_replace('`', '', $name); + + # Now quote Oracle reserved keywords + switch( $name ) { + case 'user': + case 'group': + case 'validate': + if ($forddl) + return $name; + else + return '"' . $name . '"'; + + default: + return strtoupper($name); + } + } + + function strencode( $s ) { + return str_replace("'", "''", $s); + } + + /** + * Return the next in a sequence, save the value for retrieval via insertId() + */ + function nextSequenceValue( $seqName ) { + $r = $this->doQuery("SELECT $seqName.nextval AS val FROM dual"); + $o = $this->fetchObject($r); + $this->freeResult($r); + return $this->mInsertId = (int)$o->val; + } + + /** + * USE INDEX clause + * PostgreSQL doesn't have them and returns "" + */ + function useIndexClause( $index ) { + return ''; + } + + # REPLACE query wrapper + # PostgreSQL simulates this with a DELETE followed by INSERT + # $row is the row to insert, an associative array + # $uniqueIndexes is an array of indexes. Each element may be either a + # field name or an array of field names + # + # It may be more efficient to leave off unique indexes which are unlikely to collide. + # However if you do this, you run the risk of encountering errors which wouldn't have + # occurred in MySQL + function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { + $table = $this->tableName( $table ); + + if (count($rows)==0) { + return; + } + + # Single row case + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } + + foreach( $rows as $row ) { + # Delete rows which collide + if ( $uniqueIndexes ) { + $sql = "DELETE FROM $table WHERE "; + $first = true; + foreach ( $uniqueIndexes as $index ) { + if ( $first ) { + $first = false; + $sql .= "("; + } else { + $sql .= ') OR ('; + } + if ( is_array( $index ) ) { + $first2 = true; + foreach ( $index as $col ) { + if ( $first2 ) { + $first2 = false; + } else { + $sql .= ' AND '; + } + $sql .= $col.'=' . $this->addQuotes( $row[$col] ); + } + } else { + $sql .= $index.'=' . $this->addQuotes( $row[$index] ); + } + } + $sql .= ')'; + $this->query( $sql, $fname ); + } + + # Now insert the row + $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . + $this->makeList( $row, LIST_COMMA ) . ')'; + $this->query( $sql, $fname ); + } + } + + # DELETE where the condition is a join + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; + if ( $conds != '*' ) { + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); + } + $sql .= ')'; + + $this->query( $sql, $fname ); + } + + # Returns the size of a text field, or -1 for "unlimited" + function textFieldSize( $table, $field ) { + $table = $this->tableName( $table ); + $sql = "SELECT t.typname as ftype,a.atttypmod as size + FROM pg_class c, pg_attribute a, pg_type t + WHERE relname='$table' AND a.attrelid=c.oid AND + a.atttypid=t.oid and a.attname='$field'"; + $res =$this->query($sql); + $row=$this->fetchObject($res); + if ($row->ftype=="varchar") { + $size=$row->size-4; + } else { + $size=$row->size; + } + $this->freeResult( $res ); + return $size; + } + + function lowPriorityOption() { + return ''; + } + + function limitResult($sql, $limit, $offset) { + $ret = "SELECT * FROM ($sql) WHERE ROWNUM < " . ((int)$limit + (int)($offset+1)); + if (is_numeric($offset)) + $ret .= " AND ROWNUM >= " . (int)$offset; + return $ret; + } + function limitResultForUpdate($sql, $limit) { + return $sql; + } + /** + * Returns an SQL expression for a simple conditional. + * Uses CASE on PostgreSQL. + * + * @param string $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false + * @return string SQL fragment + */ + function conditional( $cond, $trueVal, $falseVal ) { + return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; + } + + # FIXME: actually detecting deadlocks might be nice + function wasDeadlock() { + return false; + } + + # Return DB-style timestamp used for MySQL schema + function timestamp($ts = 0) { + return $this->strencode(wfTimestamp(TS_ORACLE, $ts)); +# return "TO_TIMESTAMP('" . $this->strencode(wfTimestamp(TS_DB, $ts)) . "', 'RRRR-MM-DD HH24:MI:SS')"; + } + + /** + * Return aggregated value function call + */ + function aggregateValue ($valuedata,$valuename='value') { + return $valuedata; + } + + + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + throw new DBUnexpectedError($this, $message); + } + + /** + * @return string wikitext of a link to the server software's web site + */ + function getSoftwareLink() { + return "[http://www.oracle.com/ Oracle]"; + } + + /** + * @return string Version information from the database + */ + function getServerVersion() { + return oci_server_version($this->mConn); + } + + function setSchema($schema=false) { + $schemas=$this->mSchemas; + if ($schema) { array_unshift($schemas,$schema); } + $searchpath=$this->makeList($schemas,LIST_NAMES); + $this->query("SET search_path = $searchpath"); + } + + function begin() { + } + + function immediateCommit( $fname = 'Database::immediateCommit' ) { + oci_commit($this->mConn); + $this->mTrxLevel = 0; + } + function rollback( $fname = 'Database::rollback' ) { + oci_rollback($this->mConn); + $this->mTrxLevel = 0; + } + function getLag() { + return false; + } + function getStatus($which=null) { + $result = array('Threads_running' => 0, 'Threads_connected' => 0); + return $result; + } + + /** + * Returns an optional USE INDEX clause to go after the table, and a + * string to go at the end of the query + * + * @access private + * + * @param array $options an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array + */ + function makeSelectOptions($options) { + $tailOpts = ''; + + if (isset( $options['ORDER BY'])) { + $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + } + + return array('', $tailOpts); + } + + function maxListLen() { + return 1000; + } + + /** + * Query whether a given table exists + */ + function tableExists( $table ) { + $table = $this->tableName($table, true); + $res = $this->query( "SELECT COUNT(*) as NUM FROM user_tables WHERE table_name='" + . $table . "'" ); + if (!$res) + return false; + $row = $this->fetchObject($res); + $this->freeResult($res); + return $row->num >= 1; + } + + /** + * UPDATE wrapper, takes a condition array and a SET array + */ + function update( $table, $values, $conds, $fname = 'Database::update' ) { + $table = $this->tableName( $table ); + + $sql = "UPDATE $table SET "; + $first = true; + foreach ($values as $field => $v) { + if ($first) + $first = false; + else + $sql .= ", "; + $sql .= "$field = :n$field "; + } + if ( $conds != '*' ) { + $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); + } + $stmt = $this->parseStatement($sql); + if ($stmt === false) { + $this->reportQueryError( $this->lastError(), $this->lastErrno(), $stmt ); + return false; + } + if ($this->debug()) + wfDebug("SQL: $sql\n"); + $s = ''; + foreach ($values as $field => $v) { + oci_bind_by_name($stmt, ":n$field", $values[$field]); + if ($this->debug()) + $s .= " [$field] = [$v]\n"; + } + if ($this->debug()) + wfdebug(" PH: $s\n"); + $ret = $this->executeStatement($stmt); + return $ret; + } + + /** + * INSERT wrapper, inserts an array into a table + * + * $a may be a single associative array, or an array of these with numeric keys, for + * multi-row insert. + * + * Usually aborts on failure + * If errors are explicitly ignored, returns success + */ + function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + # No rows to insert, easy just return now + if ( !count( $a ) ) { + return true; + } + + $table = $this->tableName( $table ); + if (!is_array($options)) + $options = array($options); + + $oldIgnore = false; + if (in_array('IGNORE', $options)) + $oldIgnore = $this->ignoreErrors( true ); + + if ( isset( $a[0] ) && is_array( $a[0] ) ) { + $multi = true; + $keys = array_keys( $a[0] ); + } else { + $multi = false; + $keys = array_keys( $a ); + } + + $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ('; + $return = ''; + $first = true; + foreach ($a as $key => $value) { + if ($first) + $first = false; + else + $sql .= ", "; + if (is_object($value) && $value->isLOB()) { + $sql .= "EMPTY_BLOB()"; + $return = "RETURNING $key INTO :bobj"; + } else + $sql .= ":$key"; + } + $sql .= ") $return"; + + if ($this->debug()) { + wfDebug("SQL: $sql\n"); + } + + if (($stmt = $this->parseStatement($sql)) === false) { + $this->reportQueryError($this->lastError(), $this->lastErrno(), $sql, $fname); + $this->ignoreErrors($oldIgnore); + return false; + } + + /* + * If we're inserting multiple rows, parse the statement once and + * execute it for each set of values. Otherwise, convert it into an + * array and pretend. + */ + if (!$multi) + $a = array($a); + + foreach ($a as $key => $row) { + $blob = false; + $bdata = false; + $s = ''; + foreach ($row as $k => $value) { + if (is_object($value) && $value->isLOB()) { + $blob = oci_new_descriptor($this->mConn, OCI_D_LOB); + $bdata = $value->data(); + oci_bind_by_name($stmt, ":bobj", $blob, -1, OCI_B_BLOB); + } else + oci_bind_by_name($stmt, ":$k", $a[$key][$k], -1); + if ($this->debug()) + $s .= " [$k] = {$row[$k]}"; + } + if ($this->debug()) + wfDebug(" PH: $s\n"); + if (($s = $this->executeStatement($stmt)) === false) { + $this->reportQueryError($this->lastError(), $this->lastErrno(), $sql, $fname); + $this->ignoreErrors($oldIgnore); + return false; + } + + if ($blob) { + $blob->save($bdata); + } + } + $this->ignoreErrors($oldIgnore); + return $this->mLastResult = $s; + } + + function ping() { + return true; + } + + function encodeBlob($b) { + return new OracleBlob($b); + } +} + +?> diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php new file mode 100644 index 00000000..5897386f --- /dev/null +++ b/includes/DatabasePostgres.php @@ -0,0 +1,609 @@ +mOut =& $wgOut; + $this->mFailFunction = $failFunction; + $this->mFlags = $flags; + + $this->open( $server, $user, $password, $dbName); + + } + + static function newFromParams( $server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0) + { + return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags ); + } + + /** + * Usually aborts on failure + * If the failFunction is set to a non-zero integer, returns success + */ + function open( $server, $user, $password, $dbName ) { + # Test for PostgreSQL support, to avoid suppressed fatal error + if ( !function_exists( 'pg_connect' ) ) { + throw new DBConnectionError( $this, "PostgreSQL functions missing, have you compiled PHP with the --with-pgsql option?\n" ); + } + + global $wgDBport; + + $this->close(); + $this->mServer = $server; + $port = $wgDBport; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + + $success = false; + + $hstring=""; + if ($server!=false && $server!="") { + $hstring="host=$server "; + } + if ($port!=false && $port!="") { + $hstring .= "port=$port "; + } + + error_reporting( E_ALL ); + + @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password"); + + if ( $this->mConn == false ) { + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( $this->lastError()."\n" ); + return false; + } + + $this->mOpened = true; + ## If this is the initial connection, setup the schema stuff + if (defined('MEDIAWIKI_INSTALL') and !defined('POSTGRES_SEARCHPATH')) { + global $wgDBmwschema, $wgDBts2schema, $wgDBname; + + ## Do we have the basic tsearch2 table? + print "
    • Checking for tsearch2 ..."; + if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) { + print "FAILED. Make sure tsearch2 is installed. See this article"; + print " for instructions.
    • \n"; + dieout("
    "); + } + print "OK\n"; + + ## Do we have plpgsql installed? + print "
  • Checking for plpgsql ..."; + $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'"; + $res = $this->doQuery($SQL); + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows < 1) { + print "FAILED. Make sure the language plpgsql is installed for the database $wgDBnamet
  • "; + ## XXX Better help + dieout(""); + } + print "OK\n"; + + ## Does the schema already exist? Who owns it? + $result = $this->schemaExists($wgDBmwschema); + if (!$result) { + print "
  • Creating schema $wgDBmwschema ..."; + $result = $this->doQuery("CREATE SCHEMA $wgDBmwschema"); + if (!$result) { + print "FAILED.
  • \n"; + return false; + } + print "ok\n"; + } + else if ($result != $user) { + print "
  • Schema $wgDBmwschema exists but is not owned by $user. Not ideal.
  • \n"; + } + else { + print "
  • Schema $wgDBmwschema exists and is owned by $user ($result). Excellent.
  • \n"; + } + + ## Fix up the search paths if needed + print "
  • Setting the search path for user $user ..."; + $path = "$wgDBmwschema"; + if ($wgDBts2schema !== $wgDBmwschema) + $path .= ", $wgDBts2schema"; + if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public') + $path .= ", public"; + $SQL = "ALTER USER $user SET search_path = $path"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "FAILED.
  • \n"; + return false; + } + print "ok\n"; + ## Set for the rest of this session + $SQL = "SET search_path = $path"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "
  • Failed to set search_path
  • \n"; + return false; + } + define( "POSTGRES_SEARCHPATH", $path ); + } + + return $this->mConn; + } + + /** + * Closes a database connection, if it is open + * Returns success, true if already closed + */ + function close() { + $this->mOpened = false; + if ( $this->mConn ) { + return pg_close( $this->mConn ); + } else { + return true; + } + } + + function doQuery( $sql ) { + return $this->mLastResult=pg_query( $this->mConn , $sql); + } + + function queryIgnore( $sql, $fname = '' ) { + return $this->query( $sql, $fname, true ); + } + + function freeResult( $res ) { + if ( !@pg_free_result( $res ) ) { + throw new DBUnexpectedError($this, "Unable to free PostgreSQL result\n" ); + } + } + + function fetchObject( $res ) { + @$row = pg_fetch_object( $res ); + # FIXME: HACK HACK HACK HACK debug + + # TODO: + # hashar : not sure if the following test really trigger if the object + # fetching failled. + if( pg_last_error($this->mConn) ) { + throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + } + return $row; + } + + function fetchRow( $res ) { + @$row = pg_fetch_array( $res ); + if( pg_last_error($this->mConn) ) { + throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + } + return $row; + } + + function numRows( $res ) { + @$n = pg_num_rows( $res ); + if( pg_last_error($this->mConn) ) { + throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + } + return $n; + } + function numFields( $res ) { return pg_num_fields( $res ); } + function fieldName( $res, $n ) { return pg_field_name( $res, $n ); } + + /** + * This must be called after nextSequenceVal + */ + function insertId() { + return $this->mInsertId; + } + + function dataSeek( $res, $row ) { return pg_result_seek( $res, $row ); } + function lastError() { + if ( $this->mConn ) { + return pg_last_error(); + } + else { + return "No database connection"; + } + } + function lastErrno() { return 1; } + + function affectedRows() { + return pg_affected_rows( $this->mLastResult ); + } + + /** + * Returns information about an index + * If errors are explicitly ignored, returns NULL on failure + */ + function indexInfo( $table, $index, $fname = 'Database::indexExists' ) { + $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; + $res = $this->query( $sql, $fname ); + if ( !$res ) { + return NULL; + } + + while ( $row = $this->fetchObject( $res ) ) { + if ( $row->indexname == $index ) { + return $row; + } + } + return false; + } + + function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { + $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'". + " AND indexdef LIKE 'CREATE UNIQUE%({$index})'"; + $res = $this->query( $sql, $fname ); + if ( !$res ) + return NULL; + while ($row = $this->fetchObject( $res )) + return true; + return false; + + } + + function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + # PostgreSQL doesn't support options + # We have a go at faking one of them + # TODO: DELAYED, LOW_PRIORITY + + if ( !is_array($options)) + $options = array($options); + + if ( in_array( 'IGNORE', $options ) ) + $oldIgnore = $this->ignoreErrors( true ); + + # IGNORE is performed using single-row inserts, ignoring errors in each + # FIXME: need some way to distiguish between key collision and other types of error + $oldIgnore = $this->ignoreErrors( true ); + if ( !is_array( reset( $a ) ) ) { + $a = array( $a ); + } + foreach ( $a as $row ) { + parent::insert( $table, $row, $fname, array() ); + } + $this->ignoreErrors( $oldIgnore ); + $retVal = true; + + if ( in_array( 'IGNORE', $options ) ) + $this->ignoreErrors( $oldIgnore ); + + return $retVal; + } + + function tableName( $name ) { + # Replace backticks into double quotes + $name = strtr($name,'`','"'); + + # Now quote PG reserved keywords + switch( $name ) { + case 'user': + case 'old': + case 'group': + return '"' . $name . '"'; + + default: + return $name; + } + } + + /** + * Return the next in a sequence, save the value for retrieval via insertId() + */ + function nextSequenceValue( $seqName ) { + $safeseq = preg_replace( "/'/", "''", $seqName ); + $res = $this->query( "SELECT nextval('$safeseq')" ); + $row = $this->fetchRow( $res ); + $this->mInsertId = $row[0]; + $this->freeResult( $res ); + return $this->mInsertId; + } + + /** + * USE INDEX clause + * PostgreSQL doesn't have them and returns "" + */ + function useIndexClause( $index ) { + return ''; + } + + # REPLACE query wrapper + # PostgreSQL simulates this with a DELETE followed by INSERT + # $row is the row to insert, an associative array + # $uniqueIndexes is an array of indexes. Each element may be either a + # field name or an array of field names + # + # It may be more efficient to leave off unique indexes which are unlikely to collide. + # However if you do this, you run the risk of encountering errors which wouldn't have + # occurred in MySQL + function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { + $table = $this->tableName( $table ); + + if (count($rows)==0) { + return; + } + + # Single row case + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } + + foreach( $rows as $row ) { + # Delete rows which collide + if ( $uniqueIndexes ) { + $sql = "DELETE FROM $table WHERE "; + $first = true; + foreach ( $uniqueIndexes as $index ) { + if ( $first ) { + $first = false; + $sql .= "("; + } else { + $sql .= ') OR ('; + } + if ( is_array( $index ) ) { + $first2 = true; + foreach ( $index as $col ) { + if ( $first2 ) { + $first2 = false; + } else { + $sql .= ' AND '; + } + $sql .= $col.'=' . $this->addQuotes( $row[$col] ); + } + } else { + $sql .= $index.'=' . $this->addQuotes( $row[$index] ); + } + } + $sql .= ')'; + $this->query( $sql, $fname ); + } + + # Now insert the row + $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . + $this->makeList( $row, LIST_COMMA ) . ')'; + $this->query( $sql, $fname ); + } + } + + # DELETE where the condition is a join + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) { + if ( !$conds ) { + throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; + if ( $conds != '*' ) { + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); + } + $sql .= ')'; + + $this->query( $sql, $fname ); + } + + # Returns the size of a text field, or -1 for "unlimited" + function textFieldSize( $table, $field ) { + $table = $this->tableName( $table ); + $sql = "SELECT t.typname as ftype,a.atttypmod as size + FROM pg_class c, pg_attribute a, pg_type t + WHERE relname='$table' AND a.attrelid=c.oid AND + a.atttypid=t.oid and a.attname='$field'"; + $res =$this->query($sql); + $row=$this->fetchObject($res); + if ($row->ftype=="varchar") { + $size=$row->size-4; + } else { + $size=$row->size; + } + $this->freeResult( $res ); + return $size; + } + + function lowPriorityOption() { + return ''; + } + + function limitResult($sql, $limit,$offset) { + return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":""); + } + + /** + * Returns an SQL expression for a simple conditional. + * Uses CASE on PostgreSQL. + * + * @param string $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false + * @return string SQL fragment + */ + function conditional( $cond, $trueVal, $falseVal ) { + return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; + } + + # FIXME: actually detecting deadlocks might be nice + function wasDeadlock() { + return false; + } + + # Return DB-style timestamp used for MySQL schema + function timestamp( $ts=0 ) { + return wfTimestamp(TS_DB,$ts); + } + + /** + * Return aggregated value function call + */ + function aggregateValue ($valuedata,$valuename='value') { + return $valuedata; + } + + + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + throw new DBUnexpectedError($this, $message); + } + + /** + * @return string wikitext of a link to the server software's web site + */ + function getSoftwareLink() { + return "[http://www.postgresql.org/ PostgreSQL]"; + } + + /** + * @return string Version information from the database + */ + function getServerVersion() { + $res = $this->query( "SELECT version()" ); + $row = $this->fetchRow( $res ); + $version = $row[0]; + $this->freeResult( $res ); + return $version; + } + + + /** + * Query whether a given table exists (in the given schema, or the default mw one if not given) + */ + function tableExists( $table, $schema = false ) { + global $wgDBmwschema; + if (! $schema ) + $schema = $wgDBmwschema; + $etable = preg_replace("/'/", "''", $table); + $eschema = preg_replace("/'/", "''", $schema); + $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " + . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema'"; + $res = $this->query( $SQL ); + $count = $res ? pg_num_rows($res) : 0; + if ($res) + $this->freeResult( $res ); + return $count; + } + + + /** + * Query whether a given schema exists. Returns the name of the owner + */ + function schemaExists( $schema ) { + $eschema = preg_replace("/'/", "''", $schema); + $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r " + ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'"; + $res = $this->query( $SQL ); + $owner = $res ? pg_num_rows($res) ? pg_fetch_result($res, 0, 0) : false : false; + if ($res) + $this->freeResult($res); + return $owner; + } + + /** + * Query whether a given column exists in the mediawiki schema + */ + function fieldExists( $table, $field ) { + global $wgDBmwschema; + $etable = preg_replace("/'/", "''", $table); + $eschema = preg_replace("/'/", "''", $wgDBmwschema); + $ecol = preg_replace("/'/", "''", $field); + $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a " + . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' " + . "AND a.attrelid = c.oid AND a.attname = '$ecol'"; + $res = $this->query( $SQL ); + $count = $res ? pg_num_rows($res) : 0; + if ($res) + $this->freeResult( $res ); + return $count; + } + + function fieldInfo( $table, $field ) { + $res = $this->query( "SELECT $field FROM $table LIMIT 1" ); + $type = pg_field_type( $res, 0 ); + return $type; + } + + function begin( $fname = 'DatabasePostgrs::begin' ) { + $this->query( 'BEGIN', $fname ); + $this->mTrxLevel = 1; + } + function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) { + return true; + } + function commit( $fname = 'DatabasePostgres::commit' ) { + $this->query( 'COMMIT', $fname ); + $this->mTrxLevel = 0; + } + + /* Not even sure why this is used in the main codebase... */ + function limitResultForUpdate($sql, $num) { + return $sql; + } + + function update_interwiki() { + ## Avoid the non-standard "REPLACE INTO" syntax + ## Called by config/index.php + $f = fopen( "../maintenance/interwiki.sql", 'r' ); + if ($f == false ) { + dieout( "
  • Could not find the interwiki.sql file"); + } + ## We simply assume it is already empty as we have just created it + $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; + while ( ! feof( $f ) ) { + $line = fgets($f,1024); + if (!preg_match("/^\s*(\(.+?),(\d)\)/", $line, $matches)) { + continue; + } + $yesno = $matches[2]; ## ? "'true'" : "'false'"; + $this->query("$SQL $matches[1],$matches[2])"); + } + print " (table interwiki successfully populated)...\n"; + } + + function encodeBlob($b) { + return array('bytea',pg_escape_bytea($b)); + } + function decodeBlob($b) { + return pg_unescape_bytea( $b ); + } + + function strencode( $s ) { ## Should not be called by us + return pg_escape_string( $s ); + } + + function addQuotes( $s ) { + if ( is_null( $s ) ) { + return 'NULL'; + } else if (is_array( $s )) { ## Assume it is bytea data + return "E'$s[1]'"; + } + return "'" . pg_escape_string($s) . "'"; + return "E'" . pg_escape_string($s) . "'"; + } + +} + +?> diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php new file mode 100644 index 00000000..02acac73 --- /dev/null +++ b/includes/DateFormatter.php @@ -0,0 +1,288 @@ +monthNames = $this->getMonthRegex(); + for ( $i=1; $i<=12; $i++ ) { + $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i; + $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i; + } + + $this->regexTrail = '(?![a-z])/iu'; + + # Partial regular expressions + $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]'; + $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]'; + $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]'; + $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]'; + $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]'; + + # Real regular expressions + $this->regexes[DF_DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[DF_YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}"; + $this->regexes[DF_MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[DF_YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}"; + $this->regexes[DF_DM] = "/{$this->prxDM}{$this->regexTrail}"; + $this->regexes[DF_MD] = "/{$this->prxMD}{$this->regexTrail}"; + $this->regexes[DF_ISO1] = "/{$this->prxISO1}{$this->regexTrail}"; + $this->regexes[DF_ISO2] = "/{$this->prxISO2}{$this->regexTrail}"; + + # Extraction keys + # See the comments in replace() for the meaning of the letters + $this->keys[DF_DMY] = 'jFY'; + $this->keys[DF_YDM] = 'Y jF'; + $this->keys[DF_MDY] = 'FjY'; + $this->keys[DF_YMD] = 'Y Fj'; + $this->keys[DF_DM] = 'jF'; + $this->keys[DF_MD] = 'Fj'; + $this->keys[DF_ISO1] = 'ymd'; # y means ISO year + $this->keys[DF_ISO2] = 'ymd'; + + # Target date formats + $this->targets[DF_DMY] = '[[F j|j F]] [[Y]]'; + $this->targets[DF_YDM] = '[[Y]], [[F j|j F]]'; + $this->targets[DF_MDY] = '[[F j]], [[Y]]'; + $this->targets[DF_YMD] = '[[Y]] [[F j]]'; + $this->targets[DF_DM] = '[[F j|j F]]'; + $this->targets[DF_MD] = '[[F j]]'; + $this->targets[DF_ISO1] = '[[Y|y]]-[[F j|m-d]]'; + $this->targets[DF_ISO2] = '[[y-m-d]]'; + + # Rules + # pref source target + $this->rules[DF_DMY][DF_MD] = DF_DM; + $this->rules[DF_ALL][DF_MD] = DF_MD; + $this->rules[DF_MDY][DF_DM] = DF_MD; + $this->rules[DF_ALL][DF_DM] = DF_DM; + $this->rules[DF_NONE][DF_ISO2] = DF_ISO1; + } + + /** + * @static + */ + function &getInstance() { + global $wgDBname, $wgMemc; + static $dateFormatter = false; + if ( !$dateFormatter ) { + $dateFormatter = $wgMemc->get( "$wgDBname:dateformatter" ); + if ( !$dateFormatter ) { + $dateFormatter = new DateFormatter; + $wgMemc->set( "$wgDBname:dateformatter", $dateFormatter, 3600 ); + } + } + return $dateFormatter; + } + + /** + * @param $preference + * @param $text + */ + function reformat( $preference, $text ) { + if ($preference == 'ISO 8601') $preference = 4; # The ISO 8601 option used to be 4 + for ( $i=1; $i<=DF_LAST; $i++ ) { + $this->mSource = $i; + if ( @$this->rules[$preference][$i] ) { + # Specific rules + $this->mTarget = $this->rules[$preference][$i]; + } elseif ( @$this->rules[DF_ALL][$i] ) { + # General rules + $this->mTarget = $this->rules[DF_ALL][$i]; + } elseif ( $preference ) { + # User preference + $this->mTarget = $preference; + } else { + # Default + $this->mTarget = $i; + } + $text = preg_replace_callback( $this->regexes[$i], 'wfMainDateReplace', $text ); + } + return $text; + } + + /** + * @param $matches + */ + function replace( $matches ) { + # Extract information from $matches + $bits = array(); + $key = $this->keys[$this->mSource]; + for ( $p=0; $p < strlen($key); $p++ ) { + if ( $key{$p} != ' ' ) { + $bits[$key{$p}] = $matches[$p+1]; + } + } + + $format = $this->targets[$this->mTarget]; + + # Construct new date + $text = ''; + $fail = false; + + for ( $p=0; $p < strlen( $format ); $p++ ) { + $char = $format{$p}; + switch ( $char ) { + case 'd': # ISO day of month + if ( !isset($bits['d']) ) { + $text .= sprintf( '%02d', $bits['j'] ); + } else { + $text .= $bits['d']; + } + break; + case 'm': # ISO month + if ( !isset($bits['m']) ) { + $m = $this->makeIsoMonth( $bits['F'] ); + if ( !$m || $m == '00' ) { + $fail = true; + } else { + $text .= $m; + } + } else { + $text .= $bits['m']; + } + break; + case 'y': # ISO year + if ( !isset( $bits['y'] ) ) { + $text .= $this->makeIsoYear( $bits['Y'] ); + } else { + $text .= $bits['y']; + } + break; + case 'j': # ordinary day of month + if ( !isset($bits['j']) ) { + $text .= intval( $bits['d'] ); + } else { + $text .= $bits['j']; + } + break; + case 'F': # long month + if ( !isset( $bits['F'] ) ) { + $m = intval($bits['m']); + if ( $m > 12 || $m < 1 ) { + $fail = true; + } else { + global $wgContLang; + $text .= $wgContLang->getMonthName( $m ); + } + } else { + $text .= ucfirst( $bits['F'] ); + } + break; + case 'Y': # ordinary (optional BC) year + if ( !isset( $bits['Y'] ) ) { + $text .= $this->makeNormalYear( $bits['y'] ); + } else { + $text .= $bits['Y']; + } + break; + default: + $text .= $char; + } + } + if ( $fail ) { + $text = $matches[0]; + } + return $text; + } + + /** + * @todo document + */ + function getMonthRegex() { + global $wgContLang; + $names = array(); + for( $i = 1; $i <= 12; $i++ ) { + $names[] = $wgContLang->getMonthName( $i ); + $names[] = $wgContLang->getMonthAbbreviation( $i ); + } + return implode( '|', $names ); + } + + /** + * Makes an ISO month, e.g. 02, from a month name + * @param $monthName String: month name + * @return string ISO month name + */ + function makeIsoMonth( $monthName ) { + global $wgContLang; + + $n = $this->xMonths[$wgContLang->lc( $monthName )]; + return sprintf( '%02d', $n ); + } + + /** + * @todo document + * @param $year String: Year name + * @return string ISO year name + */ + function makeIsoYear( $year ) { + # Assumes the year is in a nice format, as enforced by the regex + if ( substr( $year, -2 ) == 'BC' ) { + $num = intval(substr( $year, 0, -3 )) - 1; + # PHP bug note: sprintf( "%04d", -1 ) fails poorly + $text = sprintf( '-%04d', $num ); + + } else { + $text = sprintf( '%04d', $year ); + } + return $text; + } + + /** + * @todo document + */ + function makeNormalYear( $iso ) { + if ( $iso{0} == '-' ) { + $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC'; + } else { + $text = intval( $iso ); + } + return $text; + } +} + +/** + * @todo document + */ +function wfMainDateReplace( $matches ) { + $df =& DateFormatter::getInstance(); + return $df->replace( $matches ); +} + +?> diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php new file mode 100644 index 00000000..1964aaf2 --- /dev/null +++ b/includes/DefaultSettings.php @@ -0,0 +1,2189 @@ + array ( + 'command' => "clamscan --no-summary ", + + 'codemap'=> array ( + "0"=> AV_NO_VIRUS, #no virus + "1"=> AV_VIRUS_FOUND, #virus found + "52"=> AV_SCAN_ABORTED, #unsupported file format (probably imune) + "*"=> AV_SCAN_FAILED, #else scan failed + ), + + 'messagepattern'=> '/.*?:(.*)/sim', + ), + + #setup for f-prot + 'f-prot' => array ( + 'command' => "f-prot ", + + 'codemap'=> array ( + "0"=> AV_NO_VIRUS, #no virus + "3"=> AV_VIRUS_FOUND, #virus found + "6"=> AV_VIRUS_FOUND, #virus found + "*"=> AV_SCAN_FAILED, #else scan failed + ), + + 'messagepattern'=> '/.*?Infection:(.*)$/m', + ), +); + + +/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. + * @global boolean $wgAntivirusRequired +*/ +$wgAntivirusRequired= true; + +/** Determines if the mime type of uploaded files should be checked + * @global boolean $wgVerifyMimeType +*/ +$wgVerifyMimeType= true; + +/** Sets the mime type definition file to use by MimeMagic.php. +* @global string $wgMimeTypeFile +*/ +#$wgMimeTypeFile= "/etc/mime.types"; +$wgMimeTypeFile= "includes/mime.types"; +#$wgMimeTypeFile= NULL; #use built-in defaults only. + +/** Sets the mime type info file to use by MimeMagic.php. +* @global string $wgMimeInfoFile +*/ +$wgMimeInfoFile= "includes/mime.info"; +#$wgMimeInfoFile= NULL; #use built-in defaults only. + +/** Switch for loading the FileInfo extension by PECL at runtime. +* This should be used only if fileinfo is installed as a shared object / dynamic libary +* @global string $wgLoadFileinfoExtension +*/ +$wgLoadFileinfoExtension= false; + +/** Sets an external mime detector program. The command must print only the mime type to standard output. +* the name of the file to process will be appended to the command given here. +* If not set or NULL, mime_content_type will be used if available. +*/ +$wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0 +#$wgMimeDetectorCommand= "file -bi"; #use external mime detector (Linux) + +/** Switch for trivial mime detection. Used by thumb.php to disable all fance things, +* because only a few types of images are needed and file extensions can be trusted. +*/ +$wgTrivialMimeDetection= false; + +/** + * To set 'pretty' URL paths for actions other than + * plain page views, add to this array. For instance: + * 'edit' => "$wgScriptPath/edit/$1" + * + * There must be an appropriate script or rewrite rule + * in place to handle these URLs. + */ +$wgActionPaths = array(); + +/** + * If you operate multiple wikis, you can define a shared upload path here. + * Uploads to this wiki will NOT be put there - they will be put into + * $wgUploadDirectory. + * If $wgUseSharedUploads is set, the wiki will look in the shared repository if + * no file of the given name is found in the local repository (for [[Image:..]], + * [[Media:..]] links). Thumbnails will also be looked for and generated in this + * directory. + */ +$wgUseSharedUploads = false; +/** Full path on the web server where shared uploads can be found */ +$wgSharedUploadPath = "http://commons.wikimedia.org/shared/images"; +/** Fetch commons image description pages and display them on the local wiki? */ +$wgFetchCommonsDescriptions = false; +/** Path on the file system where shared uploads can be found. */ +$wgSharedUploadDirectory = "/var/www/wiki3/images"; +/** DB name with metadata about shared directory. Set this to false if the uploads do not come from a wiki. */ +$wgSharedUploadDBname = false; +/** Optional table prefix used in database. */ +$wgSharedUploadDBprefix = ''; +/** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */ +$wgCacheSharedUploads = true; + +/** + * Point the upload navigation link to an external URL + * Useful if you want to use a shared repository by default + * without disabling local uploads (use $wgEnableUploads = false for that) + * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; +*/ +$wgUploadNavigationUrl = false; + +/** + * Give a path here to use thumb.php for thumbnail generation on client request, instead of + * generating them on render and outputting a static URL. This is necessary if some of your + * apache servers don't have read/write access to the thumbnail path. + * + * Example: + * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb.php"; + */ +$wgThumbnailScriptPath = false; +$wgSharedThumbnailScriptPath = false; + +/** + * Set the following to false especially if you have a set of files that need to + * be accessible by all wikis, and you do not want to use the hash (path/a/aa/) + * directory layout. + */ +$wgHashedSharedUploadDirectory = true; + +/** + * Base URL for a repository wiki. Leave this blank if uploads are just stored + * in a shared directory and not meant to be accessible through a separate wiki. + * Otherwise the image description pages on the local wiki will link to the + * image description page on this wiki. + * + * Please specify the namespace, as in the example below. + */ +$wgRepositoryBaseUrl="http://commons.wikimedia.org/wiki/Image:"; + + +# +# Email settings +# + +/** + * Site admin email address + * Default to wikiadmin@SERVER_NAME + * @global string $wgEmergencyContact + */ +$wgEmergencyContact = 'wikiadmin@' . $wgServerName; + +/** + * Password reminder email address + * The address we should use as sender when a user is requesting his password + * Default to apache@SERVER_NAME + * @global string $wgPasswordSender + */ +$wgPasswordSender = 'MediaWiki Mail '; + +/** + * dummy address which should be accepted during mail send action + * It might be necessay to adapt the address or to set it equal + * to the $wgEmergencyContact address + */ +#$wgNoReplyAddress = $wgEmergencyContact; +$wgNoReplyAddress = 'reply@not.possible'; + +/** + * Set to true to enable the e-mail basic features: + * Password reminders, etc. If sending e-mail on your + * server doesn't work, you might want to disable this. + * @global bool $wgEnableEmail + */ +$wgEnableEmail = true; + +/** + * Set to true to enable user-to-user e-mail. + * This can potentially be abused, as it's hard to track. + * @global bool $wgEnableUserEmail + */ +$wgEnableUserEmail = true; + +/** + * SMTP Mode + * For using a direct (authenticated) SMTP server connection. + * Default to false or fill an array : + * + * "host" => 'SMTP domain', + * "IDHost" => 'domain for MessageID', + * "port" => "25", + * "auth" => true/false, + * "username" => user, + * "password" => password + * + * + * @global mixed $wgSMTP + */ +$wgSMTP = false; + + +/**#@+ + * Database settings + */ +/** database host name or ip address */ +$wgDBserver = 'localhost'; +/** database port number */ +$wgDBport = ''; +/** name of the database */ +$wgDBname = 'wikidb'; +/** */ +$wgDBconnection = ''; +/** Database username */ +$wgDBuser = 'wikiuser'; +/** Database type + * "mysql" for working code and "PostgreSQL" for development/broken code + */ +$wgDBtype = "mysql"; +/** Search type + * Leave as null to select the default search engine for the + * selected database type (eg SearchMySQL4), or set to a class + * name to override to a custom search engine. + */ +$wgSearchType = null; +/** Table name prefix */ +$wgDBprefix = ''; +/**#@-*/ + +/** Live high performance sites should disable this - some checks acquire giant mysql locks */ +$wgCheckDBSchema = true; + + +/** + * Shared database for multiple wikis. Presently used for storing a user table + * for single sign-on. The server for this database must be the same as for the + * main database. + * EXPERIMENTAL + */ +$wgSharedDB = null; + +# Database load balancer +# This is a two-dimensional array, an array of server info structures +# Fields are: +# host: Host name +# dbname: Default database name +# user: DB user +# password: DB password +# type: "mysql" or "pgsql" +# load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 +# groupLoads: array of load ratios, the key is the query group name. A query may belong +# to several groups, the most specific group defined here is used. +# +# flags: bit field +# DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended) +# DBO_DEBUG -- equivalent of $wgDebugDumpSql +# DBO_TRX -- wrap entire request in a transaction +# DBO_IGNORE -- ignore errors (not useful in LocalSettings.php) +# DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) +# +# max lag: (optional) Maximum replication lag before a slave will taken out of rotation +# max threads: (optional) Maximum number of running threads +# +# 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 +$wgDBservers = false; + +/** How long to wait for a slave to catch up to the master */ +$wgMasterWaitTimeout = 10; + +/** File to log database errors to */ +$wgDBerrorLog = false; + +/** When to give an error message */ +$wgDBClusterTimeout = 10; + +/** + * wgDBminWordLen : + * MySQL 3.x : used to discard words that MySQL will not return any results for + * shorter values configure mysql directly. + * MySQL 4.x : ignore it and configure mySQL + * See: http://dev.mysql.com/doc/mysql/en/Fulltext_Fine-tuning.html + */ +$wgDBminWordLen = 4; +/** Set to true if using InnoDB tables */ +$wgDBtransactions = false; +/** Set to true for compatibility with extensions that might be checking. + * MySQL 3.23.x is no longer supported. */ +$wgDBmysql4 = true; + +/** + * Set to true to engage MySQL 4.1/5.0 charset-related features; + * for now will just cause sending of 'SET NAMES=utf8' on connect. + * + * WARNING: THIS IS EXPERIMENTAL! + * + * May break if you're not using the table defs from mysql5/tables.sql. + * May break if you're upgrading an existing wiki if set differently. + * Broken symptoms likely to include incorrect behavior with page titles, + * usernames, comments etc containing non-ASCII characters. + * Might also cause failures on the object cache and other things. + * + * Even correct usage may cause failures with Unicode supplementary + * characters (those not in the Basic Multilingual Plane) unless MySQL + * has enhanced their Unicode support. + */ +$wgDBmysql5 = false; + +/** + * Other wikis on this site, can be administered from a single developer + * account. + * Array numeric key => database name + */ +$wgLocalDatabases = array(); + +/** + * Object cache settings + * See Defines.php for types + */ +$wgMainCacheType = CACHE_NONE; +$wgMessageCacheType = CACHE_ANYTHING; +$wgParserCacheType = CACHE_ANYTHING; + +$wgParserCacheExpireTime = 86400; + +$wgSessionsInMemcached = false; +$wgLinkCacheMemcached = false; # Not fully tested + +/** + * Memcached-specific settings + * See docs/memcached.txt + */ +$wgUseMemCached = false; +$wgMemCachedDebug = false; # Will be set to false in Setup.php, if the server isn't working +$wgMemCachedServers = array( '127.0.0.1:11000' ); +$wgMemCachedDebug = false; +$wgMemCachedPersistent = false; + +/** + * Directory for local copy of message cache, for use in addition to memcached + */ +$wgLocalMessageCache = false; +/** + * Defines format of local cache + * true - Serialized object + * false - PHP source file (Warning - security risk) + */ +$wgLocalMessageCacheSerialized = true; + +/** + * Directory for compiled constant message array databases + * WARNING: turning anything on will just break things, aaaaaah!!!! + */ +$wgCachedMessageArrays = false; + +# Language settings +# +/** Site language code, should be one of ./languages/Language(.*).php */ +$wgLanguageCode = 'en'; + +/** + * Some languages need different word forms, usually for different cases. + * Used in Language::convertGrammar(). + */ +$wgGrammarForms = array(); +#$wgGrammarForms['en']['genitive']['car'] = 'car\'s'; + +/** Treat language links as magic connectors, not inline links */ +$wgInterwikiMagic = true; + +/** Hide interlanguage links from the sidebar */ +$wgHideInterlanguageLinks = false; + + +/** We speak UTF-8 all the time now, unless some oddities happen */ +$wgInputEncoding = 'UTF-8'; +$wgOutputEncoding = 'UTF-8'; +$wgEditEncoding = ''; + +# Set this to eg 'ISO-8859-1' to perform character set +# conversion when loading old revisions not marked with +# "utf-8" flag. Use this when converting wiki to UTF-8 +# without the burdensome mass conversion of old text data. +# +# NOTE! This DOES NOT touch any fields other than old_text. +# Titles, comments, user names, etc still must be converted +# en masse in the database before continuing as a UTF-8 wiki. +$wgLegacyEncoding = false; + +/** + * If set to true, the MediaWiki 1.4 to 1.5 schema conversion will + * create stub reference rows in the text table instead of copying + * the full text of all current entries from 'cur' to 'text'. + * + * This will speed up the conversion step for large sites, but + * requires that the cur table be kept around for those revisions + * to remain viewable. + * + * maintenance/migrateCurStubs.php can be used to complete the + * migration in the background once the wiki is back online. + * + * This option affects the updaters *only*. Any present cur stub + * revisions will be readable at runtime regardless of this setting. + */ +$wgLegacySchemaConversion = false; + +$wgMimeType = 'text/html'; +$wgJsMimeType = 'text/javascript'; +$wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; +$wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; + +/** Enable to allow rewriting dates in page text. + * DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES */ +$wgUseDynamicDates = false; +/** Enable dates like 'May 12' instead of '12 May', this only takes effect if + * the interface is set to English + */ +$wgAmericanDates = false; +/** + * For Hindi and Arabic use local numerals instead of Western style (0-9) + * numerals in interface. + */ +$wgTranslateNumerals = true; + + +# Translation using MediaWiki: namespace +# This will increase load times by 25-60% unless memcached is installed +# Interface messages will be loaded from the database. +$wgUseDatabaseMessages = true; +$wgMsgCacheExpiry = 86400; + +# Whether to enable language variant conversion. +$wgDisableLangConversion = false; + +/** + * Show a bar of language selection links in the user login and user + * registration forms; edit the "loginlanguagelinks" message to + * customise these + */ +$wgLoginLanguageSelector = false; + +# Whether to use zhdaemon to perform Chinese text processing +# zhdaemon is under developement, so normally you don't want to +# use it unless for testing +$wgUseZhdaemon = false; +$wgZhdaemonHost="localhost"; +$wgZhdaemonPort=2004; + +/** Normally you can ignore this and it will be something + like $wgMetaNamespace . "_talk". In some languages, you + may want to set this manually for grammatical reasons. + It is currently only respected by those languages + where it might be relevant and where no automatic + grammar converter exists. +*/ +$wgMetaNamespaceTalk = false; + +# Miscellaneous configuration settings +# + +$wgLocalInterwiki = 'w'; +$wgInterwikiExpiry = 10800; # Expiry time for cache of interwiki table + +/** Interwiki caching settings. + $wgInterwikiCache specifies path to constant database file + This cdb database is generated by dumpInterwiki from maintenance + and has such key formats: + dbname:key - a simple key (e.g. enwiki:meta) + _sitename:key - site-scope key (e.g. wiktionary:meta) + __global:key - global-scope key (e.g. __global:meta) + __sites:dbname - site mapping (e.g. __sites:enwiki) + Sites mapping just specifies site name, other keys provide + "local url" data layout. + $wgInterwikiScopes specify number of domains to check for messages: + 1 - Just wiki(db)-level + 2 - wiki and global levels + 3 - site levels + $wgInterwikiFallbackSite - if unable to resolve from cache +*/ +$wgInterwikiCache = false; +$wgInterwikiScopes = 3; +$wgInterwikiFallbackSite = 'wiki'; + +/** + * If local interwikis are set up which allow redirects, + * set this regexp to restrict URLs which will be displayed + * as 'redirected from' links. + * + * It might look something like this: + * $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!'; + * + * Leave at false to avoid displaying any incoming redirect markers. + * This does not affect intra-wiki redirects, which don't change + * the URL. + */ +$wgRedirectSources = false; + + +$wgShowIPinHeader = true; # For non-logged in users +$wgMaxNameChars = 255; # Maximum number of bytes in username +$wgMaxArticleSize = 2048; # Maximum article size in kilobytes + +$wgExtraSubtitle = ''; +$wgSiteSupportPage = ''; # A page where you users can receive donations + +$wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; + +/** + * The debug log file should be not be publicly accessible if it is used, as it + * may contain private data. */ +$wgDebugLogFile = ''; + +/**#@+ + * @global bool + */ +$wgDebugRedirects = false; +$wgDebugRawPage = false; # Avoid overlapping debug entries by leaving out CSS + +$wgDebugComments = false; +$wgReadOnly = null; +$wgLogQueries = false; + +/** + * Write SQL queries to the debug log + */ +$wgDebugDumpSql = false; + +/** + * Set to an array of log group keys to filenames. + * If set, wfDebugLog() output for that group will go to that file instead + * of the regular $wgDebugLogFile. Useful for enabling selective logging + * in production. + */ +$wgDebugLogGroups = array(); + +/** + * Whether to show "we're sorry, but there has been a database error" pages. + * Displaying errors aids in debugging, but may display information useful + * to an attacker. + */ +$wgShowSQLErrors = false; + +/** + * If true, some error messages will be colorized when running scripts on the + * command line; this can aid picking important things out when debugging. + * Ignored when running on Windows or when output is redirected to a file. + */ +$wgColorErrors = true; + +/** + * disable experimental dmoz-like category browsing. Output things like: + * Encyclopedia > Music > Style of Music > Jazz + */ +$wgUseCategoryBrowser = false; + +/** + * Keep parsed pages in a cache (objectcache table, turck, or memcached) + * to speed up output of the same page viewed by another user with the + * same options. + * + * This can provide a significant speedup for medium to large pages, + * so you probably want to keep it on. + */ +$wgEnableParserCache = true; + +/** + * If on, the sidebar navigation links are cached for users with the + * current language set. This can save a touch of load on a busy site + * by shaving off extra message lookups. + * + * However it is also fragile: changing the site configuration, or + * having a variable $wgArticlePath, can produce broken links that + * don't update as expected. + */ +$wgEnableSidebarCache = false; + +/** + * 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 + * (default), it will only be counted if it contains at least one [[wiki + * link]]. See http://meta.wikimedia.org/wiki/Help:Article_count + * + * Retroactively changing this variable will not affect + * the existing count (cf. maintenance/recount.sql). +*/ +$wgUseCommaCount = false; + +/**#@-*/ + +/** + * wgHitcounterUpdateFreq sets how often page counters should be updated, higher + * values are easier on the database. A value of 1 causes the counters to be + * updated on every hit, any higher value n cause them to update *on average* + * every n hits. Should be set to either 1 or something largish, eg 1000, for + * maximum efficiency. +*/ +$wgHitcounterUpdateFreq = 1; + +# Basic user rights and block settings +$wgSysopUserBans = true; # Allow sysops to ban logged-in users +$wgSysopRangeBans = true; # Allow sysops to ban IP ranges +$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire +$wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user talk page + +# Pages anonymous user may see as an array, e.g.: +# array ( "Main Page", "Special:Userlogin", "Wikipedia:Help"); +# NOTE: This will only work if $wgGroupPermissions['*']['read'] +# is false -- see below. Otherwise, ALL pages are accessible, +# regardless of this setting. +# Also note that this will only protect _pages in the wiki_. +# Uploaded files will remain readable. Make your upload +# directory name unguessable, or use .htaccess to protect it. +$wgWhitelistRead = false; + +/** + * Should editors be required to have a validated e-mail + * address before being allowed to edit? + */ +$wgEmailConfirmToEdit=false; + +/** + * Permission keys given to users in each group. + * All users are implicitly in the '*' group including anonymous visitors; + * logged-in users are all implicitly in the 'user' group. These will be + * combined with the permissions of all groups that a given user is listed + * in in the user_groups table. + * + * Functionality to make pages inaccessible has not been extensively tested + * for security. Use at your own risk! + * + * This replaces wgWhitelistAccount and wgWhitelistEdit + */ +$wgGroupPermissions = array(); + +// Implicit group for all visitors +$wgGroupPermissions['*' ]['createaccount'] = true; +$wgGroupPermissions['*' ]['read'] = true; +$wgGroupPermissions['*' ]['edit'] = true; +$wgGroupPermissions['*' ]['createpage'] = true; +$wgGroupPermissions['*' ]['createtalk'] = true; + +// Implicit group for all logged-in accounts +$wgGroupPermissions['user' ]['move'] = true; +$wgGroupPermissions['user' ]['read'] = true; +$wgGroupPermissions['user' ]['edit'] = true; +$wgGroupPermissions['user' ]['createpage'] = true; +$wgGroupPermissions['user' ]['createtalk'] = true; +$wgGroupPermissions['user' ]['upload'] = true; +$wgGroupPermissions['user' ]['reupload'] = true; +$wgGroupPermissions['user' ]['reupload-shared'] = true; +$wgGroupPermissions['user' ]['minoredit'] = true; + +// Implicit group for accounts that pass $wgAutoConfirmAge +$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; + +// Implicit group for accounts with confirmed email addresses +// This has little use when email address confirmation is off +$wgGroupPermissions['emailconfirmed']['emailconfirmed'] = true; + +// Users with bot privilege can have their edits hidden +// from various log pages by default +$wgGroupPermissions['bot' ]['bot'] = true; +$wgGroupPermissions['bot' ]['autoconfirmed'] = true; + +// Most extra permission abilities go to this group +$wgGroupPermissions['sysop']['block'] = true; +$wgGroupPermissions['sysop']['createaccount'] = true; +$wgGroupPermissions['sysop']['delete'] = true; +$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text +$wgGroupPermissions['sysop']['editinterface'] = true; +$wgGroupPermissions['sysop']['import'] = true; +$wgGroupPermissions['sysop']['importupload'] = true; +$wgGroupPermissions['sysop']['move'] = true; +$wgGroupPermissions['sysop']['patrol'] = true; +$wgGroupPermissions['sysop']['protect'] = true; +$wgGroupPermissions['sysop']['proxyunbannable'] = true; +$wgGroupPermissions['sysop']['rollback'] = true; +$wgGroupPermissions['sysop']['trackback'] = true; +$wgGroupPermissions['sysop']['upload'] = true; +$wgGroupPermissions['sysop']['reupload'] = true; +$wgGroupPermissions['sysop']['reupload-shared'] = true; +$wgGroupPermissions['sysop']['unwatchedpages'] = true; +$wgGroupPermissions['sysop']['autoconfirmed'] = true; + +// Permission to change users' group assignments +$wgGroupPermissions['bureaucrat']['userrights'] = true; + +// Experimental permissions, not ready for production use +//$wgGroupPermissions['sysop']['deleterevision'] = true; +//$wgGroupPermissions['bureaucrat']['hiderevision'] = true; + +/** + * The developer group is deprecated, but can be activated if need be + * to use the 'lockdb' and 'unlockdb' special pages. Those require + * that a lock file be defined and creatable/removable by the web + * server. + */ +# $wgGroupPermissions['developer']['siteadmin'] = true; + +/** + * Set of available actions that can be restricted via Special:Protect + * You probably shouldn't change this. + * Translated trough restriction-* messages. + */ +$wgRestrictionTypes = array( 'edit', 'move' ); + +/** + * Set of permission keys that can be selected via Special:Protect. + * 'autoconfirm' allows all registerd users if $wgAutoConfirmAge is 0. + */ +$wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); + + +/** + * Number of seconds an account is required to age before + * it's given the implicit 'autoconfirm' group membership. + * This can be used to limit privileges of new accounts. + * + * Accounts created by earlier versions of the software + * may not have a recorded creation date, and will always + * be considered to pass the age test. + * + * When left at 0, all registered accounts will pass. + */ +$wgAutoConfirmAge = 0; +//$wgAutoConfirmAge = 600; // ten minutes +//$wgAutoConfirmAge = 3600*24; // one day + + + +# Proxy scanner settings +# + +/** + * If you enable this, every editor's IP address will be scanned for open HTTP + * proxies. + * + * Don't enable this. Many sysops will report "hostile TCP port scans" to your + * ISP and ask for your server to be shut down. + * + * You have been warned. + */ +$wgBlockOpenProxies = false; +/** Port we want to scan for a proxy */ +$wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 ); +/** Script used to scan */ +$wgProxyScriptPath = "$IP/proxy_check.php"; +/** */ +$wgProxyMemcExpiry = 86400; +/** This should always be customised in LocalSettings.php */ +$wgSecretKey = false; +/** big list of banned IP addresses, in the keys not the values */ +$wgProxyList = array(); +/** deprecated */ +$wgProxyKey = false; + +/** Number of accounts each IP address may create, 0 to disable. + * Requires memcached */ +$wgAccountCreationThrottle = 0; + +# Client-side caching: + +/** Allow client-side caching of pages */ +$wgCachePages = true; + +/** + * Set this to current time to invalidate all prior cached pages. Affects both + * client- and server-side caching. + * You can get the current date on your server by using the command: + * date +%Y%m%d%H%M%S + */ +$wgCacheEpoch = '20030516000000'; + + +# Server-side caching: + +/** + * This will cache static pages for non-logged-in users to reduce + * database traffic on public sites. + * Must set $wgShowIPinHeader = false + */ +$wgUseFileCache = false; +/** Directory where the cached page will be saved */ +$wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; + +/** + * When using the file cache, we can store the cached HTML gzipped to save disk + * space. Pages will then also be served compressed to clients that support it. + * THIS IS NOT COMPATIBLE with ob_gzhandler which is now enabled if supported in + * the default LocalSettings.php! If you enable this, remove that setting first. + * + * Requires zlib support enabled in PHP. + */ +$wgUseGzip = false; + +# Email notification settings +# + +/** For email notification on page changes */ +$wgPasswordSender = $wgEmergencyContact; + +# true: from page editor if s/he opted-in +# false: Enotif mails appear to come from $wgEmergencyContact +$wgEnotifFromEditor = false; + +// TODO move UPO to preferences probably ? +# If set to true, users get a corresponding option in their preferences and can choose to enable or disable at their discretion +# If set to false, the corresponding input form on the user preference page is suppressed +# It call this to be a "user-preferences-option (UPO)" +$wgEmailAuthentication = true; # UPO (if this is set to false, texts referring to authentication are suppressed) +$wgEnotifWatchlist = false; # UPO +$wgEnotifUserTalk = false; # UPO +$wgEnotifRevealEditorAddress = false; # UPO; reply-to address may be filled with page editor's address (if user allowed this in the preferences) +$wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger notification mails. +# # Attention: _every_ change on a user_talk page trigger a notification mail (if the user is not yet notified) + + +/** Show watching users in recent changes, watchlist and page history views */ +$wgRCShowWatchingUsers = false; # UPO +/** Show watching users in Page views */ +$wgPageShowWatchingUsers = false; +/** + * Show "Updated (since my last visit)" marker in RC view, watchlist and history + * view for watched pages with new changes */ +$wgShowUpdatedMarker = true; + +$wgCookieExpiration = 2592000; + +/** Clock skew or the one-second resolution of time() can occasionally cause cache + * problems when the user requests two pages within a short period of time. This + * variable adds a given number of seconds to vulnerable timestamps, thereby giving + * a grace period. + */ +$wgClockSkewFudge = 5; + +# Squid-related settings +# + +/** Enable/disable Squid */ +$wgUseSquid = false; + +/** If you run Squid3 with ESI support, enable this (default:false): */ +$wgUseESI = false; + +/** Internal server name as known to Squid, if different */ +# $wgInternalServer = 'http://yourinternal.tld:8000'; +$wgInternalServer = $wgServer; + +/** + * Cache timeout for the squid, will be sent as s-maxage (without ESI) or + * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in + * the Squid config. 18000 seconds = 5 hours, more cache hits with 2678400 = 31 + * days + */ +$wgSquidMaxage = 18000; + +/** + * A list of proxy servers (ips if possible) to purge on changes don't specify + * ports here (80 is default) + */ +# $wgSquidServers = array('127.0.0.1'); +$wgSquidServers = array(); +$wgSquidServersNoPurge = array(); + +/** Maximum number of titles to purge in any one client operation */ +$wgMaxSquidPurgeTitles = 400; + +/** HTCP multicast purging */ +$wgHTCPPort = 4827; +$wgHTCPMulticastTTL = 1; +# $wgHTCPMulticastAddress = "224.0.0.85"; + +# Cookie settings: +# +/** + * Set to set an explicit domain on the login cookies eg, "justthis.domain. org" + * or ".any.subdomain.net" + */ +$wgCookieDomain = ''; +$wgCookiePath = '/'; +$wgCookieSecure = ($wgProto == 'https'); +$wgDisableCookieCheck = false; + +/** Override to customise the session name */ +$wgSessionName = false; + +/** Whether to allow inline image pointing to other websites */ +$wgAllowExternalImages = false; + +/** If the above is false, you can specify an exception here. Image URLs + * that start with this string are then rendered, while all others are not. + * You can use this to set up a trusted, simple repository of images. + * + * Example: + * $wgAllowExternalImagesFrom = 'http://127.0.0.1/'; + */ +$wgAllowExternalImagesFrom = ''; + +/** Disable database-intensive features */ +$wgMiserMode = false; +/** Disable all query pages if miser mode is on, not just some */ +$wgDisableQueryPages = false; +/** Generate a watchlist once every hour or so */ +$wgUseWatchlistCache = false; +/** The hour or so mentioned above */ +$wgWLCacheTimeout = 3600; +/** Number of links to a page required before it is deemed "wanted" */ +$wgWantedPagesThreshold = 1; +/** Enable slow parser functions */ +$wgAllowSlowParserFunctions = false; + +/** + * To use inline TeX, you need to compile 'texvc' (in the 'math' subdirectory of + * the MediaWiki package and have latex, dvips, gs (ghostscript), andconvert + * (ImageMagick) installed and available in the PATH. + * Please see math/README for more information. + */ +$wgUseTeX = false; +/** Location of the texvc binary */ +$wgTexvc = './math/texvc'; + +# +# Profiling / debugging +# +# You have to create a 'profiling' table in your database before using +# profiling see maintenance/archives/patch-profiling.sql . + +/** Enable for more detailed by-function times in debug log */ +$wgProfiling = false; +/** Only record profiling info for pages that took longer than this */ +$wgProfileLimit = 0.0; +/** Don't put non-profiling info into log file */ +$wgProfileOnly = false; +/** Log sums from profiling into "profiling" table in db. */ +$wgProfileToDatabase = false; +/** Only profile every n requests when profiling is turned on */ +$wgProfileSampleRate = 1; +/** If true, print a raw call tree instead of per-function report */ +$wgProfileCallTree = false; +/** If not empty, specifies profiler type to load */ +$wgProfilerType = ''; +/** Should application server host be put into profiling table */ +$wgProfilePerHost = false; + +/** Settings for UDP profiler */ +$wgUDPProfilerHost = '127.0.0.1'; +$wgUDPProfilerPort = '3811'; + +/** Detects non-matching wfProfileIn/wfProfileOut calls */ +$wgDebugProfiling = false; +/** Output debug message on every wfProfileIn/wfProfileOut */ +$wgDebugFunctionEntry = 0; +/** Lots of debugging output from SquidUpdate.php */ +$wgDebugSquid = false; + +$wgDisableCounters = false; +$wgDisableTextSearch = false; +$wgDisableSearchContext = false; +/** + * If you've disabled search semi-permanently, this also disables updates to the + * table. If you ever re-enable, be sure to rebuild the search table. + */ +$wgDisableSearchUpdate = false; +/** Uploads have to be specially set up to be secure */ +$wgEnableUploads = false; +/** + * Show EXIF data, on by default if available. + * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php + */ +$wgShowEXIF = function_exists( 'exif_read_data' ); + +/** + * Set to true to enable the upload _link_ while local uploads are disabled. + * Assumes that the special page link will be bounced to another server where + * uploads do work. + */ +$wgRemoteUploads = false; +$wgDisableAnonTalk = false; +/** + * Do DELETE/INSERT for link updates instead of incremental + */ +$wgUseDumbLinkUpdate = false; + +/** + * Anti-lock flags - bitfield + * ALF_PRELOAD_LINKS + * Preload links during link update for save + * ALF_PRELOAD_EXISTENCE + * Preload cur_id during replaceLinkHolders + * ALF_NO_LINK_LOCK + * Don't use locking reads when updating the link table. This is + * necessary for wikis with a high edit rate for performance + * reasons, but may cause link table inconsistency + * ALF_NO_BLOCK_LOCK + * As for ALF_LINK_LOCK, this flag is a necessity for high-traffic + * wikis. + */ +$wgAntiLockFlags = 0; + +/** + * Path to the GNU diff3 utility. If the file doesn't exist, edit conflicts will + * fall back to the old behaviour (no merging). + */ +$wgDiff3 = '/usr/bin/diff3'; + +/** + * We can also compress text in the old revisions table. If this is set on, old + * revisions will be compressed on page save if zlib support is available. Any + * compressed revisions will be decompressed on load regardless of this setting + * *but will not be readable at all* if zlib support is not available. + */ +$wgCompressRevisions = false; + +/** + * This is the list of preferred extensions for uploading files. Uploading files + * with extensions not in this list will trigger a warning. + */ +$wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' ); + +/** Files with these extensions will never be allowed as uploads. */ +$wgFileBlacklist = array( + # HTML may contain cookie-stealing JavaScript and web bugs + 'html', 'htm', 'js', 'jsb', + # PHP scripts may execute arbitrary code on the server + 'php', 'phtml', 'php3', 'php4', 'phps', + # Other types that may be interpreted by some servers + 'shtml', 'jhtml', 'pl', 'py', 'cgi', + # May contain harmful executables for Windows victims + 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl' ); + +/** Files with these mime types will never be allowed as uploads + * if $wgVerifyMimeType is enabled. + */ +$wgMimeTypeBlacklist= array( + # HTML may contain cookie-stealing JavaScript and web bugs + 'text/html', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', + # PHP scripts may execute arbitrary code on the server + 'application/x-php', 'text/x-php', + # Other types that may be interpreted by some servers + 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', + # Windows metafile, client-side vulnerability on some systems + 'application/x-msmetafile' +); + +/** This is a flag to determine whether or not to check file extensions on upload. */ +$wgCheckFileExtensions = true; + +/** + * If this is turned off, users may override the warning for files not covered + * by $wgFileExtensions. + */ +$wgStrictFileExtensions = true; + +/** Warn if uploaded files are larger than this */ +$wgUploadSizeWarning = 150 * 1024; + +/** For compatibility with old installations set to false */ +$wgPasswordSalt = true; + +/** Which namespaces should support subpages? + * See Language.php for a list of namespaces. + */ +$wgNamespacesWithSubpages = array( + NS_TALK => true, + NS_USER => true, + NS_USER_TALK => true, + NS_PROJECT_TALK => true, + NS_IMAGE_TALK => true, + NS_MEDIAWIKI_TALK => true, + NS_TEMPLATE_TALK => true, + NS_HELP_TALK => true, + NS_CATEGORY_TALK => true +); + +$wgNamespacesToBeSearchedDefault = array( + NS_MAIN => true, +); + +/** If set, a bold ugly notice will show up at the top of every page. */ +$wgSiteNotice = ''; + + +# +# Images settings +# + +/** dynamic server side image resizing ("Thumbnails") */ +$wgUseImageResize = false; + +/** + * Resizing can be done using PHP's internal image libraries or using + * ImageMagick or another third-party converter, e.g. GraphicMagick. + * These support more file formats than PHP, which only supports PNG, + * GIF, JPG, XBM and WBMP. + * + * Use Image Magick instead of PHP builtin functions. + */ +$wgUseImageMagick = false; +/** The convert command shipped with ImageMagick */ +$wgImageMagickConvertCommand = '/usr/bin/convert'; + +/** + * Use another resizing converter, e.g. GraphicMagick + * %s will be replaced with the source path, %d with the destination + * %w and %h will be replaced with the width and height + * + * An example is provided for GraphicMagick + * Leave as false to skip this + */ +#$wgCustomConvertCommand = "gm convert %s -resize %wx%h %d" +$wgCustomConvertCommand = false; + +# Scalable Vector Graphics (SVG) may be uploaded as images. +# Since SVG support is not yet standard in browsers, it is +# necessary to rasterize SVGs to PNG as a fallback format. +# +# An external program is required to perform this conversion: +$wgSVGConverters = array( + 'ImageMagick' => '$path/convert -background white -geometry $width $input $output', + 'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output', + 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', + 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', + 'rsvg' => '$path/rsvg -w$width -h$height $input $output', + ); +/** Pick one of the above */ +$wgSVGConverter = 'ImageMagick'; +/** If not in the executable PATH, specify */ +$wgSVGConverterPath = ''; +/** Don't scale a SVG larger than this */ +$wgSVGMaxSize = 1024; +/** + * Don't thumbnail an image if it will use too much working memory + * Default is 50 MB if decompressed to RGBA form, which corresponds to + * 12.5 million pixels or 3500x3500 + */ +$wgMaxImageArea = 1.25e7; +/** + * If rendered thumbnail files are older than this timestamp, they + * will be rerendered on demand as if the file didn't already exist. + * Update if there is some need to force thumbs and SVG rasterizations + * to rerender, such as fixes to rendering bugs. + */ +$wgThumbnailEpoch = '20030516000000'; + +/** + * If set, inline scaled images will still produce tags ready for + * output instead of showing an error message. + * + * This may be useful if errors are transitory, especially if the site + * is configured to automatically render thumbnails on request. + * + * On the other hand, it may obscure error conditions from debugging. + * Enable the debug log or the 'thumbnail' log group to make sure errors + * are logged to a file for review. + */ +$wgIgnoreImageErrors = false; + +/** + * Allow thumbnail rendering on page view. If this is false, a valid + * thumbnail URL is still output, but no file will be created at + * the target location. This may save some time if you have a + * thumb.php or 404 handler set up which is faster than the regular + * webserver(s). + */ +$wgGenerateThumbnailOnParse = true; + +/** Set $wgCommandLineMode if it's not set already, to avoid notices */ +if( !isset( $wgCommandLineMode ) ) { + $wgCommandLineMode = false; +} + + +# +# Recent changes settings +# + +/** Log IP addresses in the recentchanges table */ +$wgPutIPinRC = true; + +/** + * Recentchanges items are periodically purged; entries older than this many + * seconds will go. + * For one week : 7 * 24 * 3600 + */ +$wgRCMaxAge = 7 * 24 * 3600; + + +# Send RC updates via UDP +$wgRC2UDPAddress = false; +$wgRC2UDPPort = false; +$wgRC2UDPPrefix = ''; + +# +# Copyright and credits settings +# + +/** RDF metadata toggles */ +$wgEnableDublinCoreRdf = false; +$wgEnableCreativeCommonsRdf = false; + +/** Override for copyright metadata. + * TODO: these options need documentation + */ +$wgRightsPage = NULL; +$wgRightsUrl = NULL; +$wgRightsText = NULL; +$wgRightsIcon = NULL; + +/** Set this to some HTML to override the rights icon with an arbitrary logo */ +$wgCopyrightIcon = NULL; + +/** Set this to true if you want detailed copyright information forms on Upload. */ +$wgUseCopyrightUpload = false; + +/** Set this to false if you want to disable checking that detailed copyright + * information values are not empty. */ +$wgCheckCopyrightUpload = true; + +/** + * Set this to the number of authors that you want to be credited below an + * article text. Set it to zero to hide the attribution block, and a negative + * number (like -1) to show all authors. Note that this will require 2-3 extra + * database hits, which can have a not insignificant impact on performance for + * large wikis. + */ +$wgMaxCredits = 0; + +/** If there are more than $wgMaxCredits authors, show $wgMaxCredits of them. + * Otherwise, link to a separate credits page. */ +$wgShowCreditsIfMax = true; + + + +/** + * Set this to false to avoid forcing the first letter of links to capitals. + * WARNING: may break links! This makes links COMPLETELY case-sensitive. Links + * appearing with a capital at the beginning of a sentence will *not* go to the + * same place as links in the middle of a sentence using a lowercase initial. + */ +$wgCapitalLinks = true; + +/** + * List of interwiki prefixes for wikis we'll accept as sources for + * Special:Import (for sysops). Since complete page history can be imported, + * these should be 'trusted'. + * + * If a user has the 'import' permission but not the 'importupload' permission, + * they will only be able to run imports through this transwiki interface. + */ +$wgImportSources = array(); + +/** + * Optional default target namespace for interwiki imports. + * Can use this to create an incoming "transwiki"-style queue. + * Set to numeric key, not the name. + * + * Users may override this in the Special:Import dialog. + */ +$wgImportTargetNamespace = null; + +/** + * If set to false, disables the full-history option on Special:Export. + * This is currently poorly optimized for long edit histories, so is + * disabled on Wikimedia's sites. + */ +$wgExportAllowHistory = true; + +/** + * If set nonzero, Special:Export requests for history of pages with + * more revisions than this will be rejected. On some big sites things + * could get bogged down by very very long pages. + */ +$wgExportMaxHistory = 0; + +$wgExportAllowListContributors = false ; + + +/** Text matching this regular expression will be recognised as spam + * See http://en.wikipedia.org/wiki/Regular_expression */ +$wgSpamRegex = false; +/** Similarly if this function returns true */ +$wgFilterCallback = false; + +/** Go button goes straight to the edit screen if the article doesn't exist. */ +$wgGoToEdit = false; + +/** Allow limited user-specified HTML in wiki pages? + * It will be run through a whitelist for security. Set this to false if you + * want wiki pages to consist only of wiki markup. Note that replacements do not + * yet exist for all HTML constructs.*/ +$wgUserHtml = true; + +/** Allow raw, unchecked HTML in ... sections. + * THIS IS VERY DANGEROUS on a publically editable site, so USE wgGroupPermissions + * TO RESTRICT EDITING to only those that you trust + */ +$wgRawHtml = false; + +/** + * $wgUseTidy: use tidy to make sure HTML output is sane. + * This should only be enabled if $wgUserHtml is true. + * tidy is a free tool that fixes broken HTML. + * See http://www.w3.org/People/Raggett/tidy/ + * $wgTidyBin should be set to the path of the binary and + * $wgTidyConf to the path of the configuration file. + * $wgTidyOpts can include any number of parameters. + * + * $wgTidyInternal controls the use of the PECL extension to use an in- + * process tidy library instead of spawning a separate program. + * Normally you shouldn't need to override the setting except for + * debugging. To install, use 'pear install tidy' and add a line + * 'extension=tidy.so' to php.ini. + */ +$wgUseTidy = false; +$wgAlwaysUseTidy = false; +$wgTidyBin = 'tidy'; +$wgTidyConf = $IP.'/extensions/tidy/tidy.conf'; +$wgTidyOpts = ''; +$wgTidyInternal = function_exists( 'tidy_load_config' ); + +/** See list of skins and their symbolic names in languages/Language.php */ +$wgDefaultSkin = 'monobook'; + +/** + * Settings added to this array will override the language globals for the user + * preferences used by anonymous visitors and newly created accounts. (See names + * and sample values in languages/Language.php) + * For instance, to disable section editing links: + * $wgDefaultUserOptions ['editsection'] = 0; + * + */ +$wgDefaultUserOptions = array(); + +/** Whether or not to allow and use real name fields. Defaults to true. */ +$wgAllowRealName = true; + +/** Use XML parser? */ +$wgUseXMLparser = false ; + +/***************************************************************************** + * Extensions + */ + +/** + * A list of callback functions which are called once MediaWiki is fully initialised + */ +$wgExtensionFunctions = array(); + +/** + * Extension functions for initialisation of skins. This is called somewhat earlier + * than $wgExtensionFunctions. + */ +$wgSkinExtensionFunctions = array(); + +/** + * List of valid skin names. + * The key should be the name in all lower case, the value should be a display name. + * The default skins will be added later, by Skin::getSkinNames(). Use + * Skin::getSkinNames() as an accessor if you wish to have access to the full list. + */ +$wgValidSkinNames = array(); + +/** + * Special page list. + * See the top of SpecialPage.php for documentation. + */ +$wgSpecialPages = array(); + +/** + * Array mapping class names to filenames, for autoloading. + */ +$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. + * + * + * $wgExtensionCredits[$type][] = array( + * 'name' => 'Example extension', + * 'version' => 1.9, + * 'author' => 'Foo Barstein', + * 'url' => 'http://wwww.example.com/Example%20Extension/', + * ); + * + * + * Where $type is 'specialpage', 'parserhook', or 'other'. + */ +$wgExtensionCredits = array(); +/* + * end extensions + ******************************************************************************/ + +/** + * Allow user Javascript page? + * This enables a lot of neat customizations, but may + * increase security risk to users and server load. + */ +$wgAllowUserJs = false; + +/** + * Allow user Cascading Style Sheets (CSS)? + * This enables a lot of neat customizations, but may + * increase security risk to users and server load. + */ +$wgAllowUserCss = false; + +/** Use the site's Javascript page? */ +$wgUseSiteJs = true; + +/** Use the site's Cascading Style Sheets (CSS)? */ +$wgUseSiteCss = true; + +/** Filter for Special:Randompage. Part of a WHERE clause */ +$wgExtraRandompageSQL = false; + +/** Allow the "info" action, very inefficient at the moment */ +$wgAllowPageInfo = false; + +/** Maximum indent level of toc. */ +$wgMaxTocLevel = 999; + +/** Name of the external diff engine to use */ +$wgExternalDiffEngine = false; + +/** Use RC Patrolling to check for vandalism */ +$wgUseRCPatrol = true; + +/** Set maximum number of results to return in syndication feeds (RSS, Atom) for + * eg Recentchanges, Newpages. */ +$wgFeedLimit = 50; + +/** _Minimum_ timeout for cached Recentchanges feed, in seconds. + * A cached version will continue to be served out even if changes + * are made, until this many seconds runs out since the last render. + * + * If set to 0, feed caching is disabled. Use this for debugging only; + * feed generation can be pretty slow with diffs. + */ +$wgFeedCacheTimeout = 60; + +/** When generating Recentchanges RSS/Atom feed, diffs will not be generated for + * pages larger than this size. */ +$wgFeedDiffCutoff = 32768; + + +/** + * Additional namespaces. If the namespaces defined in Language.php and + * Namespace.php are insufficient, you can create new ones here, for example, + * to import Help files in other languages. + * PLEASE NOTE: Once you delete a namespace, the pages in that namespace will + * no longer be accessible. If you rename it, then you can access them through + * the new namespace name. + * + * Custom namespaces should start at 100 to avoid conflicting with standard + * namespaces, and should always follow the even/odd main/talk pattern. + */ +#$wgExtraNamespaces = +# array(100 => "Hilfe", +# 101 => "Hilfe_Diskussion", +# 102 => "Aide", +# 103 => "Discussion_Aide" +# ); +$wgExtraNamespaces = NULL; + +/** + * Limit images on image description pages to a user-selectable limit. In order + * to reduce disk usage, limits can only be selected from a list. This is the + * list of settings the user can choose from: + */ +$wgImageLimits = array ( + array(320,240), + array(640,480), + array(800,600), + array(1024,768), + array(1280,1024), + array(10000,10000) ); + +/** + * Adjust thumbnails on image pages according to a user setting. In order to + * reduce disk usage, the values can only be selected from a list. This is the + * list of settings the user can choose from: + */ +$wgThumbLimits = array( + 120, + 150, + 180, + 200, + 250, + 300 +); + +/** + * On category pages, show thumbnail gallery for images belonging to that + * category instead of listing them as articles. + */ +$wgCategoryMagicGallery = true; + +/** + * Paging limit for categories + */ +$wgCategoryPagingLimit = 200; + +/** + * Browser Blacklist for unicode non compliant browsers + * Contains a list of regexps : "/regexp/" matching problematic browsers + */ +$wgBrowserBlackList = array( + /** + * Netscape 2-4 detection + * The minor version may contain strings such as "Gold" or "SGoldC-SGI" + * Lots of non-netscape user agents have "compatible", so it's useful to check for that + * with a negative assertion. The [UIN] identifier specifies the level of security + * in a Netscape/Mozilla browser, checking for it rules out a number of fakers. + * The language string is unreliable, it is missing on NS4 Mac. + * + * Reference: http://www.psychedelix.com/agents/index.shtml + */ + '/^Mozilla\/2\.[^ ]+ .*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/3\.[^ ]+ .*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/4\.[^ ]+ .*?\((?!compatible).*; [UIN]/', + + /** + * MSIE on Mac OS 9 is teh sux0r, converts þ to , ð to , Þ to and Ð to + * + * Known useragents: + * - Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC) + * - Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC) + * - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) + * - [...] + * + * @link http://en.wikipedia.org/w/index.php?title=User%3A%C6var_Arnfj%F6r%F0_Bjarmason%2Ftestme&diff=12356041&oldid=12355864 + * @link http://en.wikipedia.org/wiki/Template%3AOS9 + */ + '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/' +); + +/** + * Fake out the timezone that the server thinks it's in. This will be used for + * date display and not for what's stored in the DB. Leave to null to retain + * your server's OS-based timezone value. This is the same as the timezone. + * + * This variable is currently used ONLY for signature formatting, not for + * anything else. + */ +# $wgLocaltimezone = 'GMT'; +# $wgLocaltimezone = 'PST8PDT'; +# $wgLocaltimezone = 'Europe/Sweden'; +# $wgLocaltimezone = 'CET'; +$wgLocaltimezone = null; + +/** + * Set an offset from UTC in minutes to use for the default timezone setting + * for anonymous users and new user accounts. + * + * This setting is used for most date/time displays in the software, and is + * overrideable in user preferences. It is *not* used for signature timestamps. + * + * You can set it to match the configured server timezone like this: + * $wgLocalTZoffset = date("Z") / 60; + * + * If your server is not configured for the timezone you want, you can set + * this in conjunction with the signature timezone and override the TZ + * environment variable like so: + * $wgLocaltimezone="Europe/Berlin"; + * putenv("TZ=$wgLocaltimezone"); + * $wgLocalTZoffset = date("Z") / 60; + * + * Leave at NULL to show times in universal time (UTC/GMT). + */ +$wgLocalTZoffset = null; + + +/** + * When translating messages with wfMsg(), it is not always clear what should be + * considered UI messages and what shoud be content messages. + * + * For example, for regular wikipedia site like en, there should be only one + * 'mainpage', therefore when getting the link of 'mainpage', we should treate + * it as content of the site and call wfMsgForContent(), while for rendering the + * text of the link, we call wfMsg(). The code in default behaves this way. + * However, sites like common do offer different versions of 'mainpage' and the + * like for different languages. This array provides a way to override the + * default behavior. For example, to allow language specific mainpage and + * community portal, set + * + * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' ); + */ +$wgForceUIMsgAsContentMsg = array(); + + +/** + * Authentication plugin. + */ +$wgAuth = null; + +/** + * Global list of hooks. + * Add a hook by doing: + * $wgHooks['event_name'][] = $function; + * or: + * $wgHooks['event_name'][] = array($function, $data); + * or: + * $wgHooks['event_name'][] = array($object, 'method'); + */ +$wgHooks = array(); + +/** + * The logging system has two levels: an event type, which describes the + * general category and can be viewed as a named subset of all logs; and + * an action, which is a specific kind of event that can exist in that + * log type. + */ +$wgLogTypes = array( '', + 'block', + 'protect', + 'rights', + 'delete', + 'upload', + 'move', + 'import' ); + +/** + * Lists the message key string for each log type. The localized messages + * will be listed in the user interface. + * + * Extensions with custom log types may add to this array. + */ +$wgLogNames = array( + '' => 'log', + 'block' => 'blocklogpage', + 'protect' => 'protectlogpage', + 'rights' => 'rightslog', + 'delete' => 'dellogpage', + 'upload' => 'uploadlogpage', + 'move' => 'movelogpage', + 'import' => 'importlogpage' ); + +/** + * Lists the message key string for descriptive text to be shown at the + * top of each log type. + * + * Extensions with custom log types may add to this array. + */ +$wgLogHeaders = array( + '' => 'alllogstext', + 'block' => 'blocklogtext', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', + 'delete' => 'dellogpagetext', + 'upload' => 'uploadlogpagetext', + 'move' => 'movelogpagetext', + 'import' => 'importlogpagetext', ); + +/** + * Lists the message key string for formatting individual events of each + * type and action when listed in the logs. + * + * Extensions with custom log types may add to this array. + */ +$wgLogActions = array( + 'block/block' => 'blocklogentry', + 'block/unblock' => 'unblocklogentry', + 'protect/protect' => 'protectedarticle', + 'protect/unprotect' => 'unprotectedarticle', + 'rights/rights' => 'rightslogentry', + 'delete/delete' => 'deletedarticle', + 'delete/restore' => 'undeletedarticle', + 'delete/revision' => 'revdelete-logentry', + 'upload/upload' => 'uploadedimage', + 'upload/revert' => 'uploadedimage', + 'move/move' => '1movedto2', + 'move/move_redir' => '1movedto2_redir', + 'import/upload' => 'import-logentry-upload', + 'import/interwiki' => 'import-logentry-interwiki' ); + +/** + * Experimental preview feature to fetch rendered text + * over an XMLHttpRequest from JavaScript instead of + * forcing a submit and reload of the whole page. + * Leave disabled unless you're testing it. + */ +$wgLivePreview = false; + +/** + * Disable the internal MySQL-based search, to allow it to be + * implemented by an extension instead. + */ +$wgDisableInternalSearch = false; + +/** + * Set this to a URL to forward search requests to some external location. + * If the URL includes '$1', this will be replaced with the URL-encoded + * search term. + * + * For example, to forward to Google you'd have something like: + * $wgSearchForwardUrl = 'http://www.google.com/search?q=$1' . + * '&domains=http://example.com' . + * '&sitesearch=http://example.com' . + * '&ie=utf-8&oe=utf-8'; + */ +$wgSearchForwardUrl = null; + +/** + * If true, external URL links in wiki text will be given the + * rel="nofollow" attribute as a hint to search engines that + * they should not be followed for ranking purposes as they + * are user-supplied and thus subject to spamming. + */ +$wgNoFollowLinks = true; + +/** + * Namespaces in which $wgNoFollowLinks doesn't apply. + * See Language.php for a list of namespaces. + */ +$wgNoFollowNsExceptions = array(); + +/** + * Robot policies for namespaces + * e.g. $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); + */ +$wgNamespaceRobotPolicies = array(); + +/** + * Specifies the minimal length of a user password. If set to + * 0, empty passwords are allowed. + */ +$wgMinimalPasswordLength = 0; + +/** + * Activate external editor interface for files and pages + * See http://meta.wikimedia.org/wiki/Help:External_editors + */ +$wgUseExternalEditor = true; + +/** Whether or not to sort special pages in Special:Specialpages */ + +$wgSortSpecialPages = true; + +/** + * Specify the name of a skin that should not be presented in the + * list of available skins. + * Use for blacklisting a skin which you do not want to remove + * from the .../skins/ directory + */ +$wgSkipSkin = ''; +$wgSkipSkins = array(); # More of the same + +/** + * Array of disabled article actions, e.g. view, edit, dublincore, delete, etc. + */ +$wgDisabledActions = array(); + +/** + * Disable redirects to special pages and interwiki redirects, which use a 302 and have no "redirected from" link + */ +$wgDisableHardRedirects = false; + +/** + * Use http.dnsbl.sorbs.net to check for open proxies + */ +$wgEnableSorbs = false; + +/** + * Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other + * methods might say + */ +$wgProxyWhitelist = array(); + +/** + * Simple rate limiter options to brake edit floods. + * Maximum number actions allowed in the given number of seconds; + * after that the violating client receives HTTP 500 error pages + * until the period elapses. + * + * array( 4, 60 ) for a maximum of 4 hits in 60 seconds. + * + * This option set is experimental and likely to change. + * Requires memcached. + */ +$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' + 'ip' => null, // for each anon and recent account + 'subnet' => null, // ... with final octet removed + ), + 'move' => array( + 'user' => null, + 'newbie' => null, + 'ip' => null, + 'subnet' => null, + ), + 'mailpassword' => array( + 'anon' => NULL, + ), + ); + +/** + * Set to a filename to log rate limiter hits. + */ +$wgRateLimitLog = null; + +/** + * Array of groups which should never trigger the rate limiter + */ +$wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); + +/** + * On Special:Unusedimages, consider images "used", if they are put + * into a category. Default (false) is not to count those as used. + */ +$wgCountCategorizedImagesAsUsed = false; + +/** + * External stores allow including content + * from non database sources following URL links + * + * Short names of ExternalStore classes may be specified in an array here: + * $wgExternalStores = array("http","file","custom")... + * + * CAUTION: Access to database might lead to code execution + */ +$wgExternalStores = false; + +/** + * An array of external mysql servers, e.g. + * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) ); + */ +$wgExternalServers = array(); + +/** + * The place to put new revisions, false to put them in the local text table. + * Part of a URL, e.g. DB://cluster1 + * + * Can be an array instead of a single string, to enable data distribution. Keys + * must be consecutive integers, starting at zero. Example: + * + * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); + * + */ +$wgDefaultExternalStore = false; + +/** +* list of trusted media-types and mime types. +* Use the MEDIATYPE_xxx constants to represent media types. +* This list is used by Image::isSafeFile +* +* Types not listed here will have a warning about unsafe content +* displayed on the images description page. It would also be possible +* to use this for further restrictions, like disabling direct +* [[media:...]] links for non-trusted formats. +*/ +$wgTrustedMediaFormats= array( + MEDIATYPE_BITMAP, //all bitmap formats + MEDIATYPE_AUDIO, //all audio formats + MEDIATYPE_VIDEO, //all plain video formats + "image/svg", //svg (only needed if inline rendering of svg is not supported) + "application/pdf", //PDF files + #"application/x-shockwafe-flash", //flash/shockwave movie +); + +/** + * Allow special page inclusions such as {{Special:Allpages}} + */ +$wgAllowSpecialInclusion = true; + +/** + * Timeout for HTTP requests done via CURL + */ +$wgHTTPTimeout = 3; + +/** + * Proxy to use for CURL requests. + */ +$wgHTTPProxy = false; + +/** + * Enable interwiki transcluding. Only when iw_trans=1. + */ +$wgEnableScaryTranscluding = false; +/** + * Expiry time for interwiki transclusion + */ +$wgTranscludeCacheExpiry = 3600; + +/** + * Support blog-style "trackbacks" for articles. See + * http://www.sixapart.com/pronet/docs/trackback_spec for details. + */ +$wgUseTrackbacks = false; + +/** + * Enable filtering of categories in Recentchanges + */ +$wgAllowCategorizedRecentChanges = false ; + +/** + * Number of jobs to perform per request. May be less than one in which case + * jobs are performed probabalistically. If this is zero, jobs will not be done + * during ordinary apache requests. In this case, maintenance/runJobs.php should + * be run periodically. + */ +$wgJobRunRate = 1; + +/** + * Number of rows to update per job + */ +$wgUpdateRowsPerJob = 500; + +/** + * Number of rows to update per query + */ +$wgUpdateRowsPerQuery = 10; + +/** + * Enable use of AJAX features, currently auto suggestion for the search bar + */ +$wgUseAjax = false; + +/** + * List of Ajax-callable functions + */ +$wgAjaxExportList = array( 'wfSajaxSearch' ); + +/** + * Allow DISPLAYTITLE to change title display + */ +$wgAllowDisplayTitle = false ; + +/** + * Array of usernames which may not be registered or logged in from + * Maintenance scripts can still use these + */ +$wgReservedUsernames = array( 'MediaWiki default', 'Conversion script' ); + +/** + * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't + * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading + * crap files as images. When this directive is on, will be allowed in files with + * an "image/svg" MIME type. You should leave this disabled if your web server is misconfigured + * and doesn't send appropriate MIME types for SVG images. + */ +$wgAllowTitlesInSVG = false; + +/** + * Array of namespaces which can be deemed to contain valid "content", as far + * as the site statistics are concerned. Useful if additional namespaces also + * contain "content" which should be considered when generating a count of the + * number of articles in the wiki. + */ +$wgContentNamespaces = array( NS_MAIN ); + +/** + * Maximum amount of virtual memory available to shell processes under linux, in KB. + */ +$wgMaxShellMemory = 102400; + +?> diff --git a/includes/Defines.php b/includes/Defines.php new file mode 100644 index 00000000..9ff8303b --- /dev/null +++ b/includes/Defines.php @@ -0,0 +1,183 @@ +<?php +/** + * A few constants that might be needed during LocalSettings.php + * @package MediaWiki + */ + +/** + * Version constants for the benefit of extensions + */ +define( 'MW_SPECIALPAGE_VERSION', 2 ); + +/**#@+ + * Database related constants + */ +define( 'DBO_DEBUG', 1 ); +define( 'DBO_NOBUFFER', 2 ); +define( 'DBO_IGNORE', 4 ); +define( 'DBO_TRX', 8 ); +define( 'DBO_DEFAULT', 16 ); +define( 'DBO_PERSISTENT', 32 ); +/**#@-*/ + +/**#@+ + * Virtual namespaces; don't appear in the page database + */ +define('NS_MEDIA', -2); +define('NS_SPECIAL', -1); +/**#@-*/ + +/**#@+ + * Real namespaces + * + * Number 100 and beyond are reserved for custom namespaces; + * DO NOT assign standard namespaces at 100 or beyond. + * DO NOT Change integer values as they are most probably hardcoded everywhere + * see bug #696 which talked about that. + */ +define('NS_MAIN', 0); +define('NS_TALK', 1); +define('NS_USER', 2); +define('NS_USER_TALK', 3); +define('NS_PROJECT', 4); +define('NS_PROJECT_TALK', 5); +define('NS_IMAGE', 6); +define('NS_IMAGE_TALK', 7); +define('NS_MEDIAWIKI', 8); +define('NS_MEDIAWIKI_TALK', 9); +define('NS_TEMPLATE', 10); +define('NS_TEMPLATE_TALK', 11); +define('NS_HELP', 12); +define('NS_HELP_TALK', 13); +define('NS_CATEGORY', 14); +define('NS_CATEGORY_TALK', 15); +/**#@-*/ + +/** + * Available feeds objects + * Should probably only be defined when a page is syndicated ie when + * $wgOut->isSyndicated() is true + */ +$wgFeedClasses = array( + 'rss' => 'RSSFeed', + 'atom' => 'AtomFeed', +); + +/**#@+ + * Maths constants + */ +define( 'MW_MATH_PNG', 0 ); +define( 'MW_MATH_SIMPLE', 1 ); +define( 'MW_MATH_HTML', 2 ); +define( 'MW_MATH_SOURCE', 3 ); +define( 'MW_MATH_MODERN', 4 ); +define( 'MW_MATH_MATHML', 5 ); +/**#@-*/ + +/** + * User rights management + * a big array of string defining a right, that's how they are saved in the + * database. + * @todo Is this necessary? + */ +$wgAvailableRights = array( + 'block', + 'bot', + 'createaccount', + 'delete', + 'edit', + 'editinterface', + 'import', + 'importupload', + 'move', + 'patrol', + 'protect', + 'read', + 'rollback', + 'siteadmin', + 'unwatchedpages', + 'upload', + 'userrights', +); + +/**#@+ + * Cache type + */ +define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works +define( 'CACHE_NONE', 0 ); // Do not cache +define( 'CACHE_DB', 1 ); // Store cache objects in the DB +define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers +define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available +/**#@-*/ + + + +/**#@+ + * Media types. + * This defines constants for the value returned by Image::getMediaType() + */ +define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); // unknown format +define( 'MEDIATYPE_BITMAP', 'BITMAP' ); // some bitmap image or image source (like psd, etc). Can't scale up. +define( 'MEDIATYPE_DRAWING', 'DRAWING' ); // some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. +define( 'MEDIATYPE_AUDIO', 'AUDIO' ); // simple audio file (ogg, mp3, wav, midi, whatever) +define( 'MEDIATYPE_VIDEO', 'VIDEO' ); // simple video file (ogg, mpg, etc; no not include formats here that may contain executable sections or scripts!) +define( 'MEDIATYPE_MULTIMEDIA', 'MULTIMEDIA' ); // Scriptable Multimedia (flash, advanced video container formats, etc) +define( 'MEDIATYPE_OFFICE', 'OFFICE' ); // Office Documents, Spreadsheets (office formats possibly containing apples, scripts, etc) +define( 'MEDIATYPE_TEXT', 'TEXT' ); // Plain text (possibly containing program code or scripts) +define( 'MEDIATYPE_EXECUTABLE', 'EXECUTABLE' ); // binary executable +define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); // archive file (zip, tar, etc) +/**#@-*/ + +/**#@+ + * Antivirus result codes, for use in $wgAntivirusSetup. + */ +define( 'AV_NO_VIRUS', 0 ); #scan ok, no virus found +define( 'AV_VIRUS_FOUND', 1 ); #virus found! +define( 'AV_SCAN_ABORTED', -1 ); #scan aborted, the file is probably imune +define( 'AV_SCAN_FAILED', false ); #scan failed (scanner not found or error in scanner) +/**#@-*/ + +/**#@+ + * Anti-lock flags + * See DefaultSettings.php for a description + */ +define( 'ALF_PRELOAD_LINKS', 1 ); +define( 'ALF_PRELOAD_EXISTENCE', 2 ); +define( 'ALF_NO_LINK_LOCK', 4 ); +define( 'ALF_NO_BLOCK_LOCK', 8 ); +/**#@-*/ + +/**#@+ + * Date format selectors; used in user preference storage and by + * Language::date() and co. + */ +define( 'MW_DATE_DEFAULT', '0' ); +define( 'MW_DATE_MDY', '1' ); +define( 'MW_DATE_DMY', '2' ); +define( 'MW_DATE_YMD', '3' ); +define( 'MW_DATE_ISO', 'ISO 8601' ); +/**#@-*/ + +/**#@+ + * RecentChange type identifiers + * This may be obsolete; log items are now used for moves? + */ +define( 'RC_EDIT', 0); +define( 'RC_NEW', 1); +define( 'RC_MOVE', 2); +define( 'RC_LOG', 3); +define( 'RC_MOVE_OVER_REDIRECT', 4); +/**#@-*/ + +/**#@+ + * Article edit flags + */ +define( 'EDIT_NEW', 1 ); +define( 'EDIT_UPDATE', 2 ); +define( 'EDIT_MINOR', 4 ); +define( 'EDIT_SUPPRESS_RC', 8 ); +define( 'EDIT_FORCE_BOT', 16 ); +define( 'EDIT_DEFER_UPDATES', 32 ); +/**#@-*/ + +?> diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php new file mode 100644 index 00000000..741b7199 --- /dev/null +++ b/includes/DifferenceEngine.php @@ -0,0 +1,1751 @@ +<?php +/** + * See diff.doc + * @package MediaWiki + * @subpackage DifferenceEngine + */ + +/** */ +define( 'MAX_DIFF_LINE', 10000 ); +define( 'MAX_DIFF_XREF_LENGTH', 10000 ); + +/** + * @todo document + * @public + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class DifferenceEngine { + /**#@+ + * @private + */ + var $mOldid, $mNewid, $mTitle; + var $mOldtitle, $mNewtitle, $mPagetitle; + var $mOldtext, $mNewtext; + var $mOldPage, $mNewPage; + var $mRcidMarkPatrolled; + var $mOldRev, $mNewRev; + var $mRevisionsLoaded = false; // Have the revisions been loaded + var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2? + /**#@-*/ + + /** + * Constructor + * @param $titleObj Title object that the diff is associated with + * @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) + */ + function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) { + $this->mTitle = $titleObj; + wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n"); + + if ( 'prev' === $new ) { + # Show diff between revision $old and the previous one. + # Get previous one from DB. + # + $this->mNewid = intval($old); + + $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid ); + + } elseif ( 'next' === $new ) { + # Show diff between revision $old and the previous one. + # Get previous one from DB. + # + $this->mOldid = intval($old); + $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid ); + if ( false === $this->mNewid ) { + # if no result, NewId points to the newest old revision. The only newer + # revision is cur, which is "0". + $this->mNewid = 0; + } + + } else { + $this->mOldid = intval($old); + $this->mNewid = intval($new); + } + $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer + } + + function showDiffPage() { + global $wgUser, $wgOut, $wgContLang, $wgUseExternalEditor, $wgUseRCPatrol; + $fname = 'DifferenceEngine::showDiffPage'; + wfProfileIn( $fname ); + + # If external diffs are enabled both globally and for the user, + # we'll use the application/x-external-editor interface to call + # an external diff tool like kompare, kdiff3, etc. + if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) { + global $wgInputEncoding,$wgServer,$wgScript,$wgLang; + $wgOut->disable(); + header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding ); + $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid); + $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid); + $special=$wgLang->getNsText(NS_SPECIAL); + $control=<<<CONTROL +[Process] +Type=Diff text +Engine=MediaWiki +Script={$wgServer}{$wgScript} +Special namespace={$special} + +[File] +Extension=wiki +URL=$url1 + +[File 2] +Extension=wiki +URL=$url2 +CONTROL; + echo($control); + return; + } + + $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . + "{$this->mNewid})"; + $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); + + $wgOut->setArticleFlag( false ); + if ( ! $this->loadRevisionData() ) { + $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); + $wgOut->addWikitext( $mtext ); + wfProfileOut( $fname ); + return; + } + + wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); + + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + + # mOldid is false if the difference engine is called with a "vague" query for + # a diff between a version V and its previous version V' AND the version V + # is the first version of that article. In that case, V' does not exist. + if ( $this->mOldid === false ) { + $this->showFirstRevision(); + wfProfileOut( $fname ); + return; + } + + $wgOut->suppressQuickbar(); + + $oldTitle = $this->mOldPage->getPrefixedText(); + $newTitle = $this->mNewPage->getPrefixedText(); + if( $oldTitle == $newTitle ) { + $wgOut->setPageTitle( $newTitle ); + } else { + $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle ); + } + $wgOut->setSubtitle( wfMsg( 'difference' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) { + $wgOut->loginToUse(); + $wgOut->output(); + wfProfileOut( $fname ); + exit; + } + + $sk = $wgUser->getSkin(); + $talk = $wgContLang->getNsText( NS_TALK ); + $contribs = wfMsg( 'contribslink' ); + + if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) { + $username = $this->mNewRev->getUserText(); + $rollback = '   <strong>[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'rollbacklink' ), + 'action=rollback&from=' . urlencode( $username ) . + '&token=' . urlencode( $wgUser->editToken( array( $this->mTitle->getPrefixedText(), $username ) ) ) ) . + ']</strong>'; + } else { + $rollback = ''; + } + if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed( 'patrol' ) ) { + $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']'; + } else { + $patrol = ''; + } + + $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ), + 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' ); + if ( $this->mNewRev->isCurrent() ) { + $nextlink = ' '; + } else { + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), + 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + } + + $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" . + $sk->revUserTools( $this->mOldRev ) . "<br />" . + $sk->revComment( $this->mOldRev ) . "<br />" . + $prevlink; + $newHeader = "<strong>{$this->mNewtitle}</strong><br />" . + $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . $patrol; + + $this->showDiff( $oldHeader, $newHeader ); + $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + + if( !$this->mNewRev->isCurrent() ) { + $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + } + + $this->loadNewText(); + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + $wgOut->addSecondaryWikiText( $this->mNewtext ); + + if( !$this->mNewRev->isCurrent() ) { + $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + } + + wfProfileOut( $fname ); + } + + /** + * Show the first revision of an article. Uses normal diff headers in + * contrast to normal "old revision" display style. + */ + function showFirstRevision() { + global $wgOut, $wgUser; + + $fname = 'DifferenceEngine::showFirstRevision'; + wfProfileIn( $fname ); + + # Get article text from the DB + # + 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 ); + wfProfileOut( $fname ); + return; + } + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + + # Check if user is allowed to look at this page. If not, bail out. + # + if ( !( $this->mTitle->userCanRead() ) ) { + $wgOut->loginToUse(); + $wgOut->output(); + wfProfileOut( $fname ); + exit; + } + + # Prepare the header box + # + $sk = $wgUser->getSkin(); + + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" . + $sk->revUserTools( $this->mNewRev ) . "<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . "</div>\n"; + + $wgOut->addHTML( $header ); + + $wgOut->setSubtitle( wfMsg( 'difference' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + + # Show current revision + # + $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + $wgOut->addSecondaryWikiText( $this->mNewtext ); + + wfProfileOut( $fname ); + } + + /** + * Get the diff text, send it to $wgOut + * Returns false if the diff could not be generated, otherwise returns true + */ + function showDiff( $otitle, $ntitle ) { + global $wgOut; + $diff = $this->getDiff( $otitle, $ntitle ); + if ( $diff === false ) { + $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) ); + return false; + } else { + $wgOut->addHTML( $diff ); + return true; + } + } + + /** + * Get diff table, including header + * Note that the interface has changed, it's no longer static. + * Returns false on error + */ + function getDiff( $otitle, $ntitle ) { + $body = $this->getDiffBody(); + if ( $body === false ) { + return false; + } else { + return $this->addHeader( $body, $otitle, $ntitle ); + } + } + + /** + * Get the diff table body, without header + * Results are cached + * Returns false on error + */ + function getDiffBody() { + global $wgMemc, $wgDBname; + $fname = 'DifferenceEngine::getDiffBody'; + wfProfileIn( $fname ); + + // Cacheable? + $key = false; + if ( $this->mOldid && $this->mNewid ) { + // Try cache + $key = "$wgDBname:diff: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; + } + } + + if ( !$this->loadText() ) { + wfProfileOut( $fname ); + return false; + } + + $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + + // Save to cache for 7 days + if ( $key !== false && $difftext !== false ) { + wfIncrStats( 'diff_cache_miss' ); + $wgMemc->set( $key, $difftext, 7*86400 ); + } else { + wfIncrStats( 'diff_uncacheable' ); + } + // Replace line numbers with the text in the user's language + if ( $difftext !== false ) { + $difftext = $this->localiseLineNumbers( $difftext ); + } + wfProfileOut( $fname ); + return $difftext; + } + + /** + * Generate a diff, no caching + * $otext and $ntext must be already segmented + */ + function generateDiffBody( $otext, $ntext ) { + global $wgExternalDiffEngine, $wgContLang; + $fname = 'DifferenceEngine::generateDiffBody'; + + $otext = str_replace( "\r\n", "\n", $otext ); + $ntext = str_replace( "\r\n", "\n", $ntext ); + + if ( $wgExternalDiffEngine == 'wikidiff' ) { + # For historical reasons, external diff engine expects + # input text to be HTML-escaped already + $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); + $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); + if( !function_exists( 'wikidiff_do_diff' ) ) { + dl('php_wikidiff.so'); + } + return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ); + } + + if ( $wgExternalDiffEngine == 'wikidiff2' ) { + # Better external diff engine, the 2 may some day be dropped + # This one does the escaping and segmenting itself + if ( !function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( "$fname-dl" ); + @dl('php_wikidiff2.so'); + wfProfileOut( "$fname-dl" ); + } + if ( function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( 'wikidiff2_do_diff' ); + $text = wikidiff2_do_diff( $otext, $ntext, 2 ); + wfProfileOut( 'wikidiff2_do_diff' ); + return $text; + } + } + if ( $wgExternalDiffEngine !== false ) { + # Diff via the shell + global $wgTmpDirectory; + $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); + $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); + + $tempFile1 = fopen( $tempName1, "w" ); + if ( !$tempFile1 ) { + wfProfileOut( $fname ); + return false; + } + $tempFile2 = fopen( $tempName2, "w" ); + if ( !$tempFile2 ) { + wfProfileOut( $fname ); + return false; + } + fwrite( $tempFile1, $otext ); + fwrite( $tempFile2, $ntext ); + fclose( $tempFile1 ); + fclose( $tempFile2 ); + $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); + wfProfileIn( "$fname-shellexec" ); + $difftext = wfShellExec( $cmd ); + wfProfileOut( "$fname-shellexec" ); + unlink( $tempName1 ); + unlink( $tempName2 ); + return $difftext; + } + + # Native PHP diff + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + $diffs =& new Diff( $ota, $nta ); + $formatter =& new TableDiffFormatter(); + return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); + } + + + /** + * Replace line numbers with the text in the user's language + */ + function localiseLineNumbers( $text ) { + return preg_replace_callback( '/<!--LINE (\d+)-->/', + array( &$this, 'localiseLineNumbersCb' ), $text ); + } + + function localiseLineNumbersCb( $matches ) { + global $wgLang; + return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); + } + + /** + * Add the header to a diff body + */ + function addHeader( $diff, $otitle, $ntitle ) { + $out = " + <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'> + <tr> + <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td> + <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td> + </tr> + $diff + </table> + "; + return $out; + } + + /** + * Use specified text instead of loading from the database + */ + function setText( $oldText, $newText ) { + $this->mOldtext = $oldText; + $this->mNewtext = $newText; + $this->mTextLoaded = 2; + } + + /** + * Load revision metadata for the specified articles. If newid is 0, then compare + * the old article in oldid to the current article; if oldid is 0, then + * compare the current article to the immediately previous one (ignoring the + * value of newid). + * + * If oldid is false, leave the corresponding revision object set + * to false. This is impossible via ordinary user input, and is provided for + * API convenience. + */ + function loadRevisionData() { + global $wgLang; + if ( $this->mRevisionsLoaded ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mRevisionsLoaded = true; + } + + // Load the new revision object + if( $this->mNewid ) { + $this->mNewRev = Revision::newFromId( $this->mNewid ); + } else { + $this->mNewRev = Revision::newFromTitle( $this->mTitle ); + } + + if( is_null( $this->mNewRev ) ) { + return false; + } + + // Set assorted variables + $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true ); + $this->mNewPage = $this->mNewRev->getTitle(); + if( $this->mNewRev->isCurrent() ) { + $newLink = $this->mNewPage->escapeLocalUrl(); + $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); + + $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)</strong>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + + } else { + $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); + $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); + + $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a></strong>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + } + + // Load the old revision object + $this->mOldRev = false; + if( $this->mOldid ) { + $this->mOldRev = Revision::newFromId( $this->mOldid ); + } elseif ( $this->mOldid === 0 ) { + $rev = $this->mNewRev->getPrevious(); + if( $rev ) { + $this->mOldid = $rev->getId(); + $this->mOldRev = $rev; + } else { + // No previous revision; mark to show as first-version only. + $this->mOldid = false; + $this->mOldRev = false; + } + }/* elseif ( $this->mOldid === false ) leave mOldRev false; */ + + if( is_null( $this->mOldRev ) ) { + return false; + } + + if ( $this->mOldRev ) { + $this->mOldPage = $this->mOldRev->getTitle(); + + $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 = "<strong><a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) + . "</a></strong> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + } + + return true; + } + + /** + * Load the text of the revisions, as well as revision data. + */ + function loadText() { + if ( $this->mTextLoaded == 2 ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mTextLoaded = 2; + } + + if ( !$this->loadRevisionData() ) { + return false; + } + if ( $this->mOldRev ) { + // FIXME: permission tests + $this->mOldtext = $this->mOldRev->getText(); + if ( $this->mOldtext === false ) { + return false; + } + } + if ( $this->mNewRev ) { + $this->mNewtext = $this->mNewRev->getText(); + if ( $this->mNewtext === false ) { + return false; + } + } + return true; + } + + /** + * Load the text of the new revision, not the old one + */ + function loadNewText() { + if ( $this->mTextLoaded >= 1 ) { + return true; + } else { + $this->mTextLoaded = 1; + } + if ( !$this->loadRevisionData() ) { + return false; + } + $this->mNewtext = $this->mNewRev->getText(); + return true; + } + + +} + +// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) +// +// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> +// You may copy this code freely under the conditions of the GPL. +// + +define('USE_ASSERTS', function_exists('assert')); + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp { + var $type; + var $orig; + var $closing; + + function reverse() { + trigger_error('pure virtual', E_USER_ERROR); + } + + function norig() { + return $this->orig ? sizeof($this->orig) : 0; + } + + function nclosing() { + return $this->closing ? sizeof($this->closing) : 0; + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Copy extends _DiffOp { + var $type = 'copy'; + + function _DiffOp_Copy ($orig, $closing = false) { + if (!is_array($closing)) + $closing = $orig; + $this->orig = $orig; + $this->closing = $closing; + } + + function reverse() { + return new _DiffOp_Copy($this->closing, $this->orig); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Delete extends _DiffOp { + var $type = 'delete'; + + function _DiffOp_Delete ($lines) { + $this->orig = $lines; + $this->closing = false; + } + + function reverse() { + return new _DiffOp_Add($this->orig); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Add extends _DiffOp { + var $type = 'add'; + + function _DiffOp_Add ($lines) { + $this->closing = $lines; + $this->orig = false; + } + + function reverse() { + return new _DiffOp_Delete($this->closing); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Change extends _DiffOp { + var $type = 'change'; + + function _DiffOp_Change ($orig, $closing) { + $this->orig = $orig; + $this->closing = $closing; + } + + function reverse() { + return new _DiffOp_Change($this->closing, $this->orig); + } +} + + +/** + * Class used internally by Diff to actually compute the diffs. + * + * The algorithm used here is mostly lifted from the perl module + * Algorithm::Diff (version 1.06) by Ned Konz, which is available at: + * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip + * + * More ideas are taken from: + * http://www.ics.uci.edu/~eppstein/161/960229.html + * + * Some ideas are (and a bit of code) are from from analyze.c, from GNU + * diffutils-2.7, which can be found at: + * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz + * + * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations) + * are my own. + * + * Line length limits for robustness added by Tim Starling, 2005-08-31 + * + * @author Geoffrey T. Dairiki, Tim Starling + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffEngine +{ + function diff ($from_lines, $to_lines) { + $fname = '_DiffEngine::diff'; + wfProfileIn( $fname ); + + $n_from = sizeof($from_lines); + $n_to = sizeof($to_lines); + + $this->xchanged = $this->ychanged = array(); + $this->xv = $this->yv = array(); + $this->xind = $this->yind = array(); + unset($this->seq); + unset($this->in_seq); + unset($this->lcs); + + // Skip leading common lines. + for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { + if ($from_lines[$skip] !== $to_lines[$skip]) + break; + $this->xchanged[$skip] = $this->ychanged[$skip] = false; + } + // Skip trailing common lines. + $xi = $n_from; $yi = $n_to; + for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { + if ($from_lines[$xi] !== $to_lines[$yi]) + break; + $this->xchanged[$xi] = $this->ychanged[$yi] = false; + } + + // Ignore lines which do not exist in both files. + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $xhash[$this->_line_hash($from_lines[$xi])] = 1; + } + + for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { + $line = $to_lines[$yi]; + if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) ) + continue; + $yhash[$this->_line_hash($line)] = 1; + $this->yv[] = $line; + $this->yind[] = $yi; + } + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $line = $from_lines[$xi]; + if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) ) + continue; + $this->xv[] = $line; + $this->xind[] = $xi; + } + + // Find the LCS. + $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv)); + + // Merge edits when possible + $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged); + $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged); + + // Compute the edit operations. + $edits = array(); + $xi = $yi = 0; + while ($xi < $n_from || $yi < $n_to) { + USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]); + USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]); + + // Skip matching "snake". + $copy = array(); + while ( $xi < $n_from && $yi < $n_to + && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { + $copy[] = $from_lines[$xi++]; + ++$yi; + } + if ($copy) + $edits[] = new _DiffOp_Copy($copy); + + // Find deletes & adds. + $delete = array(); + while ($xi < $n_from && $this->xchanged[$xi]) + $delete[] = $from_lines[$xi++]; + + $add = array(); + while ($yi < $n_to && $this->ychanged[$yi]) + $add[] = $to_lines[$yi++]; + + if ($delete && $add) + $edits[] = new _DiffOp_Change($delete, $add); + elseif ($delete) + $edits[] = new _DiffOp_Delete($delete); + elseif ($add) + $edits[] = new _DiffOp_Add($add); + } + wfProfileOut( $fname ); + return $edits; + } + + /** + * Returns the whole line if it's small enough, or the MD5 hash otherwise + */ + function _line_hash( $line ) { + if ( strlen( $line ) > MAX_DIFF_XREF_LENGTH ) { + return md5( $line ); + } else { + return $line; + } + } + + + /* Divide the Largest Common Subsequence (LCS) of the sequences + * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally + * sized segments. + * + * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an + * array of NCHUNKS+1 (X, Y) indexes giving the diving points between + * sub sequences. The first sub-sequence is contained in [X0, X1), + * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note + * that (X0, Y0) == (XOFF, YOFF) and + * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). + * + * This function assumes that the first lines of the specified portions + * of the two files do not match, and likewise that the last lines do not + * match. The caller must trim matching lines from the beginning and end + * of the portions it is going to specify. + */ + function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) { + $fname = '_DiffEngine::_diag'; + wfProfileIn( $fname ); + $flip = false; + + if ($xlim - $xoff > $ylim - $yoff) { + // Things seems faster (I'm not sure I understand why) + // when the shortest sequence in X. + $flip = true; + list ($xoff, $xlim, $yoff, $ylim) + = array( $yoff, $ylim, $xoff, $xlim); + } + + if ($flip) + for ($i = $ylim - 1; $i >= $yoff; $i--) + $ymatches[$this->xv[$i]][] = $i; + else + for ($i = $ylim - 1; $i >= $yoff; $i--) + $ymatches[$this->yv[$i]][] = $i; + + $this->lcs = 0; + $this->seq[0]= $yoff - 1; + $this->in_seq = array(); + $ymids[0] = array(); + + $numer = $xlim - $xoff + $nchunks - 1; + $x = $xoff; + for ($chunk = 0; $chunk < $nchunks; $chunk++) { + wfProfileIn( "$fname-chunk" ); + if ($chunk > 0) + for ($i = 0; $i <= $this->lcs; $i++) + $ymids[$i][$chunk-1] = $this->seq[$i]; + + $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks); + for ( ; $x < $x1; $x++) { + $line = $flip ? $this->yv[$x] : $this->xv[$x]; + if (empty($ymatches[$line])) + continue; + $matches = $ymatches[$line]; + reset($matches); + while (list ($junk, $y) = each($matches)) + if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + USE_ASSERTS && assert($k > 0); + $ymids[$k] = $ymids[$k-1]; + break; + } + while (list ($junk, $y) = each($matches)) { + if ($y > $this->seq[$k-1]) { + USE_ASSERTS && assert($y < $this->seq[$k]); + // Optimization: this is a common case: + // next match is just replacing previous match. + $this->in_seq[$this->seq[$k]] = false; + $this->seq[$k] = $y; + $this->in_seq[$y] = 1; + } else if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + USE_ASSERTS && assert($k > 0); + $ymids[$k] = $ymids[$k-1]; + } + } + } + wfProfileOut( "$fname-chunk" ); + } + + $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); + $ymid = $ymids[$this->lcs]; + for ($n = 0; $n < $nchunks - 1; $n++) { + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); + $y1 = $ymid[$n] + 1; + $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); + } + $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); + + wfProfileOut( $fname ); + return array($this->lcs, $seps); + } + + function _lcs_pos ($ypos) { + $fname = '_DiffEngine::_lcs_pos'; + wfProfileIn( $fname ); + + $end = $this->lcs; + if ($end == 0 || $ypos > $this->seq[$end]) { + $this->seq[++$this->lcs] = $ypos; + $this->in_seq[$ypos] = 1; + wfProfileOut( $fname ); + return $this->lcs; + } + + $beg = 1; + while ($beg < $end) { + $mid = (int)(($beg + $end) / 2); + if ( $ypos > $this->seq[$mid] ) + $beg = $mid + 1; + else + $end = $mid; + } + + USE_ASSERTS && assert($ypos != $this->seq[$end]); + + $this->in_seq[$this->seq[$end]] = false; + $this->seq[$end] = $ypos; + $this->in_seq[$ypos] = 1; + wfProfileOut( $fname ); + return $end; + } + + /* Find LCS of two sequences. + * + * The results are recorded in the vectors $this->{x,y}changed[], by + * storing a 1 in the element for each line that is an insertion + * or deletion (ie. is not in the LCS). + * + * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. + * + * Note that XLIM, YLIM are exclusive bounds. + * All line numbers are origin-0 and discarded lines are not counted. + */ + function _compareseq ($xoff, $xlim, $yoff, $ylim) { + $fname = '_DiffEngine::_compareseq'; + wfProfileIn( $fname ); + + // Slide down the bottom initial diagonal. + while ($xoff < $xlim && $yoff < $ylim + && $this->xv[$xoff] == $this->yv[$yoff]) { + ++$xoff; + ++$yoff; + } + + // Slide up the top initial diagonal. + while ($xlim > $xoff && $ylim > $yoff + && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { + --$xlim; + --$ylim; + } + + if ($xoff == $xlim || $yoff == $ylim) + $lcs = 0; + else { + // This is ad hoc but seems to work well. + //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); + //$nchunks = max(2,min(8,(int)$nchunks)); + $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; + list ($lcs, $seps) + = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks); + } + + if ($lcs == 0) { + // X and Y sequences have no common subsequence: + // mark all changed. + while ($yoff < $ylim) + $this->ychanged[$this->yind[$yoff++]] = 1; + while ($xoff < $xlim) + $this->xchanged[$this->xind[$xoff++]] = 1; + } else { + // Use the partitions to split this problem into subproblems. + reset($seps); + $pt1 = $seps[0]; + while ($pt2 = next($seps)) { + $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]); + $pt1 = $pt2; + } + } + wfProfileOut( $fname ); + } + + /* Adjust inserts/deletes of identical lines to join changes + * as much as possible. + * + * We do something when a run of changed lines include a + * line at one end and has an excluded, identical line at the other. + * We are free to choose which identical line is included. + * `compareseq' usually chooses the one at the beginning, + * but usually it is cleaner to consider the following identical line + * to be the "change". + * + * This is extracted verbatim from analyze.c (GNU diffutils-2.7). + */ + function _shift_boundaries ($lines, &$changed, $other_changed) { + $fname = '_DiffEngine::_shift_boundaries'; + wfProfileIn( $fname ); + $i = 0; + $j = 0; + + USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)'); + $len = sizeof($lines); + $other_len = sizeof($other_changed); + + while (1) { + /* + * Scan forwards to find beginning of another run of changes. + * Also keep track of the corresponding point in the other file. + * + * Throughout this code, $i and $j are adjusted together so that + * the first $i elements of $changed and the first $j elements + * of $other_changed both contain the same number of zeros + * (unchanged lines). + * Furthermore, $j is always kept so that $j == $other_len or + * $other_changed[$j] == false. + */ + while ($j < $other_len && $other_changed[$j]) + $j++; + + while ($i < $len && ! $changed[$i]) { + USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); + $i++; $j++; + while ($j < $other_len && $other_changed[$j]) + $j++; + } + + if ($i == $len) + break; + + $start = $i; + + // Find the end of this run of changes. + while (++$i < $len && $changed[$i]) + continue; + + do { + /* + * Record the length of this run of changes, so that + * we can later determine whether the run has grown. + */ + $runlength = $i - $start; + + /* + * Move the changed region back, so long as the + * previous unchanged line matches the last changed one. + * This merges with previous changed regions. + */ + while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { + $changed[--$start] = 1; + $changed[--$i] = false; + while ($start > 0 && $changed[$start - 1]) + $start--; + USE_ASSERTS && assert('$j > 0'); + while ($other_changed[--$j]) + continue; + USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); + } + + /* + * Set CORRESPONDING to the end of the changed run, at the last + * point where it corresponds to a changed run in the other file. + * CORRESPONDING == LEN means no such point has been found. + */ + $corresponding = $j < $other_len ? $i : $len; + + /* + * Move the changed region forward, so long as the + * first changed line matches the following unchanged one. + * This merges with following changed regions. + * Do this second, so that if there are no merges, + * the changed region is moved forward as far as possible. + */ + while ($i < $len && $lines[$start] == $lines[$i]) { + $changed[$start++] = false; + $changed[$i++] = 1; + while ($i < $len && $changed[$i]) + $i++; + + USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); + $j++; + if ($j < $other_len && $other_changed[$j]) { + $corresponding = $i; + while ($j < $other_len && $other_changed[$j]) + $j++; + } + } + } while ($runlength != $i - $start); + + /* + * If possible, move the fully-merged run of changes + * back to a corresponding run in the other file. + */ + while ($corresponding < $i) { + $changed[--$start] = 1; + $changed[--$i] = 0; + USE_ASSERTS && assert('$j > 0'); + while ($other_changed[--$j]) + continue; + USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); + } + } + wfProfileOut( $fname ); + } +} + +/** + * Class representing a 'diff' between two sequences of strings. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class Diff +{ + var $edits; + + /** + * Constructor. + * Computes diff between sequences of strings. + * + * @param $from_lines array An array of strings. + * (Typically these are lines from a file.) + * @param $to_lines array An array of strings. + */ + function Diff($from_lines, $to_lines) { + $eng = new _DiffEngine; + $this->edits = $eng->diff($from_lines, $to_lines); + //$this->_check($from_lines, $to_lines); + } + + /** + * Compute reversed Diff. + * + * SYNOPSIS: + * + * $diff = new Diff($lines1, $lines2); + * $rev = $diff->reverse(); + * @return object A Diff object representing the inverse of the + * original diff. + */ + function reverse () { + $rev = $this; + $rev->edits = array(); + foreach ($this->edits as $edit) { + $rev->edits[] = $edit->reverse(); + } + return $rev; + } + + /** + * Check for empty diff. + * + * @return bool True iff two sequences were identical. + */ + function isEmpty () { + foreach ($this->edits as $edit) { + if ($edit->type != 'copy') + return false; + } + return true; + } + + /** + * Compute the length of the Longest Common Subsequence (LCS). + * + * This is mostly for diagnostic purposed. + * + * @return int The length of the LCS. + */ + function lcs () { + $lcs = 0; + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $lcs += sizeof($edit->orig); + } + return $lcs; + } + + /** + * Get the original set of lines. + * + * This reconstructs the $from_lines parameter passed to the + * constructor. + * + * @return array The original sequence of strings. + */ + function orig() { + $lines = array(); + + foreach ($this->edits as $edit) { + if ($edit->orig) + array_splice($lines, sizeof($lines), 0, $edit->orig); + } + return $lines; + } + + /** + * Get the closing set of lines. + * + * This reconstructs the $to_lines parameter passed to the + * constructor. + * + * @return array The sequence of strings. + */ + function closing() { + $lines = array(); + + foreach ($this->edits as $edit) { + if ($edit->closing) + array_splice($lines, sizeof($lines), 0, $edit->closing); + } + return $lines; + } + + /** + * Check a Diff for validity. + * + * This is here only for debugging purposes. + */ + function _check ($from_lines, $to_lines) { + $fname = 'Diff::_check'; + wfProfileIn( $fname ); + if (serialize($from_lines) != serialize($this->orig())) + trigger_error("Reconstructed original doesn't match", E_USER_ERROR); + if (serialize($to_lines) != serialize($this->closing())) + trigger_error("Reconstructed closing doesn't match", E_USER_ERROR); + + $rev = $this->reverse(); + if (serialize($to_lines) != serialize($rev->orig())) + trigger_error("Reversed original doesn't match", E_USER_ERROR); + if (serialize($from_lines) != serialize($rev->closing())) + trigger_error("Reversed closing doesn't match", E_USER_ERROR); + + + $prevtype = 'none'; + foreach ($this->edits as $edit) { + if ( $prevtype == $edit->type ) + trigger_error("Edit sequence is non-optimal", E_USER_ERROR); + $prevtype = $edit->type; + } + + $lcs = $this->lcs(); + trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE); + wfProfileOut( $fname ); + } +} + +/** + * FIXME: bad name. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class MappedDiff extends Diff +{ + /** + * Constructor. + * + * Computes diff between sequences of strings. + * + * This can be used to compute things like + * case-insensitve diffs, or diffs which ignore + * changes in white-space. + * + * @param $from_lines array An array of strings. + * (Typically these are lines from a file.) + * + * @param $to_lines array An array of strings. + * + * @param $mapped_from_lines array This array should + * have the same size number of elements as $from_lines. + * The elements in $mapped_from_lines and + * $mapped_to_lines are what is actually compared + * when computing the diff. + * + * @param $mapped_to_lines array This array should + * have the same number of elements as $to_lines. + */ + function MappedDiff($from_lines, $to_lines, + $mapped_from_lines, $mapped_to_lines) { + $fname = 'MappedDiff::MappedDiff'; + wfProfileIn( $fname ); + + assert(sizeof($from_lines) == sizeof($mapped_from_lines)); + assert(sizeof($to_lines) == sizeof($mapped_to_lines)); + + $this->Diff($mapped_from_lines, $mapped_to_lines); + + $xi = $yi = 0; + for ($i = 0; $i < sizeof($this->edits); $i++) { + $orig = &$this->edits[$i]->orig; + if (is_array($orig)) { + $orig = array_slice($from_lines, $xi, sizeof($orig)); + $xi += sizeof($orig); + } + + $closing = &$this->edits[$i]->closing; + if (is_array($closing)) { + $closing = array_slice($to_lines, $yi, sizeof($closing)); + $yi += sizeof($closing); + } + } + wfProfileOut( $fname ); + } +} + +/** + * A class to format Diffs + * + * This class formats the diff in classic diff format. + * It is intended that this class be customized via inheritance, + * to obtain fancier outputs. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class DiffFormatter +{ + /** + * Number of leading context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + var $leading_context_lines = 0; + + /** + * Number of trailing context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + var $trailing_context_lines = 0; + + /** + * Format a diff. + * + * @param $diff object A Diff object. + * @return string The formatted output. + */ + function format($diff) { + $fname = 'DiffFormatter::format'; + wfProfileIn( $fname ); + + $xi = $yi = 1; + $block = false; + $context = array(); + + $nlead = $this->leading_context_lines; + $ntrail = $this->trailing_context_lines; + + $this->_start_diff(); + + foreach ($diff->edits as $edit) { + if ($edit->type == 'copy') { + if (is_array($block)) { + if (sizeof($edit->orig) <= $nlead + $ntrail) { + $block[] = $edit; + } + else{ + if ($ntrail) { + $context = array_slice($edit->orig, 0, $ntrail); + $block[] = new _DiffOp_Copy($context); + } + $this->_block($x0, $ntrail + $xi - $x0, + $y0, $ntrail + $yi - $y0, + $block); + $block = false; + } + } + $context = $edit->orig; + } + else { + if (! is_array($block)) { + $context = array_slice($context, sizeof($context) - $nlead); + $x0 = $xi - sizeof($context); + $y0 = $yi - sizeof($context); + $block = array(); + if ($context) + $block[] = new _DiffOp_Copy($context); + } + $block[] = $edit; + } + + if ($edit->orig) + $xi += sizeof($edit->orig); + if ($edit->closing) + $yi += sizeof($edit->closing); + } + + if (is_array($block)) + $this->_block($x0, $xi - $x0, + $y0, $yi - $y0, + $block); + + $end = $this->_end_diff(); + wfProfileOut( $fname ); + return $end; + } + + function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) { + $fname = 'DiffFormatter::_block'; + wfProfileIn( $fname ); + $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); + foreach ($edits as $edit) { + if ($edit->type == 'copy') + $this->_context($edit->orig); + elseif ($edit->type == 'add') + $this->_added($edit->closing); + elseif ($edit->type == 'delete') + $this->_deleted($edit->orig); + elseif ($edit->type == 'change') + $this->_changed($edit->orig, $edit->closing); + else + trigger_error('Unknown edit type', E_USER_ERROR); + } + $this->_end_block(); + wfProfileOut( $fname ); + } + + function _start_diff() { + ob_start(); + } + + function _end_diff() { + $val = ob_get_contents(); + ob_end_clean(); + return $val; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) { + if ($xlen > 1) + $xbeg .= "," . ($xbeg + $xlen - 1); + if ($ylen > 1) + $ybeg .= "," . ($ybeg + $ylen - 1); + + return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; + } + + function _start_block($header) { + echo $header; + } + + function _end_block() { + } + + function _lines($lines, $prefix = ' ') { + foreach ($lines as $line) + echo "$prefix $line\n"; + } + + function _context($lines) { + $this->_lines($lines); + } + + function _added($lines) { + $this->_lines($lines, '>'); + } + function _deleted($lines) { + $this->_lines($lines, '<'); + } + + function _changed($orig, $closing) { + $this->_deleted($orig); + echo "---\n"; + $this->_added($closing); + } +} + + +/** + * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 + * + */ + +define('NBSP', ' '); // iso-8859-x non-breaking space. + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _HWLDF_WordAccumulator { + function _HWLDF_WordAccumulator () { + $this->_lines = array(); + $this->_line = ''; + $this->_group = ''; + $this->_tag = ''; + } + + function _flushGroup ($new_tag) { + if ($this->_group !== '') { + if ($this->_tag == 'mark') + $this->_line .= '<span class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</span>'; + else + $this->_line .= htmlspecialchars ( $this->_group ); + } + $this->_group = ''; + $this->_tag = $new_tag; + } + + function _flushLine ($new_tag) { + $this->_flushGroup($new_tag); + if ($this->_line != '') + array_push ( $this->_lines, $this->_line ); + else + # make empty lines visible by inserting an NBSP + array_push ( $this->_lines, NBSP ); + $this->_line = ''; + } + + function addWords ($words, $tag = '') { + if ($tag != $this->_tag) + $this->_flushGroup($tag); + + foreach ($words as $word) { + // new-line should only come as first char of word. + if ($word == '') + continue; + if ($word[0] == "\n") { + $this->_flushLine($tag); + $word = substr($word, 1); + } + assert(!strstr($word, "\n")); + $this->_group .= $word; + } + } + + function getLines() { + $this->_flushLine('~done'); + return $this->_lines; + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class WordLevelDiff extends MappedDiff +{ + function WordLevelDiff ($orig_lines, $closing_lines) { + $fname = 'WordLevelDiff::WordLevelDiff'; + wfProfileIn( $fname ); + + list ($orig_words, $orig_stripped) = $this->_split($orig_lines); + list ($closing_words, $closing_stripped) = $this->_split($closing_lines); + + $this->MappedDiff($orig_words, $closing_words, + $orig_stripped, $closing_stripped); + wfProfileOut( $fname ); + } + + function _split($lines) { + $fname = 'WordLevelDiff::_split'; + wfProfileIn( $fname ); + + $words = array(); + $stripped = array(); + $first = true; + foreach ( $lines as $line ) { + # If the line is too long, just pretend the entire line is one big word + # This prevents resource exhaustion problems + if ( $first ) { + $first = false; + } else { + $words[] = "\n"; + $stripped[] = "\n"; + } + if ( strlen( $line ) > MAX_DIFF_LINE ) { + $words[] = $line; + $stripped[] = $line; + } else { + if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', + $line, $m)) + { + $words = array_merge( $words, $m[0] ); + $stripped = array_merge( $stripped, $m[1] ); + } + } + } + wfProfileOut( $fname ); + return array($words, $stripped); + } + + function orig () { + $fname = 'WordLevelDiff::orig'; + wfProfileIn( $fname ); + $orig = new _HWLDF_WordAccumulator; + + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $orig->addWords($edit->orig); + elseif ($edit->orig) + $orig->addWords($edit->orig, 'mark'); + } + $lines = $orig->getLines(); + wfProfileOut( $fname ); + return $lines; + } + + function closing () { + $fname = 'WordLevelDiff::closing'; + wfProfileIn( $fname ); + $closing = new _HWLDF_WordAccumulator; + + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $closing->addWords($edit->closing); + elseif ($edit->closing) + $closing->addWords($edit->closing, 'mark'); + } + $lines = $closing->getLines(); + wfProfileOut( $fname ); + return $lines; + } +} + +/** + * Wikipedia Table style diff formatter. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class TableDiffFormatter extends DiffFormatter +{ + function TableDiffFormatter() { + $this->leading_context_lines = 2; + $this->trailing_context_lines = 2; + } + + function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { + $r = '<tr><td colspan="2" align="left"><strong><!--LINE '.$xbeg."--></strong></td>\n" . + '<td colspan="2" align="left"><strong><!--LINE '.$ybeg."--></strong></td></tr>\n"; + return $r; + } + + function _start_block( $header ) { + echo $header; + } + + function _end_block() { + } + + function _lines( $lines, $prefix=' ', $color='white' ) { + } + + # HTML-escape parameter before calling this + function addedLine( $line ) { + return "<td>+</td><td class='diff-addedline'>{$line}</td>"; + } + + # HTML-escape parameter before calling this + function deletedLine( $line ) { + return "<td>-</td><td class='diff-deletedline'>{$line}</td>"; + } + + # HTML-escape parameter before calling this + function contextLine( $line ) { + return "<td> </td><td class='diff-context'>{$line}</td>"; + } + + function emptyLine() { + return '<td colspan="2"> </td>'; + } + + function _added( $lines ) { + foreach ($lines as $line) { + echo '<tr>' . $this->emptyLine() . + $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + } + } + + function _deleted($lines) { + foreach ($lines as $line) { + echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) . + $this->emptyLine() . "</tr>\n"; + } + } + + function _context( $lines ) { + foreach ($lines as $line) { + echo '<tr>' . + $this->contextLine( htmlspecialchars ( $line ) ) . + $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + } + } + + function _changed( $orig, $closing ) { + $fname = 'TableDiffFormatter::_changed'; + wfProfileIn( $fname ); + + $diff = new WordLevelDiff( $orig, $closing ); + $del = $diff->orig(); + $add = $diff->closing(); + + # Notice that WordLevelDiff returns HTML-escaped output. + # Hence, we will be calling addedLine/deletedLine without HTML-escaping. + + while ( $line = array_shift( $del ) ) { + $aline = array_shift( $add ); + echo '<tr>' . $this->deletedLine( $line ) . + $this->addedLine( $aline ) . "</tr>\n"; + } + foreach ($add as $line) { # If any leftovers + echo '<tr>' . $this->emptyLine() . + $this->addedLine( $line ) . "</tr>\n"; + } + wfProfileOut( $fname ); + } +} + +?> diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php new file mode 100644 index 00000000..b857fa66 --- /dev/null +++ b/includes/DjVuImage.php @@ -0,0 +1,214 @@ +<?php +/** + * Support for detecting/validating DjVu image files and getting + * some basic file metadata (resolution etc) + * + * File format docs are available in source package for DjVuLibre: + * http://djvulibre.djvuzone.org/ + * + * + * Copyright (C) 2006 Brion Vibber <brion@pobox.com> + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @package MediaWiki + */ + +class DjVuImage { + function __construct( $filename ) { + $this->mFilename = $filename; + } + + /** + * Check if the given file is indeed a valid DjVu image file + * @return bool + */ + public function isValid() { + $info = $this->getInfo(); + return $info !== false; + } + + + /** + * Return data in the style of getimagesize() + * @return array or false on failure + */ + public function getImageSize() { + $data = $this->getInfo(); + + if( $data !== false ) { + $width = $data['width']; + $height = $data['height']; + + return array( $width, $height, 'DjVu', + "width=\"$width\" height=\"$height\"" ); + } + return false; + } + + // --------- + + /** + * For debugging; dump the IFF chunk structure + */ + function dump() { + $file = fopen( $this->mFilename, 'rb' ); + $header = fread( $file, 12 ); + extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); + echo "$chunk $chunkLength\n"; + $this->dumpForm( $file, $chunkLength, 1 ); + fclose( $file ); + } + + private function dumpForm( $file, $length, $indent ) { + $start = ftell( $file ); + $secondary = fread( $file, 4 ); + echo str_repeat( ' ', $indent * 4 ) . "($secondary)\n"; + while( ftell( $file ) - $start < $length ) { + $chunkHeader = fread( $file, 8 ); + if( $chunkHeader == '' ) { + break; + } + extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); + echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; + + if( $chunk == 'FORM' ) { + $this->dumpForm( $file, $chunkLength, $indent + 1 ); + } else { + fseek( $file, $chunkLength, SEEK_CUR ); + if( $chunkLength & 1 == 1 ) { + // Padding byte between chunks + fseek( $file, 1, SEEK_CUR ); + } + } + } + } + + function getInfo() { + $file = fopen( $this->mFilename, 'rb' ); + if( $file === false ) { + wfDebug( __METHOD__ . ": missing or failed file read\n" ); + return false; + } + + $header = fread( $file, 16 ); + $info = false; + + if( strlen( $header ) < 16 ) { + wfDebug( __METHOD__ . ": too short file header\n" ); + } else { + extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); + + if( $magic != 'AT&T' ) { + wfDebug( __METHOD__ . ": not a DjVu file\n" ); + } elseif( $subtype == 'DJVU' ) { + // Single-page document + $info = $this->getPageInfo( $file, $formLength ); + } elseif( $subtype == 'DJVM' ) { + // Multi-page document + $info = $this->getMultiPageInfo( $file, $formLength ); + } else { + wfDebug( __METHOD__ . ": unrecognized DJVU file type '$formType'\n" ); + } + } + fclose( $file ); + return $info; + } + + private function readChunk( $file ) { + $header = fread( $file, 8 ); + if( strlen( $header ) < 8 ) { + return array( false, 0 ); + } else { + extract( unpack( 'a4chunk/Nlength', $header ) ); + return array( $chunk, $length ); + } + } + + private function skipChunk( $file, $chunkLength ) { + fseek( $file, $chunkLength, SEEK_CUR ); + + if( $chunkLength & 0x01 == 1 && !feof( $file ) ) { + // padding byte + fseek( $file, 1, SEEK_CUR ); + } + } + + private function getMultiPageInfo( $file, $formLength ) { + // For now, we'll just look for the first page in the file + // and report its information, hoping others are the same size. + $start = ftell( $file ); + do { + list( $chunk, $length ) = $this->readChunk( $file ); + if( !$chunk ) { + break; + } + + if( $chunk == 'FORM' ) { + $subtype = fread( $file, 4 ); + if( $subtype == 'DJVU' ) { + wfDebug( __METHOD__ . ": found first subpage\n" ); + return $this->getPageInfo( $file, $length ); + } + $this->skipChunk( $file, $length - 4 ); + } else { + wfDebug( __METHOD__ . ": skipping '$chunk' chunk\n" ); + $this->skipChunk( $file, $length ); + } + } while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength ); + + wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" ); + return false; + } + + private function getPageInfo( $file, $formLength ) { + list( $chunk, $length ) = $this->readChunk( $file ); + if( $chunk != 'INFO' ) { + wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" ); + return false; + } + + if( $length < 9 ) { + wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" ); + return false; + } + $data = fread( $file, $length ); + if( strlen( $data ) < $length ) { + wfDebug( __METHOD__ . ": INFO chunk cut off\n" ); + return false; + } + + extract( unpack( + 'nwidth/' . + 'nheight/' . + 'Cminor/' . + 'Cmajor/' . + 'vresolution/' . + 'Cgamma', $data ) ); + # Newer files have rotation info in byte 10, but we don't use it yet. + + return array( + 'width' => $width, + 'height' => $height, + 'version' => "$major.$minor", + 'resolution' => $resolution, + 'gamma' => $gamma / 10.0 ); + } +} + + +?> \ No newline at end of file diff --git a/includes/EditPage.php b/includes/EditPage.php new file mode 100644 index 00000000..d43a1202 --- /dev/null +++ b/includes/EditPage.php @@ -0,0 +1,1864 @@ +<?php +/** + * Contain the EditPage class + * @package MediaWiki + */ + +/** + * Splitting edit page/HTML interface from Article... + * The actual database and text munging is still in Article, + * but it should get easier to call those from alternate + * interfaces. + * + * @package MediaWiki + */ + +class EditPage { + var $mArticle; + var $mTitle; + var $mMetaData = ''; + var $isConflict = false; + var $isCssJsSubpage = false; + var $deletedSinceEdit = false; + var $formtype; + var $firsttime; + var $lastDelete; + var $mTokenOk = false; + var $mTriedSave = false; + var $tooBig = false; + var $kblength = false; + var $missingComment = false; + var $missingSummary = false; + var $allowBlankSummary = false; + var $autoSumm = ''; + var $hookError = ''; + + # Form values + var $save = false, $preview = false, $diff = false; + var $minoredit = false, $watchthis = false, $recreate = false; + var $textbox1 = '', $textbox2 = '', $summary = ''; + var $edittime = '', $section = '', $starttime = ''; + var $oldid = 0, $editintro = '', $scrolltop = null; + + /** + * @todo document + * @param $article + */ + function EditPage( $article ) { + $this->mArticle =& $article; + global $wgTitle; + $this->mTitle =& $wgTitle; + } + + /** + * Fetch initial editing page content. + */ + private function getContent() { + global $wgRequest, $wgParser; + + # Get variables from query string :P + $section = $wgRequest->getVal( 'section' ); + $preload = $wgRequest->getVal( 'preload' ); + + wfProfileIn( __METHOD__ ); + + $text = ''; + if( !$this->mTitle->exists() ) { + + # If requested, preload some text. + $text = $this->getPreloadedText( $preload ); + + # We used to put MediaWiki:Newarticletext here if + # $text was empty at this point. + # This is now shown above the edit box instead. + } else { + // FIXME: may be better to use Revision class directly + // But don't mess with it just yet. Article knows how to + // fetch the page record from the high-priority server, + // which is needed to guarantee we don't pick up lagged + // information. + + $text = $this->mArticle->getContent(); + + if( $section != '' ) { + if( $section == 'new' ) { + $text = $this->getPreloadedText( $preload ); + } else { + $text = $wgParser->getSection( $text, $section ); + } + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Get the contents of a page from its title and remove includeonly tags + * + * @param $preload String: the title of the page. + * @return string The contents of the page. + */ + private function getPreloadedText($preload) { + if ( $preload === '' ) + return ''; + else { + $preloadTitle = Title::newFromText( $preload ); + if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) { + $rev=Revision::newFromTitle($preloadTitle); + if ( is_object( $rev ) ) { + $text = $rev->getText(); + // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing + // its own mini-parser! -ævar + $text = preg_replace( '~</?includeonly>~', '', $text ); + return $text; + } else + return ''; + } + } + } + + /** + * This is the function that extracts metadata from the article body on the first view. + * To turn the feature on, set $wgUseMetadataEdit = true ; in LocalSettings + * and set $wgMetadataWhitelist to the *full* title of the template whitelist + */ + function extractMetaDataFromArticle () { + global $wgUseMetadataEdit , $wgMetadataWhitelist , $wgLang ; + $this->mMetaData = '' ; + if ( !$wgUseMetadataEdit ) return ; + if ( $wgMetadataWhitelist == '' ) return ; + $s = '' ; + $t = $this->getContent(); + + # MISSING : <nowiki> filtering + + # Categories and language links + $t = explode ( "\n" , $t ) ; + $catlow = strtolower ( $wgLang->getNsText ( NS_CATEGORY ) ) ; + $cat = $ll = array() ; + foreach ( $t AS $key => $x ) + { + $y = trim ( strtolower ( $x ) ) ; + while ( substr ( $y , 0 , 2 ) == '[[' ) + { + $y = explode ( ']]' , trim ( $x ) ) ; + $first = array_shift ( $y ) ; + $first = explode ( ':' , $first ) ; + $ns = array_shift ( $first ) ; + $ns = trim ( str_replace ( '[' , '' , $ns ) ) ; + if ( strlen ( $ns ) == 2 OR strtolower ( $ns ) == $catlow ) + { + $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]' ; + if ( strtolower ( $ns ) == $catlow ) $cat[] = $add ; + else $ll[] = $add ; + $x = implode ( ']]' , $y ) ; + $t[$key] = $x ; + $y = trim ( strtolower ( $x ) ) ; + } + } + } + if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n" ; + if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n" ; + $t = implode ( "\n" , $t ) ; + + # Load whitelist + $sat = array () ; # stand-alone-templates; must be lowercase + $wl_title = Title::newFromText ( $wgMetadataWhitelist ) ; + $wl_article = new Article ( $wl_title ) ; + $wl = explode ( "\n" , $wl_article->getContent() ) ; + foreach ( $wl AS $x ) + { + $isentry = false ; + $x = trim ( $x ) ; + while ( substr ( $x , 0 , 1 ) == '*' ) + { + $isentry = true ; + $x = trim ( substr ( $x , 1 ) ) ; + } + if ( $isentry ) + { + $sat[] = strtolower ( $x ) ; + } + + } + + # Templates, but only some + $t = explode ( '{{' , $t ) ; + $tl = array () ; + foreach ( $t AS $key => $x ) + { + $y = explode ( '}}' , $x , 2 ) ; + if ( count ( $y ) == 2 ) + { + $z = $y[0] ; + $z = explode ( '|' , $z ) ; + $tn = array_shift ( $z ) ; + if ( in_array ( strtolower ( $tn ) , $sat ) ) + { + $tl[] = '{{' . $y[0] . '}}' ; + $t[$key] = $y[1] ; + $y = explode ( '}}' , $y[1] , 2 ) ; + } + else $t[$key] = '{{' . $x ; + } + else if ( $key != 0 ) $t[$key] = '{{' . $x ; + else $t[$key] = $x ; + } + if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ) ; + $t = implode ( '' , $t ) ; + + $t = str_replace ( "\n\n\n" , "\n" , $t ) ; + $this->mArticle->mContent = $t ; + $this->mMetaData = $s ; + } + + function submit() { + $this->edit(); + } + + /** + * This is the function that gets called for "action=edit". It + * sets up various member variables, then passes execution to + * another function, usually showEditForm() + * + * The edit form is self-submitting, so that when things like + * preview and edit conflicts occur, we get the same form back + * with the extra stuff added. Only when the final submission + * is made and all is well do we actually save and redirect to + * the newly-edited page. + */ + function edit() { + global $wgOut, $wgUser, $wgRequest, $wgTitle; + global $wgEmailConfirmToEdit; + + if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + return; + + $fname = 'EditPage::edit'; + wfProfileIn( $fname ); + wfDebug( "$fname: enter\n" ); + + // this is not an article + $wgOut->setArticleFlag(false); + + $this->importFormData( $wgRequest ); + $this->firsttime = false; + + if( $this->live ) { + $this->livePreview(); + wfProfileOut( $fname ); + return; + } + + if ( ! $this->mTitle->userCanEdit() ) { + wfDebug( "$fname: user can't edit\n" ); + $wgOut->readOnlyPage( $this->getContent(), true ); + wfProfileOut( $fname ); + return; + } + wfDebug( "$fname: Checking blocks\n" ); + if ( !$this->preview && !$this->diff && $wgUser->isBlockedFrom( $this->mTitle, !$this->save ) ) { + # When previewing, don't check blocked state - will get caught at save time. + # Also, check when starting edition is done against slave to improve performance. + wfDebug( "$fname: user is blocked\n" ); + $this->blockedPage(); + wfProfileOut( $fname ); + return; + } + if ( !$wgUser->isAllowed('edit') ) { + if ( $wgUser->isAnon() ) { + wfDebug( "$fname: user must log in\n" ); + $this->userNotLoggedInPage(); + wfProfileOut( $fname ); + return; + } else { + wfDebug( "$fname: read-only page\n" ); + $wgOut->readOnlyPage( $this->getContent(), true ); + wfProfileOut( $fname ); + return; + } + } + if ($wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed()) { + wfDebug("$fname: user must confirm e-mail address\n"); + $this->userNotConfirmedPage(); + wfProfileOut($fname); + return; + } + if ( !$this->mTitle->userCanCreate() && !$this->mTitle->exists() ) { + wfDebug( "$fname: no create permission\n" ); + $this->noCreatePermission(); + wfProfileOut( $fname ); + return; + } + if ( wfReadOnly() ) { + wfDebug( "$fname: read-only mode is engaged\n" ); + if( $this->save || $this->preview ) { + $this->formtype = 'preview'; + } else if ( $this->diff ) { + $this->formtype = 'diff'; + } else { + $wgOut->readOnlyPage( $this->getContent() ); + wfProfileOut( $fname ); + return; + } + } else { + if ( $this->save ) { + $this->formtype = 'save'; + } else if ( $this->preview ) { + $this->formtype = 'preview'; + } else if ( $this->diff ) { + $this->formtype = 'diff'; + } else { # First time through + $this->firsttime = true; + if( $this->previewOnOpen() ) { + $this->formtype = 'preview'; + } else { + $this->extractMetaDataFromArticle () ; + $this->formtype = 'initial'; + } + } + } + + wfProfileIn( "$fname-business-end" ); + + $this->isConflict = false; + // css / js subpages of user pages get a special treatment + $this->isCssJsSubpage = $wgTitle->isCssJsSubpage(); + $this->isValidCssJsSubpage = $wgTitle->isValidCssJsSubpage(); + + /* Notice that we can't use isDeleted, because it returns true if article is ever deleted + * no matter it's current state + */ + $this->deletedSinceEdit = false; + if ( $this->edittime != '' ) { + /* Note that we rely on logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. This is done on every preview and save request. Move it further down + * to only perform it on saves + */ + if ( $this->mTitle->isDeleted() ) { + $this->lastDelete = $this->getLastDelete(); + if ( !is_null($this->lastDelete) ) { + $deletetime = $this->lastDelete->log_timestamp; + if ( ($deletetime - $this->starttime) > 0 ) { + $this->deletedSinceEdit = true; + } + } + } + } + + if(!$this->mTitle->getArticleID() && ('initial' == $this->formtype || $this->firsttime )) { # new article + $this->showIntro(); + } + if( $this->mTitle->isTalkPage() ) { + $wgOut->addWikiText( wfMsg( 'talkpagetext' ) ); + } + + # Attempt submission here. This will check for edit conflicts, + # and redundantly check for locked database, blocked IPs, etc. + # that edit() already checked just in case someone tries to sneak + # in the back door with a hand-edited submission URL. + + if ( 'save' == $this->formtype ) { + if ( !$this->attemptSave() ) { + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + return; + } + } + + # First time through: get contents, set time for conflict + # checking, etc. + if ( 'initial' == $this->formtype || $this->firsttime ) { + $this->initialiseForm(); + if( !$this->mTitle->getArticleId() ) + wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); + } + + $this->showEditForm(); + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + } + + /** + * Return true if this page should be previewed when the edit form + * is initially opened. + * @return bool + * @private + */ + function previewOnOpen() { + global $wgUser; + return $this->section != 'new' && + ( ( $wgUser->getOption( 'previewonfirst' ) && $this->mTitle->exists() ) || + ( $this->mTitle->getNamespace() == NS_CATEGORY && + !$this->mTitle->exists() ) ); + } + + /** + * @todo document + * @param $request + */ + function importFormData( &$request ) { + global $wgLang, $wgUser; + $fname = 'EditPage::importFormData'; + wfProfileIn( $fname ); + + if( $request->wasPosted() ) { + # These fields need to be checked for encoding. + # Also remove trailing whitespace, but don't remove _initial_ + # whitespace from the text boxes. This may be significant formatting. + $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); + $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); + $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); + # Truncate for whole multibyte characters. +5 bytes for ellipsis + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + + $this->edittime = $request->getVal( 'wpEdittime' ); + $this->starttime = $request->getVal( 'wpStarttime' ); + + $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); + + if( is_null( $this->edittime ) ) { + # If the form is incomplete, force to preview. + wfDebug( "$fname: Form data appears to be incomplete\n" ); + wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); + $this->preview = true; + } else { + /* Fallback for live preview */ + $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' ); + $this->diff = $request->getCheck( 'wpDiff' ); + + // Remember whether a save was requested, so we can indicate + // if we forced preview due to session failure. + $this->mTriedSave = !$this->preview; + + if ( $this->tokenOk( $request ) ) { + # Some browsers will not report any submit button + # if the user hits enter in the comment box. + # The unmarked state will be assumed to be a save, + # if the form seems otherwise complete. + wfDebug( "$fname: Passed token check.\n" ); + } else { + # Page might be a hack attempt posted from + # an external site. Preview instead of saving. + wfDebug( "$fname: Failed token check; forcing preview\n" ); + $this->preview = true; + } + } + $this->save = ! ( $this->preview OR $this->diff ); + if( !preg_match( '/^\d{14}$/', $this->edittime )) { + $this->edittime = null; + } + + if( !preg_match( '/^\d{14}$/', $this->starttime )) { + $this->starttime = null; + } + + $this->recreate = $request->getCheck( 'wpRecreate' ); + + $this->minoredit = $request->getCheck( 'wpMinoredit' ); + $this->watchthis = $request->getCheck( 'wpWatchthis' ); + + # Don't force edit summaries when a user is editing their own user or talk page + if( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && $this->mTitle->getText() == $wgUser->getName() ) { + $this->allowBlankSummary = true; + } else { + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ); + } + + $this->autoSumm = $request->getText( 'wpAutoSummary' ); + } else { + # Not a posted form? Start with nothing. + wfDebug( "$fname: Not a posted form.\n" ); + $this->textbox1 = ''; + $this->textbox2 = ''; + $this->mMetaData = ''; + $this->summary = ''; + $this->edittime = ''; + $this->starttime = wfTimestampNow(); + $this->preview = false; + $this->save = false; + $this->diff = false; + $this->minoredit = false; + $this->watchthis = false; + $this->recreate = false; + } + + $this->oldid = $request->getInt( 'oldid' ); + + # Section edit can come from either the form or a link + $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); + + $this->live = $request->getCheck( 'live' ); + $this->editintro = $request->getText( 'editintro' ); + + wfProfileOut( $fname ); + } + + /** + * Make sure the form isn't faking a user's credentials. + * + * @param $request WebRequest + * @return bool + * @private + */ + function tokenOk( &$request ) { + global $wgUser; + if( $wgUser->isAnon() ) { + # Anonymous users may not have a session + # open. Don't tokenize. + $this->mTokenOk = true; + } else { + $this->mTokenOk = $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); + } + return $this->mTokenOk; + } + + /** */ + function showIntro() { + global $wgOut, $wgUser; + $addstandardintro=true; + if($this->editintro) { + $introtitle=Title::newFromText($this->editintro); + if(isset($introtitle) && $introtitle->userCanRead()) { + $rev=Revision::newFromTitle($introtitle); + if($rev) { + $wgOut->addSecondaryWikiText($rev->getText()); + $addstandardintro=false; + } + } + } + if($addstandardintro) { + if ( $wgUser->isLoggedIn() ) + $wgOut->addWikiText( wfMsg( 'newarticletext' ) ); + else + $wgOut->addWikiText( wfMsg( 'newarticletextanon' ) ); + } + } + + /** + * Attempt submission + * @return bool false if output is done, true if the rest of the form should be displayed + */ + function attemptSave() { + global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut; + global $wgMaxArticleSize; + + $fname = 'EditPage::attemptSave'; + wfProfileIn( $fname ); + wfProfileIn( "$fname-checks" ); + + # Reintegrate metadata + if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; + $this->mMetaData = '' ; + + # Check for spam + if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { + $this->spamPage ( $matches[0] ); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + 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; + } + 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; + } elseif( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + wfProfileOut( "$fname-checks " ); + wfProfileOut( $fname ); + return true; + } + if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + # Check block state against master, thus 'false'. + $this->blockedPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + // Error will be displayed by showEditForm() + $this->tooBig = true; + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } + + if ( !$wgUser->isAllowed('edit') ) { + if ( $wgUser->isAnon() ) { + $this->userNotLoggedInPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + else { + $wgOut->readOnlyPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + } + + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + if ( $wgUser->pingLimiter() ) { + $wgOut->rateLimited(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + + # If the article has been deleted while editing, don't save it without + # confirmation + if ( $this->deletedSinceEdit && !$this->recreate ) { + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } + + wfProfileOut( "$fname-checks" ); + + # If article is new, insert it. + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); + if ( 0 == $aid ) { + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCanCreate() ) { + wfDebug( "$fname: no create permission\n" ); + $this->noCreatePermission(); + wfProfileOut( $fname ); + return; + } + + # Don't save a new article if it's blank. + if ( ( '' == $this->textbox1 ) ) { + $wgOut->redirect( $this->mTitle->getFullURL() ); + wfProfileOut( $fname ); + return false; + } + + # If no edit comment was given when creating a new page, and what's being + # created is a redirect, be smart and fill in a neat auto-comment + if( $this->summary == '' ) { + $rt = Title::newFromRedirect( $this->textbox1 ); + if( is_object( $rt ) ) + $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); + } + + $isComment=($this->section=='new'); + $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, + $this->minoredit, $this->watchthis, false, $isComment); + + wfProfileOut( $fname ); + return false; + } + + # Article exists. Check for edit conflict. + + $this->mArticle->clear(); # Force reload of dates, etc. + $this->mArticle->forUpdate( true ); # Lock the article + + if( $this->mArticle->getTimestamp() != $this->edittime ) { + $this->isConflict = true; + if( $this->section == 'new' ) { + if( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( "EditPage::editForm duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( "EditPage::editForm conflict suppressed; new section\n" ); + } + } + } + $userid = $wgUser->getID(); + + if ( $this->isConflict) { + wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . + $this->mArticle->getTimestamp() . "'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); + } + else { + wfDebug( "EditPage::editForm getting section '$this->section'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); + } + if( is_null( $text ) ) { + wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $text = $this->textbox1; + } + + # Suppress edit conflict with self, except for section edits where merging is required. + if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) { + wfDebug( "Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } else { + # switch from section editing to normal editing in edit conflict + if($this->isConflict) { + # Attempt merge + if( $this->mergeChangesInto( $text ) ){ + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( "Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( "Keeping edit conflict, failed merge.\n" ); + } + } + } + + if ( $this->isConflict ) { + wfProfileOut( $fname ); + return true; + } + + # If no edit comment was given when turning a page into a redirect, be smart + # and fill in a neat auto-comment + if( $this->summary == '' ) { + $rt = Title::newFromRedirect( $this->textbox1 ); + if( is_object( $rt ) ) + $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); + } + + # Handle the user preference to force summaries here + if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + wfProfileOut( $fname ); + return( true ); + } + } + + # All's well + wfProfileIn( "$fname-sectionanchor" ); + $sectionanchor = ''; + if( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + return true; + } + if( $this->summary != '' ) { + $sectionanchor = $this->sectionAnchor( $this->summary ); + } + } elseif( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # 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] ); + } + } + wfProfileOut( "$fname-sectionanchor" ); + + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $text; + $this->section = ''; + + // Check for length errors again now that the section is merged in + $this->kblength = (int)(strlen( $text ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + $this->tooBig = true; + wfProfileOut( $fname ); + return true; + } + + # update the article here + if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, + $this->watchthis, '', $sectionanchor ) ) { + wfProfileOut( $fname ); + return false; + } else { + $this->isConflict = true; + } + wfProfileOut( $fname ); + return true; + } + + /** + * Initialise form fields in the object + * Called on the first invocation, e.g. when a user clicks an edit link + */ + function initialiseForm() { + $this->edittime = $this->mArticle->getTimestamp(); + $this->textbox1 = $this->getContent(); + $this->summary = ''; + if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) + $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ) ; + wfProxyCheck(); + } + + /** + * Send the edit form and related headers to $wgOut + * @param $formCallback Optional callable that takes an OutputPage + * parameter; will be called during form output + * near the top, for captchas and the like. + */ + function showEditForm( $formCallback=null ) { + global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize; + + $fname = 'EditPage::showEditForm'; + wfProfileIn( $fname ); + + $sk =& $wgUser->getSkin(); + + wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; + + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + # Enabled article-related sidebar, toplinks, etc. + $wgOut->setArticleRelated( true ); + + if ( $this->isConflict ) { + $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); + $wgOut->setPageTitle( $s ); + $wgOut->addWikiText( wfMsg( '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() ); + } else { + $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); + if( !$this->preview && !$this->diff ) { + preg_match( "/^(=+)(.+)\\1/mi", + $this->textbox1, + $matches ); + if( !empty( $matches[2] ) ) { + $this->summary = "/* ". trim($matches[2])." */ "; + } + } + } + } else { + $s = wfMsg( 'editing', $this->mTitle->getPrefixedText() ); + } + $wgOut->setPageTitle( $s ); + + if ( $this->missingComment ) { + $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); + } + + if( $this->missingSummary ) { + $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); + } + + if( !$this->hookError == '' ) { + $wgOut->addWikiText( $this->hookError ); + } + + if ( !$this->checkUnicodeCompliantBrowser() ) { + $wgOut->addWikiText( wfMsg( 'nonunicodebrowser') ); + } + if ( isset( $this->mArticle ) + && isset( $this->mArticle->mRevision ) + && !$this->mArticle->mRevision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); + $wgOut->addWikiText( wfMsg( 'editingold' ) ); + } + } + + if( wfReadOnly() ) { + $wgOut->addWikiText( wfMsg( 'readonlywarning' ) ); + } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) { + $wgOut->addWikiText( wfMsg( 'anoneditwarning' ) ); + } else { + if( $this->isCssJsSubpage && $this->formtype != 'preview' ) { + # Check the skin exists + if( $this->isValidCssJsSubpage ) { + $wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); + } + } + } + + if( $this->mTitle->isProtected( 'edit' ) ) { + # Is the protection due to the namespace, e.g. interface text? + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # Yes; remind the user + $notice = wfMsg( 'editinginterface' ); + } elseif( $this->mTitle->isSemiProtected() ) { + # No; semi protected + $notice = wfMsg( 'semiprotectedpagewarning' ); + if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) { + $notice = ''; + } + } else { + # No; regular protection + $notice = wfMsg( 'protectedpagewarning' ); + } + $wgOut->addWikiText( $notice ); + } + + 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 ) ); + } elseif( $this->kblength > 29 ) { + $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) ); + } + + $rows = $wgUser->getIntOption( 'rows' ); + $cols = $wgUser->getIntOption( 'cols' ); + + $ew = $wgUser->getOption( 'editwidth' ); + if ( $ew ) $ew = " style=\"width:100%\""; + else $ew = ''; + + $q = 'action=submit'; + #if ( "no" == $redirect ) { $q .= "&redirect=no"; } + $action = $this->mTitle->escapeLocalURL( $q ); + + $summary = wfMsg('summary'); + $subject = wfMsg('subject'); + $minor = wfMsgExt('minoredit', array('parseinline')); + $watchthis = wfMsgExt('watchthis', array('parseinline')); + + $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), + wfMsgExt('cancel', array('parseinline')) ); + $edithelpurl = $sk->makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); + $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. + htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. + htmlspecialchars( wfMsg( 'newwindow' ) ); + + global $wgRightsText; + $copywarn = "<div id=\"editpage-copywarn\">\n" . + wfMsg( $wgRightsText ? 'copyrightwarning' : 'copyrightwarning2', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', + $wgRightsText ) . "\n</div>"; + + if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { + # prepare toolbar for edit buttons + $toolbar = $this->getEditToolbar(); + } else { + $toolbar = ''; + } + + // activate checkboxes if user wants them to be always active + if( !$this->preview && !$this->diff ) { + # Sort out the "watch" checkbox + if( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } + + if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; + } + + $minoredithtml = ''; + + if ( $wgUser->isAllowed('minoredit') ) { + $minoredithtml = + "<input tabindex='3' type='checkbox' value='1' name='wpMinoredit'".($this->minoredit?" checked='checked'":""). + " accesskey='".wfMsg('accesskey-minoredit')."' id='wpMinoredit' />\n". + "<label for='wpMinoredit' title='".wfMsg('tooltip-minoredit')."'>{$minor}</label>\n"; + } + + $watchhtml = ''; + + if ( $wgUser->isLoggedIn() ) { + $watchhtml = "<input tabindex='4' type='checkbox' name='wpWatchthis'". + ($this->watchthis?" checked='checked'":""). + " accesskey=\"".htmlspecialchars(wfMsg('accesskey-watch'))."\" id='wpWatchthis' />\n". + "<label for='wpWatchthis' title=\"" . + htmlspecialchars(wfMsg('tooltip-watch'))."\">{$watchthis}</label>\n"; + } + + $checkboxhtml = $minoredithtml . $watchhtml; + + if ( $wgUser->getOption( 'previewontop' ) ) { + + if ( 'preview' == $this->formtype ) { + $this->showPreview(); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); + } + + if ( 'diff' == $this->formtype ) { + $wgOut->addHTML( $this->getDiff() ); + } + } + + + # if this is a comment, show a subject line at the top, which is also the edit summary. + # Otherwise, show a summary field at the bottom + $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME + if( $this->section == 'new' ) { + $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $editsummary = ''; + } else { + $commentsubject = ''; + $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + } + + # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display + if( !$this->preview && !$this->diff ) { + $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); + } + $templates = $this->formatTemplates(); + + global $wgUseMetadataEdit ; + if ( $wgUseMetadataEdit ) { + $metadata = $this->mMetaData ; + $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; + $top = wfMsgWikiHtml( 'metadata_help' ); + $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ; + } + else $metadata = "" ; + + $hidden = ''; + $recreate = ''; + if ($this->deletedSinceEdit) { + if ( 'save' != $this->formtype ) { + $wgOut->addWikiText( wfMsg('deletedwhileediting')); + } else { + // Hide the toolbar and edit area, use can click preview to get it back + // Add an confirmation checkbox and explanation. + $toolbar = ''; + $hidden = 'type="hidden" style="display:none;"'; + $recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment )); + $recreate .= + "<br /><input tabindex='1' type='checkbox' value='1' name='wpRecreate' id='wpRecreate' />". + "<label for='wpRecreate' title='".wfMsg('tooltip-recreate')."'>". wfMsg('recreate')."</label>"; + } + } + + $temp = array( + 'id' => 'wpSave', + 'name' => 'wpSave', + 'type' => 'submit', + 'tabindex' => '5', + 'value' => wfMsg('savearticle'), + 'accesskey' => wfMsg('accesskey-save'), + 'title' => wfMsg('tooltip-save'), + ); + $buttons['save'] = wfElement('input', $temp, ''); + $temp = array( + 'id' => 'wpDiff', + 'name' => 'wpDiff', + 'type' => 'submit', + 'tabindex' => '7', + 'value' => wfMsg('showdiff'), + 'accesskey' => wfMsg('accesskey-diff'), + 'title' => wfMsg('tooltip-diff'), + ); + $buttons['diff'] = wfElement('input', $temp, ''); + + global $wgLivePreview; + if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showpreview'), + 'accesskey' => '', + 'title' => wfMsg('tooltip-preview'), + 'style' => 'display: none;', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $temp = array( + 'id' => 'wpLivePreview', + 'name' => 'wpLivePreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showlivepreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => '', + 'onclick' => $this->doLivePreviewScript(), + ); + $buttons['live'] = wfElement('input', $temp, ''); + } else { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showpreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => wfMsg('tooltip-preview'), + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['live'] = ''; + } + + $safemodehtml = $this->checkUnicodeCompliantBrowser() + ? "" + : "<input type='hidden' name=\"safemode\" value='1' />\n"; + + $wgOut->addHTML( <<<END +{$toolbar} +<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> +END +); + + if( is_callable( $formCallback ) ) { + call_user_func_array( $formCallback, array( &$wgOut ) ); + } + + // Put these up at the top to ensure they aren't lost on early form submission + $wgOut->addHTML( " +<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> +<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n +<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n +<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + + $wgOut->addHTML( <<<END +$recreate +{$commentsubject} +<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' +cols='{$cols}'{$ew} $hidden> +END +. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ) . +" +</textarea> + " ); + + $wgOut->addWikiText( $copywarn ); + $wgOut->addHTML( " +{$metadata} +{$editsummary} +{$checkboxhtml} +{$safemodehtml} +"); + + $wgOut->addHTML( +"<div class='editButtons'> + {$buttons['save']} + {$buttons['preview']} + {$buttons['live']} + {$buttons['diff']} + <span class='editHelp'>{$cancel} | {$edithelp}</span> +</div><!-- editButtons --> +</div><!-- editOptions -->"); + + $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); + + $wgOut->addHTML( " +<div class='templatesUsed'> +{$templates} +</div> +" ); + + if ( $wgUser->isLoggedIn() ) { + /** + * To make it harder for someone to slip a user a page + * which submits an edit form to the wiki without their + * knowledge, a random token is associated with the login + * session. If it's not passed back with the submission, + * we won't save the page, or render user JavaScript and + * CSS previews. + */ + $token = htmlspecialchars( $wgUser->editToken() ); + $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); + } + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag here. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + if( $this->missingSummary ) { + $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" ); + } + + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in a hidden field. + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); + + if ( $this->isConflict ) { + require_once( "DifferenceEngine.php" ); + $wgOut->addWikiText( '==' . wfMsg( "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'>" + . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); + } + $wgOut->addHTML( "</form>\n" ); + if ( !$wgUser->getOption( 'previewontop' ) ) { + + if ( $this->formtype == 'preview') { + $this->showPreview(); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); + } + + if ( $this->formtype == 'diff') { + $wgOut->addHTML( $this->getDiff() ); + } + + } + + wfProfileOut( $fname ); + } + + /** + * Append preview output to $wgOut. + * Includes category rendering if this is a category page. + * @private + */ + function showPreview() { + global $wgOut; + $wgOut->addHTML( '<div id="wikiPreview">' ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->openShowCategory(); + } + $previewOutput = $this->getPreviewText(); + $wgOut->addHTML( $previewOutput ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->closeShowCategory(); + } + $wgOut->addHTML( "<br style=\"clear:both;\" />\n" ); + $wgOut->addHTML( '</div>' ); + } + + /** + * Prepare a list of templates used by this page. Returns HTML. + */ + function formatTemplates() { + global $wgUser; + + $fname = 'EditPage::formatTemplates'; + wfProfileIn( $fname ); + + $sk =& $wgUser->getSkin(); + + $outText = ''; + $templates = $this->mArticle->getUsedTemplates(); + if ( count( $templates ) > 0 ) { + # Do a batch existence check + $batch = new LinkBatch; + foreach( $templates as $title ) { + $batch->addObj( $title ); + } + $batch->execute(); + + # Construct the HTML + $outText = '<br />'. wfMsgExt( 'templatesused', array( 'parseinline' ) ) . '<ul>'; + foreach ( $templates as $titleObj ) { + $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . '</li>'; + } + $outText .= '</ul>'; + } + wfProfileOut( $fname ); + return $outText; + } + + /** + * Live Preview lets us fetch rendered preview page content and + * add it to the page without refreshing the whole page. + * If not supported by the browser it will fall through to the normal form + * submission method. + * + * This function outputs a script tag to support live preview, and + * returns an onclick handler which should be added to the attributes + * of the preview button + */ + function doLivePreviewScript() { + global $wgStylePath, $wgJsMimeType, $wgOut, $wgTitle; + $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . + htmlspecialchars( $wgStylePath . '/common/preview.js' ) . + '"></script>' . "\n" ); + $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); + return "return !livePreview(" . + "getElementById('wikiPreview')," . + "editform.wpTextbox1.value," . + '"' . $liveAction . '"' . ")"; + } + + function getLastDelete() { + $dbr =& wfGetDB( DB_SLAVE ); + $fname = 'EditPage::getLastDelete'; + $res = $dbr->select( + array( 'logging', 'user' ), + array( 'log_type', + 'log_action', + 'log_timestamp', + 'log_user', + 'log_namespace', + 'log_title', + 'log_comment', + 'log_params', + 'user_name', ), + array( 'log_namespace' => $this->mTitle->getNamespace(), + 'log_title' => $this->mTitle->getDBkey(), + 'log_type' => 'delete', + 'log_action' => 'delete', + 'user_id=log_user' ), + $fname, + array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); + + if($dbr->numRows($res) == 1) { + while ( $x = $dbr->fetchObject ( $res ) ) + $data = $x; + $dbr->freeResult ( $res ) ; + } else { + $data = null; + } + return $data; + } + + /** + * @todo document + */ + function getPreviewText() { + global $wgOut, $wgUser, $wgTitle, $wgParser; + + $fname = 'EditPage::getPreviewText'; + wfProfileIn( $fname ); + + if ( $this->mTriedSave && !$this->mTokenOk ) { + $msg = '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"; + } + + $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions->setEditSection( false ); + + global $wgRawHtml; + if( $wgRawHtml && !$this->mTokenOk ) { + // Could be an offsite preview attempt. This is very unsafe if + // HTML is enabled, as it could be an attack. + return $wgOut->parse( "<div class='previewnote'>" . + wfMsg( 'session_fail_preview_html' ) . "</div>" ); + } + + # don't parse user css/js, show message about preview + # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here + + if ( $this->isCssJsSubpage ) { + if(preg_match("/\\.css$/", $wgTitle->getText() ) ) { + $previewtext = wfMsg('usercsspreview'); + } else if(preg_match("/\\.js$/", $wgTitle->getText() ) ) { + $previewtext = wfMsg('userjspreview'); + } + $parserOptions->setTidy(true); + $parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions ); + $wgOut->addHTML( $parserOutput->mText ); + wfProfileOut( $fname ); + return $previewhead; + } else { + # if user want to see preview when he edit an article + if( $wgUser->getOption('previewonfirst') and ($this->textbox1 == '')) { + $this->textbox1 = $this->getContent(); + } + + $toparse = $this->textbox1; + + # If we're adding a comment, we need to show the + # summary as the headline + if($this->section=="new" && $this->summary!="") { + $toparse="== {$this->summary} ==\n\n".$toparse; + } + + if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ; + $parserOptions->setTidy(true); + $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n", + $wgTitle, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $wgOut->addParserOutputNoText( $parserOutput ); + + wfProfileOut( $fname ); + return $previewhead . $previewHTML; + } + } + + /** + * Call the stock "user is blocked" page + */ + function blockedPage() { + global $wgOut, $wgUser; + $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' + + # If the user made changes, preserve them when showing the markup + # (This happens when a user is blocked during edit, for instance) + $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); + $source = $first ? $this->getContent() : $this->textbox1; + + # Spit out the source or the user's modified version + $rows = $wgUser->getOption( 'rows' ); + $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->addHtml( wfElement( 'textarea', $attribs, $source ) ); + } + + /** + * Produce the stock "please login to edit pages" page + */ + function userNotLoggedInPage() { + global $wgUser, $wgOut; + $skin = $wgUser->getSkin(); + + $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->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 ); + } + + /** + * Produce the stock "your edit contains spam" page + * + * @param $match Text which triggered one or more filters + */ + function spamPage( $match = false ) { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); + if ( $match ) + $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); + + $wgOut->returnToMain( false ); + } + + /** + * @private + * @todo document + */ + function mergeChangesInto( &$editText ){ + $fname = 'EditPage::mergeChangesInto'; + wfProfileIn( $fname ); + + $db =& wfGetDB( DB_MASTER ); + + // This is the revision the editor started from + $baseRevision = Revision::loadFromTimestamp( + $db, $this->mArticle->mTitle, $this->edittime ); + if( is_null( $baseRevision ) ) { + wfProfileOut( $fname ); + return false; + } + $baseText = $baseRevision->getText(); + + // The current state, we want to merge updates into it + $currentRevision = Revision::loadFromTitle( + $db, $this->mArticle->mTitle ); + if( is_null( $currentRevision ) ) { + wfProfileOut( $fname ); + return false; + } + $currentText = $currentRevision->getText(); + + if( wfMerge( $baseText, $editText, $currentText, $result ) ){ + $editText = $result; + wfProfileOut( $fname ); + return true; + } else { + wfProfileOut( $fname ); + return false; + } + } + + /** + * Check if the browser is on a blacklist of user-agents known to + * mangle UTF-8 data on form submission. Returns true if Unicode + * should make it through, false if it's known to be a problem. + * @return bool + * @private + */ + function checkUnicodeCompliantBrowser() { + global $wgBrowserBlackList; + if( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { + // No User-Agent header sent? Trust it by default... + return true; + } + $currentbrowser = $_SERVER["HTTP_USER_AGENT"]; + foreach ( $wgBrowserBlackList as $browser ) { + if ( preg_match($browser, $currentbrowser) ) { + return false; + } + } + return true; + } + + /** + * 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 ); + } + + /** + * Shows a bulletin board style toolbar for common editing functions. + * It can be disabled in the user preferences. + * The necessary JavaScript code can be found in style/wikibits.js. + */ + function getEditToolbar() { + global $wgStylePath, $wgContLang, $wgJsMimeType; + + /** + * toolarray an array of arrays which each include the filename of + * the button image (without path), the opening tag, the closing tag, + * and optionally a sample text that is inserted between the two when no + * selection is highlighted. + * The tip text is shown when the user moves the mouse over the button. + * + * Already here are accesskeys (key), which are not used yet until someone + * can figure out a way to make them work in IE. However, we should make + * sure these keys are not defined on the edit page. + */ + $toolarray=array( + array( 'image'=>'button_bold.png', + 'open' => "\'\'\'", + 'close' => "\'\'\'", + 'sample'=> wfMsg('bold_sample'), + 'tip' => wfMsg('bold_tip'), + 'key' => 'B' + ), + array( 'image'=>'button_italic.png', + 'open' => "\'\'", + 'close' => "\'\'", + 'sample'=> wfMsg('italic_sample'), + 'tip' => wfMsg('italic_tip'), + 'key' => 'I' + ), + array( 'image'=>'button_link.png', + 'open' => '[[', + 'close' => ']]', + 'sample'=> wfMsg('link_sample'), + 'tip' => wfMsg('link_tip'), + 'key' => 'L' + ), + array( 'image'=>'button_extlink.png', + 'open' => '[', + 'close' => ']', + 'sample'=> wfMsg('extlink_sample'), + 'tip' => wfMsg('extlink_tip'), + 'key' => 'X' + ), + array( 'image'=>'button_headline.png', + 'open' => "\\n== ", + 'close' => " ==\\n", + 'sample'=> wfMsg('headline_sample'), + 'tip' => wfMsg('headline_tip'), + 'key' => 'H' + ), + array( 'image'=>'button_image.png', + 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", + 'close' => ']]', + 'sample'=> wfMsg('image_sample'), + 'tip' => wfMsg('image_tip'), + 'key' => 'D' + ), + array( 'image' =>'button_media.png', + 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'close' => ']]', + 'sample'=> wfMsg('media_sample'), + 'tip' => wfMsg('media_tip'), + 'key' => 'M' + ), + array( 'image' =>'button_math.png', + 'open' => "<math>", + 'close' => "<\\/math>", + 'sample'=> wfMsg('math_sample'), + 'tip' => wfMsg('math_tip'), + 'key' => 'C' + ), + array( 'image' =>'button_nowiki.png', + 'open' => "<nowiki>", + 'close' => "<\\/nowiki>", + 'sample'=> wfMsg('nowiki_sample'), + 'tip' => wfMsg('nowiki_tip'), + 'key' => 'N' + ), + array( 'image' =>'button_sig.png', + 'open' => '--~~~~', + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('sig_tip'), + 'key' => 'Y' + ), + array( 'image' =>'button_hr.png', + 'open' => "\\n----\\n", + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('hr_tip'), + 'key' => 'R' + ) + ); + $toolbar = "<div id='toolbar'>\n"; + $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; + + foreach($toolarray as $tool) { + + $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');\n"; + } + + $toolbar.="/*]]>*/\n</script>"; + $toolbar.="\n</div>"; + return $toolbar; + } + + /** + * Output preview text only. This can be sucked into the edit page + * via JavaScript, and saves the server time rendering the skin as + * well as theoretically being more robust on the client (doesn't + * disturb the edit box's undo history, won't eat your text on + * failure, etc). + * + * @todo This doesn't include category or interlanguage links. + * Would need to enhance it a bit, maybe wrap them in XML + * or something... that might also require more skin + * initialization, so check whether that's a problem. + */ + function livePreview() { + global $wgOut; + $wgOut->disable(); + header( 'Content-type: text/xml' ); + header( 'Cache-control: no-cache' ); + # FIXME + echo $this->getPreviewText( ); + /* To not shake screen up and down between preview and live-preview */ + echo "<br style=\"clear:both;\" />\n"; + } + + + /** + * Get a diff between the current contents of the edit box and the + * version of the page we're editing from. + * + * If this is a section edit, we'll replace the section as for final + * save and then make a comparison. + * + * @return string HTML + */ + function getDiff() { + require_once( 'DifferenceEngine.php' ); + $oldtext = $this->mArticle->fetchContent(); + $newtext = $this->mArticle->replaceSection( + $this->section, $this->textbox1, $this->summary, $this->edittime ); + $newtext = $this->mArticle->preSaveTransform( $newtext ); + $oldtitle = wfMsgExt( 'currentrev', array('parseinline') ); + $newtitle = wfMsgExt( 'yourtext', array('parseinline') ); + if ( $oldtext !== false || $newtext != '' ) { + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $oldtext, $newtext ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); + } else { + $difftext = ''; + } + + return '<div id="wikiDiff">' . $difftext . '</div>'; + } + + /** + * Filter an input field through a Unicode de-armoring process if it + * came from an old browser with known broken Unicode editing issues. + * + * @param WebRequest $request + * @param string $field + * @return string + * @private + */ + function safeUnicodeInput( $request, $field ) { + $text = rtrim( $request->getText( $field ) ); + return $request->getBool( 'safemode' ) + ? $this->unmakesafe( $text ) + : $text; + } + + /** + * Filter an output field through a Unicode armoring process if it is + * going to an old browser with known broken Unicode editing issues. + * + * @param string $text + * @return string + * @private + */ + function safeUnicodeOutput( $text ) { + global $wgContLang; + $codedText = $wgContLang->recodeForEdit( $text ); + return $this->checkUnicodeCompliantBrowser() + ? $codedText + : $this->makesafe( $codedText ); + } + + /** + * A number of web browsers are known to corrupt non-ASCII characters + * in a UTF-8 text editing environment. To protect against this, + * detected browsers will be served an armored version of the text, + * with non-ASCII chars converted to numeric HTML character references. + * + * Preexisting such character references will have a 0 added to them + * to ensure that round-trips do not alter the original data. + * + * @param string $invalue + * @return string + * @private + */ + function makesafe( $invalue ) { + // Armor existing references for reversability. + $invalue = strtr( $invalue, array( "&#x" => "�" ) ); + + $bytesleft = 0; + $result = ""; + $working = 0; + for( $i = 0; $i < strlen( $invalue ); $i++ ) { + $bytevalue = ord( $invalue{$i} ); + if( $bytevalue <= 0x7F ) { //0xxx xxxx + $result .= chr( $bytevalue ); + $bytesleft = 0; + } elseif( $bytevalue <= 0xBF ) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if( $bytesleft <= 0 ) { + $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; + } + } elseif( $bytevalue <= 0xDF ) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif( $bytevalue <= 0xEF ) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** + * Reverse the previously applied transliteration of non-ASCII characters + * back to UTF-8. Used to protect data from corruption by broken web browsers + * as listed in $wgBrowserBlackList. + * + * @param string $invalue + * @return string + * @private + */ + function unmakesafe( $invalue ) { + $result = ""; + for( $i = 0; $i < strlen( $invalue ); $i++ ) { + if( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) { + $i += 3; + $hexstring = ""; + do { + $hexstring .= $invalue{$i}; + $i++; + } while( ctype_xdigit( $invalue{$i} ) && ( $i < strlen( $invalue ) ) ); + + // Do some sanity checks. These aren't needed for reversability, + // but should help keep the breakage down if the editor + // breaks one of the entities whilst editing. + if ((substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6)) { + $codepoint = hexdec($hexstring); + $result .= codepointToUtf8( $codepoint ); + } else { + $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); + } + } else { + $result .= substr( $invalue, $i, 1 ); + } + } + // reverse the transform that we made for reversability reasons. + return strtr( $result, array( "�" => "&#x" ) ); + } + + function noCreatePermission() { + global $wgOut; + $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); + $wgOut->addWikiText( wfMsg( 'nocreatetext' ) ); + } + +} + +?> diff --git a/includes/Exception.php b/includes/Exception.php new file mode 100644 index 00000000..1e24515b --- /dev/null +++ b/includes/Exception.php @@ -0,0 +1,193 @@ +<?php + +class MWException extends Exception +{ + function useOutputPage() { + return !empty( $GLOBALS['wgFullyInitialised'] ); + } + + function useMessageCache() { + global $wgLang; + return is_object( $wgLang ); + } + + function msg( $key, $fallback /*[, params...] */ ) { + $args = array_slice( func_get_args(), 2 ); + if ( $this->useMessageCache() ) { + return wfMsgReal( $key, $args ); + } else { + return wfMsgReplaceArgs( $fallback, $args ); + } + } + + function getHTML() { + return '<p>' . htmlspecialchars( $this->getMessage() ) . + '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . + "</p>\n"; + } + + function getText() { + return $this->getMessage() . + "\nBacktrace:\n" . $this->getTraceAsString() . "\n"; + } + + function getPageTitle() { + if ( $this->useMessageCache() ) { + return wfMsg( 'internalerror' ); + } else { + global $wgSitename; + return "$wgSitename error"; + } + } + + function reportHTML() { + global $wgOut; + if ( $this->useOutputPage() ) { + $wgOut->setPageTitle( $this->getPageTitle() ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->redirect( '' ); + $wgOut->clearHTML(); + $wgOut->addHTML( $this->getHTML() ); + $wgOut->output(); + } else { + echo $this->htmlHeader(); + echo $this->getHTML(); + echo $this->htmlFooter(); + } + } + + function reportText() { + echo $this->getText(); + } + + function report() { + global $wgCommandLineMode; + if ( $wgCommandLineMode ) { + $this->reportText(); + } else { + $this->reportHTML(); + } + } + + function htmlHeader() { + global $wgLogo, $wgSitename, $wgOutputEncoding; + + if ( !headers_sent() ) { + header( 'HTTP/1.0 500 Internal Server Error' ); + header( 'Content-type: text/html; charset='.$wgOutputEncoding ); + /* Don't cache error pages! They cause no end of trouble... */ + header( 'Cache-control: none' ); + header( 'Pragma: nocache' ); + } + $title = $this->getPageTitle(); + echo "<html> + <head> + <title>$title + + +

    $title

    + "; + } + + function htmlFooter() { + echo ""; + } +} + +/** + * Exception class which takes an HTML error message, and does not + * produce a backtrace. Replacement for OutputPage::fatalError(). + */ +class FatalError extends MWException { + function getHTML() { + return $this->getMessage(); + } + + function getText() { + return $this->getMessage(); + } +} + +class ErrorPageError extends MWException { + public $title, $msg; + + /** + * Note: these arguments are keys into wfMsg(), not text! + */ + function __construct( $title, $msg ) { + $this->title = $title; + $this->msg = $msg; + parent::__construct( wfMsg( $msg ) ); + } + + function report() { + global $wgOut; + $wgOut->showErrorPage( $this->title, $this->msg ); + $wgOut->output(); + } +} + +/** + * Install an exception handler for MediaWiki exception types. + */ +function wfInstallExceptionHandler() { + set_exception_handler( 'wfExceptionHandler' ); +} + +/** + * Report an exception to the user + */ +function wfReportException( Exception $e ) { + if ( is_a( $e, 'MWException' ) ) { + try { + $e->report(); + } catch ( Exception $e2 ) { + // Exception occurred from within exception handler + // Show a simpler error message for the original exception, + // don't try to invoke report() + $message = "MediaWiki internal error.\n\n" . + "Original exception: " . $e->__toString() . + "\n\nException caught inside exception handler: " . + $e2->__toString() . "\n"; + + if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { + echo $message; + } else { + echo nl2br( htmlspecialchars( $message ) ). "\n"; + } + } + } else { + echo $e->__toString(); + } +} + +/** + * Exception handler which simulates the appropriate catch() handling: + * + * try { + * ... + * } catch ( MWException $e ) { + * $e->report(); + * } catch ( Exception $e ) { + * echo $e->__toString(); + * } + */ +function wfExceptionHandler( $e ) { + global $wgFullyInitialised; + wfReportException( $e ); + + // Final cleanup, similar to wfErrorExit() + if ( $wgFullyInitialised ) { + try { + wfProfileClose(); + logProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition + } catch ( Exception $e ) {} + } + + // Exit value should be nonzero for the benefit of shell jobs + exit( 1 ); +} + +?> diff --git a/includes/Exif.php b/includes/Exif.php new file mode 100644 index 00000000..f9fb9a2c --- /dev/null +++ b/includes/Exif.php @@ -0,0 +1,1124 @@ + + * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification + * @bug 1555, 1947 + */ + +/** + * @package MediaWiki + * @subpackage Metadata + */ +class Exif { + //@{ + /* @var array + * @private + */ + + /**#@+ + * Exif tag type definition + */ + const BYTE = 1; # An 8-bit (1-byte) unsigned integer. + const ASCII = 2; # An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. + const SHORT = 3; # A 16-bit (2-byte) unsigned integer. + const LONG = 4; # A 32-bit (4-byte) unsigned integer. + const RATIONAL = 5; # Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator + const UNDEFINED = 7; # An 8-bit byte that can take any value depending on the field definition + const SLONG = 9; # A 32-bit (4-byte) signed integer (2's complement notation), + const SRATIONAL = 10; # Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + + /** + * Exif tags grouped by category, the tagname itself is the key and the type + * is the value, in the case of more than one possible value type they are + * seperated by commas. + */ + var $mExifTags; + + /** + * A one dimentional array of all Exif tags + */ + var $mFlatExifTags; + + /** + * The raw Exif data returned by exif_read_data() + */ + var $mRawExifData; + + /** + * A Filtered version of $mRawExifData that has been pruned of invalid + * tags and tags that contain content they shouldn't contain according + * to the Exif specification + */ + var $mFilteredExifData; + + /** + * Filtered and formatted Exif data, see FormatExif::getFormattedData() + */ + var $mFormattedExifData; + + //@} + + //@{ + /* @var string + * @private + */ + + /** + * The file being processed + */ + var $file; + + /** + * The basename of the file being processed + */ + var $basename; + + /** + * The private log to log to + */ + var $log = 'exif'; + + //@} + + /** + * Constructor + * + * @param $file String: filename. + */ + function Exif( $file ) { + /** + * Page numbers here refer to pages in the EXIF 2.2 standard + * + * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification + */ + $this->mExifTags = array( + # TIFF Rev. 6.0 Attribute Information (p22) + 'tiff' => array( + # Tags relating to image structure + 'structure' => array( + 'ImageWidth' => Exif::SHORT.','.Exif::LONG, # Image width + 'ImageLength' => Exif::SHORT.','.Exif::LONG, # Image height + 'BitsPerSample' => Exif::SHORT, # Number of bits per component + # "When a primary image is JPEG compressed, this designation is not" + # "necessary and is omitted." (p23) + 'Compression' => Exif::SHORT, # Compression scheme #p23 + 'PhotometricInterpretation' => Exif::SHORT, # Pixel composition #p23 + 'Orientation' => Exif::SHORT, # Orientation of image #p24 + 'SamplesPerPixel' => Exif::SHORT, # Number of components + 'PlanarConfiguration' => Exif::SHORT, # Image data arrangement #p24 + 'YCbCrSubSampling' => Exif::SHORT, # Subsampling ratio of Y to C #p24 + 'YCbCrPositioning' => Exif::SHORT, # Y and C positioning #p24-25 + 'XResolution' => Exif::RATIONAL, # Image resolution in width direction + 'YResolution' => Exif::RATIONAL, # Image resolution in height direction + 'ResolutionUnit' => Exif::SHORT, # Unit of X and Y resolution #(p26) + ), + + # Tags relating to recording offset + 'offset' => array( + 'StripOffsets' => Exif::SHORT.','.Exif::LONG, # Image data location + 'RowsPerStrip' => Exif::SHORT.','.Exif::LONG, # Number of rows per strip + 'StripByteCounts' => Exif::SHORT.','.Exif::LONG, # Bytes per compressed strip + 'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG, # Offset to JPEG SOI + 'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG, # Bytes of JPEG data + ), + + # Tags relating to image data characteristics + 'characteristics' => array( + 'TransferFunction' => Exif::SHORT, # Transfer function + 'WhitePoint' => Exif::RATIONAL, # White point chromaticity + 'PrimaryChromaticities' => Exif::RATIONAL, # Chromaticities of primarities + 'YCbCrCoefficients' => Exif::RATIONAL, # Color space transformation matrix coefficients #p27 + 'ReferenceBlackWhite' => Exif::RATIONAL # Pair of black and white reference values + ), + + # Other tags + 'other' => array( + 'DateTime' => Exif::ASCII, # File change date and time + 'ImageDescription' => Exif::ASCII, # Image title + 'Make' => Exif::ASCII, # Image input equipment manufacturer + 'Model' => Exif::ASCII, # Image input equipment model + 'Software' => Exif::ASCII, # Software used + 'Artist' => Exif::ASCII, # Person who created the image + 'Copyright' => Exif::ASCII, # Copyright holder + ), + ), + + # Exif IFD Attribute Information (p30-31) + 'exif' => array( + # Tags relating to version + 'version' => array( + # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance + # to the EXIF 2.1 AND 2.2 standards + 'ExifVersion' => Exif::UNDEFINED, # Exif version + 'FlashpixVersion' => Exif::UNDEFINED, # Supported Flashpix version #p32 + ), + + # Tags relating to Image Data Characteristics + 'characteristics' => array( + 'ColorSpace' => Exif::SHORT, # Color space information #p32 + ), + + # Tags relating to image configuration + 'configuration' => array( + 'ComponentsConfiguration' => Exif::UNDEFINED, # Meaning of each component #p33 + 'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode + 'PixelYDimension' => Exif::SHORT.','.Exif::LONG, # Valid image width + 'PixelXDimension' => Exif::SHORT.','.Exif::LONG, # Valind image height + ), + + # Tags relating to related user information + 'user' => array( + 'MakerNote' => Exif::UNDEFINED, # Manufacturer notes + 'UserComment' => Exif::UNDEFINED, # User comments #p34 + ), + + # Tags relating to related file information + 'related' => array( + 'RelatedSoundFile' => Exif::ASCII, # Related audio file + ), + + # Tags relating to date and time + 'dateandtime' => array( + 'DateTimeOriginal' => Exif::ASCII, # Date and time of original data generation #p36 + 'DateTimeDigitized' => Exif::ASCII, # Date and time of original data generation + 'SubSecTime' => Exif::ASCII, # DateTime subseconds + 'SubSecTimeOriginal' => Exif::ASCII, # DateTimeOriginal subseconds + 'SubSecTimeDigitized' => Exif::ASCII, # DateTimeDigitized subseconds + ), + + # Tags relating to picture-taking conditions (p31) + 'conditions' => array( + 'ExposureTime' => Exif::RATIONAL, # Exposure time + 'FNumber' => Exif::RATIONAL, # F Number + 'ExposureProgram' => Exif::SHORT, # Exposure Program #p38 + 'SpectralSensitivity' => Exif::ASCII, # Spectral sensitivity + 'ISOSpeedRatings' => Exif::SHORT, # ISO speed rating + 'OECF' => Exif::UNDEFINED, # Optoelectronic conversion factor + 'ShutterSpeedValue' => Exif::SRATIONAL, # Shutter speed + 'ApertureValue' => Exif::RATIONAL, # Aperture + 'BrightnessValue' => Exif::SRATIONAL, # Brightness + 'ExposureBiasValue' => Exif::SRATIONAL, # Exposure bias + 'MaxApertureValue' => Exif::RATIONAL, # Maximum land aperture + 'SubjectDistance' => Exif::RATIONAL, # Subject distance + 'MeteringMode' => Exif::SHORT, # Metering mode #p40 + 'LightSource' => Exif::SHORT, # Light source #p40-41 + 'Flash' => Exif::SHORT, # Flash #p41-42 + 'FocalLength' => Exif::RATIONAL, # Lens focal length + 'SubjectArea' => Exif::SHORT, # Subject area + 'FlashEnergy' => Exif::RATIONAL, # Flash energy + 'SpatialFrequencyResponse' => Exif::UNDEFINED, # Spatial frequency response + 'FocalPlaneXResolution' => Exif::RATIONAL, # Focal plane X resolution + 'FocalPlaneYResolution' => Exif::RATIONAL, # Focal plane Y resolution + 'FocalPlaneResolutionUnit' => Exif::SHORT, # Focal plane resolution unit #p46 + 'SubjectLocation' => Exif::SHORT, # Subject location + 'ExposureIndex' => Exif::RATIONAL, # Exposure index + 'SensingMethod' => Exif::SHORT, # Sensing method #p46 + 'FileSource' => Exif::UNDEFINED, # File source #p47 + 'SceneType' => Exif::UNDEFINED, # Scene type #p47 + 'CFAPattern' => Exif::UNDEFINED, # CFA pattern + 'CustomRendered' => Exif::SHORT, # Custom image processing #p48 + 'ExposureMode' => Exif::SHORT, # Exposure mode #p48 + 'WhiteBalance' => Exif::SHORT, # White Balance #p49 + 'DigitalZoomRatio' => Exif::RATIONAL, # Digital zoom ration + 'FocalLengthIn35mmFilm' => Exif::SHORT, # Focal length in 35 mm film + 'SceneCaptureType' => Exif::SHORT, # Scene capture type #p49 + 'GainControl' => Exif::RATIONAL, # Scene control #p49-50 + 'Contrast' => Exif::SHORT, # Contrast #p50 + 'Saturation' => Exif::SHORT, # Saturation #p50 + 'Sharpness' => Exif::SHORT, # Sharpness #p50 + 'DeviceSettingDescription' => Exif::UNDEFINED, # Desice settings description + 'SubjectDistanceRange' => Exif::SHORT, # Subject distance range #p51 + ), + + 'other' => array( + 'ImageUniqueID' => Exif::ASCII, # Unique image ID + ), + ), + + # GPS Attribute Information (p52) + 'gps' => array( + 'GPSVersionID' => Exif::BYTE, # GPS tag version + 'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53 + 'GPSLatitude' => Exif::RATIONAL, # Latitude + 'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53 + 'GPSLongitude' => Exif::RATIONAL, # Longitude + 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference + 'GPSAltitude' => Exif::RATIONAL, # Altitude + 'GPSTimeStamp' => Exif::RATIONAL, # GPS time (atomic clock) + 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement + 'GPSStatus' => Exif::ASCII, # Receiver status #p54 + 'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55 + 'GPSDOP' => Exif::RATIONAL, # Measurement precision + 'GPSSpeedRef' => Exif::ASCII, # Speed unit #p55 + 'GPSSpeed' => Exif::RATIONAL, # Speed of GPS receiver + 'GPSTrackRef' => Exif::ASCII, # Reference for direction of movement #p55 + 'GPSTrack' => Exif::RATIONAL, # Direction of movement + 'GPSImgDirectionRef' => Exif::ASCII, # Reference for direction of image #p56 + 'GPSImgDirection' => Exif::RATIONAL, # Direction of image + 'GPSMapDatum' => Exif::ASCII, # Geodetic survey data used + 'GPSDestLatitudeRef' => Exif::ASCII, # Reference for latitude of destination #p56 + 'GPSDestLatitude' => Exif::RATIONAL, # Latitude destination + 'GPSDestLongitudeRef' => Exif::ASCII, # Reference for longitude of destination #p57 + 'GPSDestLongitude' => Exif::RATIONAL, # Longitude of destination + 'GPSDestBearingRef' => Exif::ASCII, # Reference for bearing of destination #p57 + 'GPSDestBearing' => Exif::RATIONAL, # Bearing of destination + 'GPSDestDistanceRef' => Exif::ASCII, # Reference for distance to destination #p57-58 + 'GPSDestDistance' => Exif::RATIONAL, # Distance to destination + 'GPSProcessingMethod' => Exif::UNDEFINED, # Name of GPS processing method + 'GPSAreaInformation' => Exif::UNDEFINED, # Name of GPS area + 'GPSDateStamp' => Exif::ASCII, # GPS date + 'GPSDifferential' => Exif::SHORT, # GPS differential correction + ), + ); + + $this->file = $file; + $this->basename = basename( $this->file ); + + $this->makeFlatExifTags(); + + $this->debugFile( $this->basename, __FUNCTION__, true ); + wfSuppressWarnings(); + $data = exif_read_data( $this->file ); + wfRestoreWarnings(); + /** + * exif_read_data() will return false on invalid input, such as + * when somebody uploads a file called something.jpeg + * containing random gibberish. + */ + $this->mRawExifData = $data ? $data : array(); + + $this->makeFilteredData(); + $this->makeFormattedData(); + + $this->debugFile( __FUNCTION__, false ); + } + + /**#@+ + * @private + */ + /** + * Generate a flat list of the exif tags + */ + function makeFlatExifTags() { + $this->extractTags( $this->mExifTags ); + } + + /** + * A recursing extractor function used by makeFlatExifTags() + * + * Note: This used to use an array_walk function, but it made PHP5 + * segfault, see `cvs diff -u -r 1.4 -r 1.5 Exif.php` + */ + function extractTags( &$tagset ) { + foreach( $tagset as $key => $val ) { + if( is_array( $val ) ) { + $this->extractTags( $val ); + } else { + $this->mFlatExifTags[$key] = $val; + } + } + } + + /** + * Make $this->mFilteredExifData + */ + function makeFilteredData() { + $this->mFilteredExifData = $this->mRawExifData; + + foreach( $this->mFilteredExifData as $k => $v ) { + if ( !in_array( $k, array_keys( $this->mFlatExifTags ) ) ) { + $this->debug( $v, __FUNCTION__, "'$k' is not a valid Exif tag" ); + unset( $this->mFilteredExifData[$k] ); + } + } + + foreach( $this->mFilteredExifData as $k => $v ) { + if ( !$this->validate($k, $v) ) { + $this->debug( $v, __FUNCTION__, "'$k' contained invalid data" ); + unset( $this->mFilteredExifData[$k] ); + } + } + } + + /** + * @todo document + */ + function makeFormattedData( ) { + $format = new FormatExif( $this->getFilteredData() ); + $this->mFormattedExifData = $format->getFormattedData(); + } + /**#@-*/ + + /**#@+ + * @return array + */ + /** + * Get $this->mRawExifData + */ + function getData() { + return $this->mRawExifData; + } + + /** + * Get $this->mFilteredExifData + */ + function getFilteredData() { + return $this->mFilteredExifData; + } + + /** + * Get $this->mFormattedExifData + */ + function getFormattedData() { + return $this->mFormattedExifData; + } + /**#@-*/ + + /** + * The version of the output format + * + * Before the actual metadata information is saved in the database we + * strip some of it since we don't want to save things like thumbnails + * which usually accompany Exif data. This value gets saved in the + * database along with the actual Exif data, and if the version in the + * database doesn't equal the value returned by this function the Exif + * data is regenerated. + * + * @return int + */ + function version() { + return 1; // We don't need no bloddy constants! + } + + /**#@+ + * Validates if a tag value is of the type it should be according to the Exif spec + * + * @private + * + * @param $in Mixed: the input value to check + * @return bool + */ + function isByte( $in ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) { + $this->debug( $in, __FUNCTION__, true ); + return true; + } else { + $this->debug( $in, __FUNCTION__, false ); + return false; + } + } + + function isASCII( $in ) { + if ( is_array( $in ) ) { + return false; + } + + if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) { + $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' ); + return false; + } + + if ( preg_match( "/^\s*$/", $in ) ) { + $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' ); + return false; + } + + return true; + } + + function isShort( $in ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) { + $this->debug( $in, __FUNCTION__, true ); + return true; + } else { + $this->debug( $in, __FUNCTION__, false ); + return false; + } + } + + function isLong( $in ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) { + $this->debug( $in, __FUNCTION__, true ); + return true; + } else { + $this->debug( $in, __FUNCTION__, false ); + return false; + } + } + + function isRational( $in ) { + if ( !is_array( $in ) && @preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + return $this->isLong( $m[1] ) && $this->isLong( $m[2] ); + } else { + $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); + return false; + } + } + + function isUndefined( $in ) { + if ( !is_array( $in ) && preg_match( "/^\d{4}$/", $in ) ) { // Allow ExifVersion and FlashpixVersion + $this->debug( $in, __FUNCTION__, true ); + return true; + } else { + $this->debug( $in, __FUNCTION__, false ); + return false; + } + } + + function isSlong( $in ) { + if ( $this->isLong( abs( $in ) ) ) { + $this->debug( $in, __FUNCTION__, true ); + return true; + } else { + $this->debug( $in, __FUNCTION__, false ); + return false; + } + } + + function isSrational( $in ) { + if ( !is_array( $in ) && preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] ); + } else { + $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); + return false; + } + } + /**#@-*/ + + /** + * Validates if a tag has a legal value according to the Exif spec + * + * @private + * + * @param $tag String: the tag to check. + * @param $val Mixed: the value of the tag. + * @return bool + */ + function validate( $tag, $val ) { + $debug = "tag is '$tag'"; + // Does not work if not typecast + switch( (string)$this->mFlatExifTags[$tag] ) { + case (string)Exif::BYTE: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isByte( $val ); + case (string)Exif::ASCII: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isASCII( $val ); + case (string)Exif::SHORT: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isShort( $val ); + case (string)Exif::LONG: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isLong( $val ); + case (string)Exif::RATIONAL: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isRational( $val ); + case (string)Exif::UNDEFINED: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isUndefined( $val ); + case (string)Exif::SLONG: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isSlong( $val ); + case (string)Exif::SRATIONAL: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isSrational( $val ); + case (string)Exif::SHORT.','.Exif::LONG: + $this->debug( $val, __FUNCTION__, $debug ); + return $this->isShort( $val ) || $this->isLong( $val ); + default: + $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" ); + return false; + } + } + + /** + * Convenience function for debugging output + * + * @private + * + * @param $in Mixed: + * @param $fname String: + * @param $action Mixed: , default NULL. + */ + function debug( $in, $fname, $action = NULL ) { + $type = gettype( $in ); + $class = ucfirst( __CLASS__ ); + if ( $type === 'array' ) + $in = print_r( $in, true ); + + if ( $action === true ) + wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n"); + elseif ( $action === false ) + wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n"); + elseif ( $action === null ) + wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n"); + else + wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n"); + } + + /** + * Convenience function for debugging output + * + * @private + * + * @param $fname String: the name of the function calling this function + * @param $io Boolean: Specify whether we're beginning or ending + */ + function debugFile( $fname, $io ) { + $class = ucfirst( __CLASS__ ); + if ( $io ) { + wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); + } else { + wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" ); + } + } + +} + +/** + * @package MediaWiki + * @subpackage Metadata + */ +class FormatExif { + /** + * The Exif data to format + * + * @var array + * @private + */ + var $mExif; + + /** + * Constructor + * + * @param $exif Array: the Exif data to format ( as returned by + * Exif::getFilteredData() ) + */ + function FormatExif( $exif ) { + $this->mExif = $exif; + } + + /** + * Numbers given by Exif user agents are often magical, that is they + * should be replaced by a detailed explanation depending on their + * value which most of the time are plain integers. This function + * formats Exif values into human readable form. + * + * @return array + */ + function getFormattedData() { + global $wgLang; + + $tags =& $this->mExif; + + $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; + unset( $tags['ResolutionUnit'] ); + + foreach( $tags as $tag => $val ) { + switch( $tag ) { + case 'Compression': + switch( $val ) { + case 1: case 6: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'PhotometricInterpretation': + switch( $val ) { + case 2: case 6: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'Orientation': + switch( $val ) { + case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'PlanarConfiguration': + switch( $val ) { + case 1: case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + // TODO: YCbCrSubSampling + // TODO: YCbCrPositioning + + case 'XResolution': + case 'YResolution': + switch( $resolutionunit ) { + case 2: + $tags[$tag] = $this->msg( 'XYResolution', 'i', $this->formatNum( $val ) ); + break; + case 3: + $this->msg( 'XYResolution', 'c', $this->formatNum( $val ) ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + // TODO: YCbCrCoefficients #p27 (see annex E) + case 'ExifVersion': case 'FlashpixVersion': + $tags[$tag] = "$val"/100; + break; + + case 'ColorSpace': + switch( $val ) { + case 1: case 'FFFF.H': + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'ComponentsConfiguration': + switch( $val ) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'DateTime': + case 'DateTimeOriginal': + case 'DateTimeDigitized': + if( preg_match( "/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/", $val ) ) { + $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); + } + break; + + case 'ExposureProgram': + switch( $val ) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'SubjectDistance': + $tags[$tag] = $this->msg( $tag, '', $this->formatNum( $val ) ); + break; + + case 'MeteringMode': + switch( $val ) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'LightSource': + switch( $val ) { + case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11: + case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20: + case 21: case 22: case 23: case 24: case 255: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + // TODO: Flash + case 'FocalPlaneResolutionUnit': + switch( $val ) { + case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'SensingMethod': + switch( $val ) { + case 1: case 2: case 3: case 4: case 5: case 7: case 8: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'FileSource': + switch( $val ) { + case 3: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'SceneType': + switch( $val ) { + case 1: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'CustomRendered': + switch( $val ) { + case 0: case 1: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'ExposureMode': + switch( $val ) { + case 0: case 1: case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'WhiteBalance': + switch( $val ) { + case 0: case 1: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'SceneCaptureType': + switch( $val ) { + case 0: case 1: case 2: case 3: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GainControl': + switch( $val ) { + case 0: case 1: case 2: case 3: case 4: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'Contrast': + switch( $val ) { + case 0: case 1: case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'Saturation': + switch( $val ) { + case 0: case 1: case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'Sharpness': + switch( $val ) { + case 0: case 1: case 2: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'SubjectDistanceRange': + switch( $val ) { + case 0: case 1: case 2: case 3: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSLatitudeRef': + case 'GPSDestLatitudeRef': + switch( $val ) { + case 'N': case 'S': + $tags[$tag] = $this->msg( 'GPSLatitude', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSLongitudeRef': + case 'GPSDestLongitudeRef': + switch( $val ) { + case 'E': case 'W': + $tags[$tag] = $this->msg( 'GPSLongitude', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSStatus': + switch( $val ) { + case 'A': case 'V': + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSMeasureMode': + switch( $val ) { + case 2: case 3: + $tags[$tag] = $this->msg( $tag, $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSSpeedRef': + case 'GPSDestDistanceRef': + switch( $val ) { + case 'K': case 'M': case 'N': + $tags[$tag] = $this->msg( 'GPSSpeed', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSTrackRef': + case 'GPSImgDirectionRef': + case 'GPSDestBearingRef': + switch( $val ) { + case 'T': case 'M': + $tags[$tag] = $this->msg( 'GPSDirection', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSDateStamp': + $tags[$tag] = $wgLang->date( substr( $val, 0, 4 ) . substr( $val, 5, 2 ) . substr( $val, 8, 2 ) . '000000' ); + break; + + // This is not in the Exif standard, just a special + // case for our purposes which enables wikis to wikify + // the make, model and software name to link to their articles. + case 'Make': + case 'Model': + case 'Software': + $tags[$tag] = $this->msg( $tag, '', $val ); + break; + + case 'ExposureTime': + // Show the pretty fraction as well as decimal version + $tags[$tag] = wfMsg( 'exif-exposuretime-format', + $this->formatFraction( $val ), $this->formatNum( $val ) ); + break; + + case 'FNumber': + $tags[$tag] = wfMsg( 'exif-fnumber-format', + $this->formatNum( $val ) ); + break; + + case 'FocalLength': + $tags[$tag] = wfMsg( 'exif-focallength-format', + $this->formatNum( $val ) ); + break; + + default: + $tags[$tag] = $this->formatNum( $val ); + break; + } + } + + return $tags; + } + + /** + * Convenience function for getFormattedData() + * + * @private + * + * @param $tag String: the tag name to pass on + * @param $val String: the value of the tag + * @param $arg String: an argument to pass ($1) + * @return string A wfMsg of "exif-$tag-$val" in lower case + */ + function msg( $tag, $val, $arg = null ) { + global $wgContLang; + + if ($val === '') + $val = 'value'; + return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg ); + } + + /** + * Format a number, convert numbers from fractions into floating point + * numbers + * + * @private + * + * @param $num Mixed: the value to format + * @return mixed A floating point number or whatever we were fed + */ + function formatNum( $num ) { + if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) + return $m[2] != 0 ? $m[1] / $m[2] : $num; + else + return $num; + } + + /** + * Format a rational number, reducing fractions + * + * @private + * + * @param $num Mixed: the value to format + * @return mixed A floating point number or whatever we were fed + */ + function formatFraction( $num ) { + if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) { + $numerator = intval( $m[1] ); + $denominator = intval( $m[2] ); + $gcd = $this->gcd( $numerator, $denominator ); + if( $gcd != 0 ) { + // 0 shouldn't happen! ;) + return $numerator / $gcd . '/' . $denominator / $gcd; + } + } + return $this->formatNum( $num ); + } + + /** + * Calculate the greatest common divisor of two integers. + * + * @param $a Integer: FIXME + * @param $b Integer: FIXME + * @return int + * @private + */ + function gcd( $a, $b ) { + /* + // http://en.wikipedia.org/wiki/Euclidean_algorithm + // Recursive form would be: + if( $b == 0 ) + return $a; + else + return gcd( $b, $a % $b ); + */ + while( $b != 0 ) { + $remainder = $a % $b; + + // tail recursion... + $a = $b; + $b = $remainder; + } + return $a; + } +} + +/** + * MW 1.6 compatibility + */ +define( 'MW_EXIF_BYTE', Exif::BYTE ); +define( 'MW_EXIF_ASCII', Exif::ASCII ); +define( 'MW_EXIF_SHORT', Exif::SHORT ); +define( 'MW_EXIF_LONG', Exif::LONG ); +define( 'MW_EXIF_RATIONAL', Exif::RATIONAL ); +define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED ); +define( 'MW_EXIF_SLONG', Exif::SLONG ); +define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL ); + +?> diff --git a/includes/Export.php b/includes/Export.php new file mode 100644 index 00000000..da92694e --- /dev/null +++ b/includes/Export.php @@ -0,0 +1,736 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html +/** + * + * @package MediaWiki + * @subpackage SpecialPage + */ + +/** */ + +define( 'MW_EXPORT_FULL', 0 ); +define( 'MW_EXPORT_CURRENT', 1 ); + +define( 'MW_EXPORT_BUFFER', 0 ); +define( 'MW_EXPORT_STREAM', 1 ); + +define( 'MW_EXPORT_TEXT', 0 ); +define( 'MW_EXPORT_STUB', 1 ); + + +/** + * @package MediaWiki + * @subpackage SpecialPage + */ +class WikiExporter { + + var $list_authors = false ; # Return distinct author list (when not returning full history) + var $author_list = "" ; + + /** + * If using MW_EXPORT_STREAM to stream a large amount of data, + * provide a database connection which is not managed by + * LoadBalancer to read from: some history blob types will + * make additional queries to pull source data while the + * main query is still running. + * + * @param Database $db + * @param int $history one of MW_EXPORT_FULL or MW_EXPORT_CURRENT + * @param int $buffer one of MW_EXPORT_BUFFER or MW_EXPORT_STREAM + */ + function WikiExporter( &$db, $history = MW_EXPORT_CURRENT, + $buffer = MW_EXPORT_BUFFER, $text = MW_EXPORT_TEXT ) { + $this->db =& $db; + $this->history = $history; + $this->buffer = $buffer; + $this->writer = new XmlDumpWriter(); + $this->sink = new DumpOutput(); + $this->text = $text; + } + + /** + * Set the DumpOutput or DumpFilter object which will receive + * various row objects and XML output for filtering. Filters + * can be chained or used as callbacks. + * + * @param mixed $callback + */ + function setOutputSink( &$sink ) { + $this->sink =& $sink; + } + + function openStream() { + $output = $this->writer->openStream(); + $this->sink->writeOpenStream( $output ); + } + + function closeStream() { + $output = $this->writer->closeStream(); + $this->sink->writeCloseStream( $output ); + } + + /** + * Dumps a series of page and revision records for all pages + * in the database, either including complete history or only + * the most recent version. + */ + function allPages() { + return $this->dumpFrom( '' ); + } + + /** + * Dumps a series of page and revision records for those pages + * in the database falling within the page_id range given. + * @param int $start Inclusive lower limit (this id is included) + * @param int $end Exclusive upper limit (this id is not included) + * If 0, no upper limit. + */ + function pagesByRange( $start, $end ) { + $condition = 'page_id >= ' . intval( $start ); + if( $end ) { + $condition .= ' AND page_id < ' . intval( $end ); + } + return $this->dumpFrom( $condition ); + } + + /** + * @param Title $title + */ + function pageByTitle( $title ) { + return $this->dumpFrom( + 'page_namespace=' . $title->getNamespace() . + ' AND page_title=' . $this->db->addQuotes( $title->getDbKey() ) ); + } + + function pageByName( $name ) { + $title = Title::newFromText( $name ); + if( is_null( $title ) ) { + return new WikiError( "Can't export invalid title" ); + } else { + return $this->pageByTitle( $title ); + } + } + + function pagesByName( $names ) { + foreach( $names as $name ) { + $this->pageByName( $name ); + } + } + + + // -------------------- private implementation below -------------------- + + # Generates the distinct list of authors of an article + # Not called by default (depends on $this->list_authors) + # Can be set by Special:Export when not exporting whole history + function do_list_authors ( $page , $revision , $cond ) { + $fname = "do_list_authors" ; + wfProfileIn( $fname ); + $this->author_list = ""; + $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND " . $cond ; + $result = $this->db->query( $sql, $fname ); + $resultset = $this->db->resultObject( $result ); + while( $row = $resultset->fetchObject() ) { + $this->author_list .= "" . + "" . + htmlentities( $row->rev_user_text ) . + "" . + "" . + $row->rev_user . + "" . + ""; + } + wfProfileOut( $fname ); + $this->author_list .= ""; + } + + function dumpFrom( $cond = '' ) { + $fname = 'WikiExporter::dumpFrom'; + wfProfileIn( $fname ); + + $page = $this->db->tableName( 'page' ); + $revision = $this->db->tableName( 'revision' ); + $text = $this->db->tableName( 'text' ); + + if( $this->history == MW_EXPORT_FULL ) { + $join = 'page_id=rev_page'; + } elseif( $this->history == MW_EXPORT_CURRENT ) { + if ( $this->list_authors && $cond != '' ) { // List authors, if so desired + $this->do_list_authors ( $page , $revision , $cond ); + } + $join = 'page_id=rev_page AND page_latest=rev_id'; + } else { + wfProfileOut( $fname ); + return new WikiError( "$fname given invalid history dump type." ); + } + $where = ( $cond == '' ) ? '' : "$cond AND"; + + if( $this->buffer == MW_EXPORT_STREAM ) { + $prev = $this->db->bufferResults( false ); + } + if( $cond == '' ) { + // Optimization hack for full-database dump + $revindex = $pageindex = $this->db->useIndexClause("PRIMARY"); + $straight = ' /*! STRAIGHT_JOIN */ '; + } else { + $pageindex = ''; + $revindex = ''; + $straight = ''; + } + if( $this->text == MW_EXPORT_STUB ) { + $sql = "SELECT $straight * FROM + $page $pageindex, + $revision $revindex + WHERE $where $join + ORDER BY page_id"; + } else { + $sql = "SELECT $straight * FROM + $page $pageindex, + $revision $revindex, + $text + WHERE $where $join AND rev_text_id=old_id + ORDER BY page_id"; + } + $result = $this->db->query( $sql, $fname ); + $wrapper = $this->db->resultObject( $result ); + $this->outputStream( $wrapper ); + + if ( $this->list_authors ) { + $this->outputStream( $wrapper ); + } + + if( $this->buffer == MW_EXPORT_STREAM ) { + $this->db->bufferResults( $prev ); + } + + wfProfileOut( $fname ); + } + + /** + * Runs through a query result set dumping page and revision records. + * The result set should be sorted/grouped by page to avoid duplicate + * page records in the output. + * + * The result set will be freed once complete. Should be safe for + * streaming (non-buffered) queries, as long as it was made on a + * separate database connection not managed by LoadBalancer; some + * blob storage types will make queries to pull source data. + * + * @param ResultWrapper $resultset + * @access private + */ + function outputStream( $resultset ) { + $last = null; + while( $row = $resultset->fetchObject() ) { + if( is_null( $last ) || + $last->page_namespace != $row->page_namespace || + $last->page_title != $row->page_title ) { + if( isset( $last ) ) { + $output = $this->writer->closePage(); + $this->sink->writeClosePage( $output ); + } + $output = $this->writer->openPage( $row ); + $this->sink->writeOpenPage( $row, $output ); + $last = $row; + } + $output = $this->writer->writeRevision( $row ); + $this->sink->writeRevision( $row, $output ); + } + if( isset( $last ) ) { + $output = $this->author_list . $this->writer->closePage(); + $this->sink->writeClosePage( $output ); + } + $resultset->free(); + } +} + +class XmlDumpWriter { + + /** + * Returns the export schema version. + * @return string + */ + function schemaVersion() { + return "0.3"; // FIXME: upgrade to 0.4 when updated XSD is ready, for the revision deletion bits + } + + /** + * Opens the XML output stream's root element. + * This does not include an xml directive, so is safe to include + * as a subelement in a larger XML stream. Namespace and XML Schema + * references are included. + * + * Output will be encoded in UTF-8. + * + * @return string + */ + function openStream() { + global $wgContLanguageCode; + $ver = $this->schemaVersion(); + return wfElement( 'mediawiki', array( + 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/", + 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", + 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " . + "http://www.mediawiki.org/xml/export-$ver.xsd", + 'version' => $ver, + 'xml:lang' => $wgContLanguageCode ), + null ) . + "\n" . + $this->siteInfo(); + } + + function siteInfo() { + $info = array( + $this->sitename(), + $this->homelink(), + $this->generator(), + $this->caseSetting(), + $this->namespaces() ); + return " \n " . + implode( "\n ", $info ) . + "\n \n"; + } + + function sitename() { + global $wgSitename; + return wfElement( 'sitename', array(), $wgSitename ); + } + + function generator() { + global $wgVersion; + return wfElement( 'generator', array(), "MediaWiki $wgVersion" ); + } + + function homelink() { + $page = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + return wfElement( 'base', array(), $page->getFullUrl() ); + } + + function caseSetting() { + global $wgCapitalLinks; + // "case-insensitive" option is reserved for future + $sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; + return wfElement( 'case', array(), $sensitivity ); + } + + function namespaces() { + global $wgContLang; + $spaces = " \n"; + foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) { + $spaces .= ' ' . wfElement( 'namespace', array( 'key' => $ns ), $title ) . "\n"; + } + $spaces .= " "; + return $spaces; + } + + /** + * Closes the output stream with the closing root element. + * Call when finished dumping things. + */ + function closeStream() { + return "\n"; + } + + + /** + * Opens a section on the output stream, with data + * from the given database row. + * + * @param object $row + * @return string + * @access private + */ + function openPage( $row ) { + $out = " \n"; + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $out .= ' ' . wfElementClean( 'title', array(), $title->getPrefixedText() ) . "\n"; + $out .= ' ' . wfElement( 'id', array(), strval( $row->page_id ) ) . "\n"; + if( '' != $row->page_restrictions ) { + $out .= ' ' . wfElement( 'restrictions', array(), + strval( $row->page_restrictions ) ) . "\n"; + } + return $out; + } + + /** + * Closes a section on the output stream. + * + * @access private + */ + function closePage() { + return " \n"; + } + + /** + * Dumps a section on the output stream, with + * data filled in from the given database row. + * + * @param object $row + * @return string + * @access private + */ + function writeRevision( $row ) { + $fname = 'WikiExporter::dumpRev'; + wfProfileIn( $fname ); + + $out = " \n"; + $out .= " " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n"; + + $ts = wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); + $out .= " " . wfElement( 'timestamp', null, $ts ) . "\n"; + + if( $row->rev_deleted & Revision::DELETED_USER ) { + $out .= " " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; + } else { + $out .= " \n"; + if( $row->rev_user ) { + $out .= " " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n"; + $out .= " " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n"; + } else { + $out .= " " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n"; + } + $out .= " \n"; + } + + if( $row->rev_minor_edit ) { + $out .= " \n"; + } + if( $row->rev_deleted & Revision::DELETED_COMMENT ) { + $out .= " " . wfElement( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; + } elseif( $row->rev_comment != '' ) { + $out .= " " . wfElementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n"; + } + + if( $row->rev_deleted & Revision::DELETED_TEXT ) { + $out .= " " . wfElement( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; + } elseif( isset( $row->old_text ) ) { + // Raw text from the database may have invalid chars + $text = strval( Revision::getRevisionText( $row ) ); + $out .= " " . wfElementClean( 'text', + array( 'xml:space' => 'preserve' ), + strval( $text ) ) . "\n"; + } else { + // Stub output + $out .= " " . wfElement( 'text', + array( 'id' => $row->rev_text_id ), + "" ) . "\n"; + } + + $out .= " \n"; + + wfProfileOut( $fname ); + return $out; + } + +} + + +/** + * Base class for output stream; prints to stdout or buffer or whereever. + */ +class DumpOutput { + function writeOpenStream( $string ) { + $this->write( $string ); + } + + function writeCloseStream( $string ) { + $this->write( $string ); + } + + function writeOpenPage( $page, $string ) { + $this->write( $string ); + } + + function writeClosePage( $string ) { + $this->write( $string ); + } + + function writeRevision( $rev, $string ) { + $this->write( $string ); + } + + /** + * Override to write to a different stream type. + * @return bool + */ + function write( $string ) { + print $string; + } +} + +/** + * Stream outputter to send data to a file. + */ +class DumpFileOutput extends DumpOutput { + var $handle; + + function DumpFileOutput( $file ) { + $this->handle = fopen( $file, "wt" ); + } + + function write( $string ) { + fputs( $this->handle, $string ); + } +} + +/** + * Stream outputter to send data to a file via some filter program. + * Even if compression is available in a library, using a separate + * program can allow us to make use of a multi-processor system. + */ +class DumpPipeOutput extends DumpFileOutput { + function DumpPipeOutput( $command, $file = null ) { + if( !is_null( $file ) ) { + $command .= " > " . wfEscapeShellArg( $file ); + } + $this->handle = popen( $command, "w" ); + } +} + +/** + * Sends dump output via the gzip compressor. + */ +class DumpGZipOutput extends DumpPipeOutput { + function DumpGZipOutput( $file ) { + parent::DumpPipeOutput( "gzip", $file ); + } +} + +/** + * Sends dump output via the bgzip2 compressor. + */ +class DumpBZip2Output extends DumpPipeOutput { + function DumpBZip2Output( $file ) { + parent::DumpPipeOutput( "bzip2", $file ); + } +} + +/** + * Sends dump output via the p7zip compressor. + */ +class Dump7ZipOutput extends DumpPipeOutput { + function Dump7ZipOutput( $file ) { + $command = "7za a -bd -si " . wfEscapeShellArg( $file ); + // Suppress annoying useless crap from p7zip + // Unfortunately this could suppress real error messages too + $command .= " >/dev/null 2>&1"; + parent::DumpPipeOutput( $command ); + } +} + + + +/** + * Dump output filter class. + * This just does output filtering and streaming; XML formatting is done + * higher up, so be careful in what you do. + */ +class DumpFilter { + function DumpFilter( &$sink ) { + $this->sink =& $sink; + } + + function writeOpenStream( $string ) { + $this->sink->writeOpenStream( $string ); + } + + function writeCloseStream( $string ) { + $this->sink->writeCloseStream( $string ); + } + + function writeOpenPage( $page, $string ) { + $this->sendingThisPage = $this->pass( $page, $string ); + if( $this->sendingThisPage ) { + $this->sink->writeOpenPage( $page, $string ); + } + } + + function writeClosePage( $string ) { + if( $this->sendingThisPage ) { + $this->sink->writeClosePage( $string ); + $this->sendingThisPage = false; + } + } + + function writeRevision( $rev, $string ) { + if( $this->sendingThisPage ) { + $this->sink->writeRevision( $rev, $string ); + } + } + + /** + * Override for page-based filter types. + * @return bool + */ + function pass( $page, $string ) { + return true; + } +} + +/** + * Simple dump output filter to exclude all talk pages. + */ +class DumpNotalkFilter extends DumpFilter { + function pass( $page ) { + return !Namespace::isTalk( $page->page_namespace ); + } +} + +/** + * Dump output filter to include or exclude pages in a given set of namespaces. + */ +class DumpNamespaceFilter extends DumpFilter { + var $invert = false; + var $namespaces = array(); + + function DumpNamespaceFilter( &$sink, $param ) { + parent::DumpFilter( $sink ); + + $constants = array( + "NS_MAIN" => NS_MAIN, + "NS_TALK" => NS_TALK, + "NS_USER" => NS_USER, + "NS_USER_TALK" => NS_USER_TALK, + "NS_PROJECT" => NS_PROJECT, + "NS_PROJECT_TALK" => NS_PROJECT_TALK, + "NS_IMAGE" => NS_IMAGE, + "NS_IMAGE_TALK" => NS_IMAGE_TALK, + "NS_MEDIAWIKI" => NS_MEDIAWIKI, + "NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK, + "NS_TEMPLATE" => NS_TEMPLATE, + "NS_TEMPLATE_TALK" => NS_TEMPLATE_TALK, + "NS_HELP" => NS_HELP, + "NS_HELP_TALK" => NS_HELP_TALK, + "NS_CATEGORY" => NS_CATEGORY, + "NS_CATEGORY_TALK" => NS_CATEGORY_TALK ); + + if( $param{0} == '!' ) { + $this->invert = true; + $param = substr( $param, 1 ); + } + + foreach( explode( ',', $param ) as $key ) { + $key = trim( $key ); + if( isset( $constants[$key] ) ) { + $ns = $constants[$key]; + $this->namespaces[$ns] = true; + } elseif( is_numeric( $key ) ) { + $ns = intval( $key ); + $this->namespaces[$ns] = true; + } else { + throw new MWException( "Unrecognized namespace key '$key'\n" ); + } + } + } + + function pass( $page ) { + $match = isset( $this->namespaces[$page->page_namespace] ); + return $this->invert xor $match; + } +} + + +/** + * Dump output filter to include only the last revision in each page sequence. + */ +class DumpLatestFilter extends DumpFilter { + var $page, $pageString, $rev, $revString; + + function writeOpenPage( $page, $string ) { + $this->page = $page; + $this->pageString = $string; + } + + function writeClosePage( $string ) { + if( $this->rev ) { + $this->sink->writeOpenPage( $this->page, $this->pageString ); + $this->sink->writeRevision( $this->rev, $this->revString ); + $this->sink->writeClosePage( $string ); + } + $this->rev = null; + $this->revString = null; + $this->page = null; + $this->pageString = null; + } + + function writeRevision( $rev, $string ) { + if( $rev->rev_id == $this->page->page_latest ) { + $this->rev = $rev; + $this->revString = $string; + } + } +} + +/** + * Base class for output stream; prints to stdout or buffer or whereever. + */ +class DumpMultiWriter { + function DumpMultiWriter( $sinks ) { + $this->sinks = $sinks; + $this->count = count( $sinks ); + } + + function writeOpenStream( $string ) { + for( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->writeOpenStream( $string ); + } + } + + function writeCloseStream( $string ) { + for( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->writeCloseStream( $string ); + } + } + + function writeOpenPage( $page, $string ) { + for( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->writeOpenPage( $page, $string ); + } + } + + function writeClosePage( $string ) { + for( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->writeClosePage( $string ); + } + } + + function writeRevision( $rev, $string ) { + for( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->writeRevision( $rev, $string ); + } + } +} + +function xmlsafe( $string ) { + $fname = 'xmlsafe'; + wfProfileIn( $fname ); + + /** + * The page may contain old data which has not been properly normalized. + * Invalid UTF-8 sequences or forbidden control characters will make our + * XML output invalid, so be sure to strip them out. + */ + $string = UtfNormal::cleanUp( $string ); + + $string = htmlspecialchars( $string ); + wfProfileOut( $fname ); + return $string; +} + +?> diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php new file mode 100644 index 00000000..21f632ec --- /dev/null +++ b/includes/ExternalEdit.php @@ -0,0 +1,77 @@ + + * @package MediaWiki + */ + +/** + * + * @package MediaWiki + * + * Support for external editors to modify both text and files + * in external applications. It works as follows: MediaWiki + * sends a meta-file with the MIME type 'application/x-external-editor' + * to the client. The user has to associate that MIME type with + * a helper application (a reference implementation in Perl + * can be found in extensions/ee), which will launch the editor, + * and save the modified data back to the server. + * + */ + +class ExternalEdit { + + function ExternalEdit ( $article, $mode ) { + global $wgInputEncoding; + $this->mArticle =& $article; + $this->mTitle =& $article->mTitle; + $this->mCharset = $wgInputEncoding; + $this->mMode = $mode; + } + + function edit() { + global $wgOut, $wgScript, $wgScriptPath, $wgServer, $wgLang; + $wgOut->disable(); + $name=$this->mTitle->getText(); + $pos=strrpos($name,".")+1; + header ( "Content-type: application/x-external-editor; charset=".$this->mCharset ); + + # $type can be "Edit text", "Edit file" or "Diff text" at the moment + # See the protocol specifications at [[m:Help:External editors/Tech]] for + # details. + if(!isset($this->mMode)) { + $type="Edit text"; + $url=$this->mTitle->getFullURL("action=edit&internaledit=true"); + # *.wiki file extension is used by some editors for syntax + # highlighting, so we follow that convention + $extension="wiki"; + } elseif($this->mMode=="file") { + $type="Edit file"; + $image = Image::newFromTitle( $this->mTitle ); + $img_url = $image->getURL(); + if(strpos($img_url,"://")) { + $url = $img_url; + } else { + $url = $wgServer . $img_url; + } + $extension=substr($name, $pos); + } + $special=$wgLang->getNsText(NS_SPECIAL); + $control = << diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php new file mode 100644 index 00000000..79f1a528 --- /dev/null +++ b/includes/ExternalStore.php @@ -0,0 +1,70 @@ +fetchFromURL($url); + } + + /** + * Get an external store object of the given type + */ + function &getStoreObject( $proto ) { + global $wgExternalStores; + if (!$wgExternalStores) + return false; + /* Protocol not enabled */ + if (!in_array( $proto, $wgExternalStores )) + return false; + + $class='ExternalStore'.ucfirst($proto); + /* Preloaded modules might exist, especially ones serving multiple protocols */ + if (!class_exists($class)) { + if (!include_once($class.'.php')) + return false; + } + $store=new $class(); + return $store; + } + + /** + * Store a data item to an external store, identified by a partial URL + * The protocol part is used to identify the class, the rest is passed to the + * class itself as a parameter. + * Returns the URL of the stored data item, or false on error + */ + function insert( $url, $data ) { + list( $proto, $params ) = explode( '://', $url, 2 ); + $store =& ExternalStore::getStoreObject( $proto ); + if ( $store === false ) { + return false; + } else { + return $store->store( $params, $data ); + } + } +} +?> diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php new file mode 100644 index 00000000..f610df80 --- /dev/null +++ b/includes/ExternalStoreDB.php @@ -0,0 +1,150 @@ +allowLagged(true); + return $wgExternalLoadBalancers[$cluster]; + } + + /** @todo Document.*/ + function &getSlave( $cluster ) { + $lb =& $this->getLoadBalancer( $cluster ); + return $lb->getConnection( DB_SLAVE ); + } + + /** @todo Document.*/ + function &getMaster( $cluster ) { + $lb =& $this->getLoadBalancer( $cluster ); + return $lb->getConnection( DB_MASTER ); + } + + /** @todo Document.*/ + function getTable( &$db ) { + $table = $db->getLBInfo( 'blobs table' ); + if ( is_null( $table ) ) { + $table = 'blobs'; + } + return $table; + } + + /** + * Fetch data from given URL + * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage. + */ + function fetchFromURL($url) { + $path = explode( '/', $url ); + $cluster = $path[2]; + $id = $path[3]; + if ( isset( $path[4] ) ) { + $itemID = $path[4]; + } else { + $itemID = false; + } + + $ret =& $this->fetchBlob( $cluster, $id, $itemID ); + + if ( $itemID !== false && $ret !== false ) { + return $ret->getItem( $itemID ); + } + return $ret; + } + + /** + * Fetch a blob item out of the database; a cache of the last-loaded + * blob will be kept so that multiple loads out of a multi-item blob + * can avoid redundant database access and decompression. + * @param $cluster + * @param $id + * @param $itemID + * @return mixed + * @private + */ + function &fetchBlob( $cluster, $id, $itemID ) { + global $wgExternalBlobCache; + $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/"; + if( isset( $wgExternalBlobCache[$cacheID] ) ) { + wfDebug( "ExternalStoreDB::fetchBlob cache hit on $cacheID\n" ); + return $wgExternalBlobCache[$cacheID]; + } + + wfDebug( "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" ); + + $dbr =& $this->getSlave( $cluster ); + $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ) ); + if ( $ret === false ) { + wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" ); + // Try the master + $dbw =& $this->getMaster( $cluster ); + $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ) ); + if( $ret === false) { + wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" ); + } + } + if( $itemID !== false && $ret !== false ) { + // Unserialise object; caller extracts item + $ret = unserialize( $ret ); + } + + $wgExternalBlobCache = array( $cacheID => &$ret ); + return $ret; + } + + /** + * Insert a data item into a given cluster + * + * @param $cluster String: the cluster name + * @param $data String: the data item + * @return string URL + */ + function store( $cluster, $data ) { + $fname = 'ExternalStoreDB::store'; + + $dbw =& $this->getMaster( $cluster ); + + $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' ); + $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname ); + $id = $dbw->insertId(); + if ( $dbw->getFlag( DBO_TRX ) ) { + $dbw->immediateCommit(); + } + return "DB://$cluster/$id"; + } +} +?> diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php new file mode 100644 index 00000000..daf62cc4 --- /dev/null +++ b/includes/ExternalStoreHttp.php @@ -0,0 +1,23 @@ + diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php new file mode 100644 index 00000000..ae05385a --- /dev/null +++ b/includes/FakeTitle.php @@ -0,0 +1,88 @@ +error(); } + + // PHP <5.1 compatibility + function getInterwikiLink() { $this->error(); } + function getInterwikiCached() { $this->error(); } + function isLocal() { $this->error(); } + function isTrans() { $this->error(); } + function touchArray( $titles, $timestamp = '' ) { $this->error(); } + function getText() { $this->error(); } + function getPartialURL() { $this->error(); } + function getDBkey() { $this->error(); } + function getNamespace() { $this->error(); } + function getNsText() { $this->error(); } + function getSubjectNsText() { $this->error(); } + function getInterwiki() { $this->error(); } + function getFragment() { $this->error(); } + function getDefaultNamespace() { $this->error(); } + function getIndexTitle() { $this->error(); } + function getPrefixedDBkey() { $this->error(); } + function getPrefixedText() { $this->error(); } + function getFullText() { $this->error(); } + function getPrefixedURL() { $this->error(); } + function getFullURL() {$this->error(); } + function getLocalURL() { $this->error(); } + function escapeLocalURL() { $this->error(); } + function escapeFullURL() { $this->error(); } + function getInternalURL() { $this->error(); } + function getEditURL() { $this->error(); } + function getEscapedText() { $this->error(); } + function isExternal() { $this->error(); } + function isSemiProtected() { $this->error(); } + function isProtected() { $this->error(); } + function userIsWatching() { $this->error(); } + function userCan() { $this->error(); } + function userCanEdit() { $this->error(); } + function userCanMove() { $this->error(); } + function isMovable() { $this->error(); } + function userCanRead() { $this->error(); } + function isTalkPage() { $this->error(); } + function isCssJsSubpage() { $this->error(); } + function isValidCssJsSubpage() { $this->error(); } + function getSkinFromCssJsSubpage() { $this->error(); } + function isCssSubpage() { $this->error(); } + function isJsSubpage() { $this->error(); } + function userCanEditCssJsSubpage() { $this->error(); } + function loadRestrictions( $res ) { $this->error(); } + function getRestrictions($action) { $this->error(); } + function isDeleted() { $this->error(); } + function getArticleID( $flags = 0 ) { $this->error(); } + function getLatestRevID() { $this->error(); } + function resetArticleID( $newid ) { $this->error(); } + function invalidateCache() { $this->error(); } + function getTalkPage() { $this->error(); } + function getSubjectPage() { $this->error(); } + function getLinksTo() { $this->error(); } + function getTemplateLinksTo() { $this->error(); } + function getBrokenLinksFrom() { $this->error(); } + function getSquidURLs() { $this->error(); } + function moveNoAuth() { $this->error(); } + function isValidMoveOperation() { $this->error(); } + function moveTo() { $this->error(); } + function moveOverExistingRedirect() { $this->error(); } + function moveToNewTitle() { $this->error(); } + function isValidMoveTarget() { $this->error(); } + function createRedirect() { $this->error(); } + function getParentCategories() { $this->error(); } + function getParentCategoryTree() { $this->error(); } + function pageCond() { $this->error(); } + function getPreviousRevisionID() { $this->error(); } + function getNextRevisionID() { $this->error(); } + function equals() { $this->error(); } + function exists() { $this->error(); } + function isAlwaysKnown() { $this->error(); } + function touchLinks() { $this->error(); } + function trackbackURL() { $this->error(); } + function trackbackRDF() { $this->error(); } +} + +?> diff --git a/includes/Feed.php b/includes/Feed.php new file mode 100644 index 00000000..7663e820 --- /dev/null +++ b/includes/Feed.php @@ -0,0 +1,310 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html + +/** + * Contain a feed class as well as classes to build rss / atom ... feeds + * Available feeds are defined in Defines.php + * @package MediaWiki + */ + + +/** + * @todo document + * @package MediaWiki + */ +class FeedItem { + /**#@+ + * @var string + * @private + */ + var $Title = 'Wiki'; + var $Description = ''; + var $Url = ''; + var $Date = ''; + var $Author = ''; + /**#@-*/ + + /**#@+ + * @todo document + */ + function FeedItem( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { + $this->Title = $Title; + $this->Description = $Description; + $this->Url = $Url; + $this->Date = $Date; + $this->Author = $Author; + $this->Comments = $Comments; + } + + /** + * @static + */ + function xmlEncode( $string ) { + $string = str_replace( "\r\n", "\n", $string ); + $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string ); + return htmlspecialchars( $string ); + } + + function getTitle() { return $this->xmlEncode( $this->Title ); } + function getUrl() { return $this->xmlEncode( $this->Url ); } + function getDescription() { return $this->xmlEncode( $this->Description ); } + function getLanguage() { + global $wgContLanguageCode; + return $wgContLanguageCode; + } + function getDate() { return $this->Date; } + function getAuthor() { return $this->xmlEncode( $this->Author ); } + function getComments() { return $this->xmlEncode( $this->Comments ); } + /**#@-*/ +} + +/** + * @todo document + * @package MediaWiki + */ +class ChannelFeed extends FeedItem { + /**#@+ + * Abstract function, override! + * @abstract + */ + + /** + * Generate Header of the feed + */ + function outHeader() { + # print ""; + } + + /** + * Generate an item + * @param $item + */ + function outItem( $item ) { + # print "..."; + } + + /** + * Generate Footer of the feed + */ + function outFooter() { + # print ""; + } + /**#@-*/ + + /** + * Setup and send HTTP headers. Don't send any content; + * content might end up being cached and re-sent with + * these same headers later. + * + * This should be called from the outHeader() method, + * but can also be called separately. + * + * @public + */ + function httpHeaders() { + global $wgOut; + + # We take over from $wgOut, excepting its cache header info + $wgOut->disable(); + $mimetype = $this->contentType(); + header( "Content-type: $mimetype; charset=UTF-8" ); + $wgOut->sendCacheControl(); + + } + + /** + * Return an internet media type to be sent in the headers. + * + * @return string + * @private + */ + function contentType() { + global $wgRequest; + $ctype = $wgRequest->getVal('ctype','application/xml'); + $allowedctypes = array('application/xml','text/xml','application/rss+xml','application/atom+xml'); + return (in_array($ctype, $allowedctypes) ? $ctype : 'application/xml'); + } + + /** + * Output the initial XML headers with a stylesheet for legibility + * if someone finds it in a browser. + * @private + */ + function outXmlHeader() { + global $wgServer, $wgStylePath; + + $this->httpHeaders(); + echo '' . "\n"; + echo '\n"; + } +} + +/** + * Generate a RSS feed + * @todo document + * @package MediaWiki + */ +class RSSFeed extends ChannelFeed { + + /** + * Format a date given a timestamp + * @param integer $ts Timestamp + * @return string Date string + */ + function formatTime( $ts ) { + return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) ); + } + + /** + * Ouput an RSS 2.0 header + */ + function outHeader() { + global $wgVersion; + + $this->outXmlHeader(); + ?> + + <?php print $this->getTitle() ?> + getUrl() ?> + getDescription() ?> + getLanguage() ?> + MediaWiki + formatTime( wfTimestampNow() ) ?> + + + <?php print $item->getTitle() ?> + getUrl() ?> + getDescription() ?> + getDate() ) { ?>formatTime( $item->getDate() ) ?> + getAuthor() ) { ?>getAuthor() ?> + getComments() ) { ?>getComments() ?> + + + +outXmlHeader(); + ?> + getFeedId() ?> + <?php print $this->getTitle() ?> + + + formatTime( wfTimestampNow() ) ?>Z + getDescription() ?> + MediaWiki + +getSelfUrl(); + } + + /** + * Atom 1.0 requests a self-reference to the feed. + * @return string + * @private + */ + function getSelfUrl() { + global $wgRequest; + return htmlspecialchars( $wgRequest->getFullRequestURL() ); + } + + /** + * Output a given item. + * @param $item + */ + function outItem( $item ) { + global $wgMimeType; + ?> + + getUrl() ?> + <?php print $item->getTitle() ?> + + getDate() ) { ?> + formatTime( $item->getDate() ) ?>Z + + + getDescription() ?> + getAuthor() ) { ?>getAuthor() ?> + + +getComments() ) { ?>getComments() ?> + */ + } + + /** + * Outputs the footer for Atom 1.0 feed (basicly '\'). + */ + function outFooter() {?> + diff --git a/includes/FileStore.php b/includes/FileStore.php new file mode 100644 index 00000000..85aaedfe --- /dev/null +++ b/includes/FileStore.php @@ -0,0 +1,377 @@ +mGroup = $group; + $this->mDirectory = $directory; + $this->mPath = $path; + $this->mHashLevel = $hash; + } + + /** + * Acquire a lock; use when performing write operations on a store. + * This is attached to your master database connection, so if you + * suffer an uncaught error the lock will be released when the + * connection is closed. + * + * @fixme Probably only works on MySQL. Abstract to the Database class? + */ + static function lock() { + $fname = __CLASS__ . '::' . __FUNCTION__; + + $dbw = wfGetDB( DB_MASTER ); + $lockname = $dbw->addQuotes( FileStore::lockName() ); + $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", $fname ); + $row = $dbw->fetchObject( $result ); + $dbw->freeResult( $result ); + + if( $row->lockstatus == 1 ) { + return true; + } else { + wfDebug( "$fname failed to acquire lock\n" ); + return false; + } + } + + /** + * Release the global file store lock. + */ + static function unlock() { + $fname = __CLASS__ . '::' . __FUNCTION__; + + $dbw = wfGetDB( DB_MASTER ); + $lockname = $dbw->addQuotes( FileStore::lockName() ); + $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", $fname ); + $row = $dbw->fetchObject( $result ); + $dbw->freeResult( $result ); + } + + private static function lockName() { + global $wgDBname, $wgDBprefix; + return "MediaWiki.{$wgDBname}.{$wgDBprefix}FileStore"; + } + + /** + * Copy a file into the file store from elsewhere in the filesystem. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @param $flags + * DELETE_ORIGINAL - remove the source file on transaction commit. + * + * @throws FSException if copy can't be completed + * @return FSTransaction + */ + function insert( $key, $sourcePath, $flags=0 ) { + $destPath = $this->filePath( $key ); + return $this->copyFile( $sourcePath, $destPath, $flags ); + } + + /** + * Copy a file from the file store to elsewhere in the filesystem. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @param $flags + * DELETE_ORIGINAL - remove the source file on transaction commit. + * + * @throws FSException if copy can't be completed + * @return FSTransaction on success + */ + function export( $key, $destPath, $flags=0 ) { + $sourcePath = $this->filePath( $key ); + return $this->copyFile( $sourcePath, $destPath, $flags ); + } + + private function copyFile( $sourcePath, $destPath, $flags=0 ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + + if( !file_exists( $sourcePath ) ) { + // Abort! Abort! + throw new FSException( "missing source file '$sourcePath'\n" ); + } + + $transaction = new FSTransaction(); + + if( $flags & self::DELETE_ORIGINAL ) { + $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath ); + } + + if( file_exists( $destPath ) ) { + // An identical file is already present; no need to copy. + } else { + if( !file_exists( dirname( $destPath ) ) ) { + wfSuppressWarnings(); + $ok = mkdir( dirname( $destPath ), 0777, true ); + wfRestoreWarnings(); + + if( !$ok ) { + throw new FSException( + "failed to create directory for '$destPath'\n" ); + } + } + + wfSuppressWarnings(); + $ok = copy( $sourcePath, $destPath ); + wfRestoreWarnings(); + + if( $ok ) { + wfDebug( "$fname copied '$sourcePath' to '$destPath'\n" ); + $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath ); + } else { + throw new FSException( + "$fname failed to copy '$sourcePath' to '$destPath'\n" ); + } + } + + return $transaction; + } + + /** + * Delete a file from the file store. + * Caller's responsibility to make sure it's not being used by another row. + * + * File is not actually removed until transaction commit. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @throws FSException if file can't be deleted + * @return FSTransaction + */ + function delete( $key ) { + $destPath = $this->filePath( $key ); + if( false === $destPath ) { + throw new FSExcepton( "file store does not contain file '$key'" ); + } else { + return FileStore::deleteFile( $destPath ); + } + } + + /** + * Delete a non-managed file on a transactional basis. + * + * File is not actually removed until transaction commit. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $path file to remove + * @throws FSException if file can't be deleted + * @return FSTransaction + * + * @fixme Might be worth preliminary permissions check + */ + static function deleteFile( $path ) { + if( file_exists( $path ) ) { + $transaction = new FSTransaction(); + $transaction->addCommit( FSTransaction::DELETE_FILE, $path ); + return $transaction; + } else { + throw new FSException( "cannot delete missing file '$path'" ); + } + } + + /** + * Stream a contained file directly to HTTP output. + * Will throw a 404 if file is missing; 400 if invalid key. + * @return true on success, false on failure + */ + function stream( $key ) { + $path = $this->filePath( $key ); + if( $path === false ) { + wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." ); + return false; + } + + if( file_exists( $path ) ) { + // Set the filename for more convenient save behavior from browsers + // FIXME: Is this safe? + header( 'Content-Disposition: inline; filename="' . $key . '"' ); + + require_once 'StreamFile.php'; + wfStreamFile( $path ); + } else { + return wfHttpError( 404, "Not found", + "The requested resource does not exist." ); + } + } + + /** + * Confirm that the given file key is valid. + * Note that a valid key may refer to a file that does not exist. + * + * Key should consist of a 32-digit base-36 SHA-1 hash and + * an optional alphanumeric extension, all lowercase. + * The whole must not exceed 64 characters. + * + * @param $key + * @return boolean + */ + static function validKey( $key ) { + return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key ); + } + + + /** + * Calculate file storage key from a file on disk. + * You must pass an extension to it, as some files may be calculated + * out of a temporary file etc. + * + * @param $path to file + * @param $extension + * @return string or false if could not open file or bad extension + */ + static function calculateKey( $path, $extension ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + + wfSuppressWarnings(); + $hash = sha1_file( $path ); + wfRestoreWarnings(); + if( $hash === false ) { + wfDebug( "$fname: couldn't hash file '$path'\n" ); + return false; + } + + $base36 = wfBaseConvert( $hash, 16, 36, 32 ); + if( $extension == '' ) { + $key = $base36; + } else { + $key = $base36 . '.' . $extension; + } + + // Sanity check + if( self::validKey( $key ) ) { + return $key; + } else { + wfDebug( "$fname: generated bad key '$key'\n" ); + return false; + } + } + + /** + * Return filesystem path to the given file. + * Note that the file may or may not exist. + * @return string or false if an invalid key + */ + function filePath( $key ) { + if( self::validKey( $key ) ) { + return $this->mDirectory . DIRECTORY_SEPARATOR . + $this->hashPath( $key, DIRECTORY_SEPARATOR ); + } else { + return false; + } + } + + /** + * Return URL path to the given file, if the store is public. + * @return string or false if not public + */ + function urlPath( $key ) { + if( $this->mUrl && self::validKey( $key ) ) { + return $this->mUrl . '/' . $this->hashPath( $key, '/' ); + } else { + return false; + } + } + + private function hashPath( $key, $separator ) { + $parts = array(); + for( $i = 0; $i < $this->mHashLevel; $i++ ) { + $parts[] = $key{$i}; + } + $parts[] = $key; + return implode( $separator, $parts ); + } +} + +/** + * Wrapper for file store transaction stuff. + * + * FileStore methods may return one of these for undoable operations; + * you can then call its rollback() or commit() methods to perform + * final cleanup if dependent database work fails or succeeds. + */ +class FSTransaction { + const DELETE_FILE = 1; + + /** + * Combine more items into a fancier transaction + */ + function add( FSTransaction $transaction ) { + $this->mOnCommit = array_merge( + $this->mOnCommit, $transaction->mOnCommit ); + $this->mOnRollback = array_merge( + $this->mOnRollback, $transaction->mOnRollback ); + } + + /** + * Perform final actions for success. + * @return true if actions applied ok, false if errors + */ + function commit() { + return $this->apply( $this->mOnCommit ); + } + + /** + * Perform final actions for failure. + * @return true if actions applied ok, false if errors + */ + function rollback() { + return $this->apply( $this->mOnRollback ); + } + + // --- Private and friend functions below... + + function __construct() { + $this->mOnCommit = array(); + $this->mOnRollback = array(); + } + + function addCommit( $action, $path ) { + $this->mOnCommit[] = array( $action, $path ); + } + + function addRollback( $action, $path ) { + $this->mOnRollback[] = array( $action, $path ); + } + + private function apply( $actions ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + $result = true; + foreach( $actions as $item ) { + list( $action, $path ) = $item; + if( $action == self::DELETE_FILE ) { + wfSuppressWarnings(); + $ok = unlink( $path ); + wfRestoreWarnings(); + if( $ok ) + wfDebug( "$fname: deleting file '$path'\n" ); + else + wfDebug( "$fname: failed to delete file '$path'\n" ); + $result = $result && $ok; + } + } + return $result; + } +} + +class FSException extends MWException { } + +?> \ No newline at end of file diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php new file mode 100644 index 00000000..e2033486 --- /dev/null +++ b/includes/GlobalFunctions.php @@ -0,0 +1,2005 @@ += 3 ) { + $end = func_get_arg( 2 ); + return join( '', array_slice( $ar[0], $start, $end ) ); + } else { + return join( '', array_slice( $ar[0], $start ) ); + } + } +} + +if( !function_exists( 'floatval' ) ) { + /** + * First defined in PHP 4.2.0 + * @param mixed $var; + * @return float + */ + function floatval( $var ) { + return (float)$var; + } +} + +if ( !function_exists( 'array_diff_key' ) ) { + /** + * Exists in PHP 5.1.0+ + * Not quite compatible, two-argument version only + * Null values will cause problems due to this use of isset() + */ + function array_diff_key( $left, $right ) { + $result = $left; + foreach ( $left as $key => $value ) { + if ( isset( $right[$key] ) ) { + unset( $result[$key] ); + } + } + return $result; + } +} + + +/** + * Wrapper for clone() for PHP 4, for the moment. + * PHP 5 won't let you declare a 'clone' function, even conditionally, + * so it has to be a wrapper with a different name. + */ +function wfClone( $object ) { + // WARNING: clone() is not a function in PHP 5, so function_exists fails. + if( version_compare( PHP_VERSION, '5.0' ) < 0 ) { + return $object; + } else { + return clone( $object ); + } +} + +/** + * Where as we got a random seed + * @var bool $wgTotalViews + */ +$wgRandomSeeded = false; + +/** + * Seed Mersenne Twister + * Only necessary in PHP < 4.2.0 + * + * @return bool + */ +function wfSeedRandom() { + global $wgRandomSeeded; + + if ( ! $wgRandomSeeded && version_compare( phpversion(), '4.2.0' ) < 0 ) { + $seed = hexdec(substr(md5(microtime()),-8)) & 0x7fffffff; + mt_srand( $seed ); + $wgRandomSeeded = true; + } +} + +/** + * Get a random decimal value between 0 and 1, in a way + * not likely to give duplicate values for any realistic + * number of articles. + * + * @return string + */ +function wfRandom() { + # The maximum random value is "only" 2^31-1, so get two random + # values to reduce the chance of dupes + $max = mt_getrandmax(); + $rand = number_format( (mt_rand() * $max + mt_rand()) + / $max / $max, 12, '.', '' ); + return $rand; +} + +/** + * We want / and : to be included as literal characters in our title URLs. + * %2F in the page titles seems to fatally break for some reason. + * + * @param $s String: + * @return string +*/ +function wfUrlencode ( $s ) { + $s = urlencode( $s ); + $s = preg_replace( '/%3[Aa]/', ':', $s ); + $s = preg_replace( '/%2[Ff]/', '/', $s ); + + return $s; +} + +/** + * Sends a line to the debug log if enabled or, optionally, to a comment in output. + * In normal operation this is a NOP. + * + * Controlling globals: + * $wgDebugLogFile - points to the log file + * $wgProfileOnly - if set, normal debug messages will not be recorded. + * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output. + * $wgDebugComments - if on, some debug items may appear in comments in the HTML output. + * + * @param $text String + * @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set + */ +function wfDebug( $text, $logonly = false ) { + global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; + + # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet + if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) { + return; + } + + if ( isset( $wgOut ) && $wgDebugComments && !$logonly ) { + $wgOut->debug( $text ); + } + if ( '' != $wgDebugLogFile && !$wgProfileOnly ) { + # Strip unprintables; they can switch terminal modes when binary data + # gets dumped, which is pretty annoying. + $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); + @error_log( $text, 3, $wgDebugLogFile ); + } +} + +/** + * Send a line to a supplementary debug log file, if configured, or main debug log if not. + * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log. + * + * @param $logGroup String + * @param $text String + * @param $public Bool: whether to log the event in the public log if no private + * log file is specified, (default true) + */ +function wfDebugLog( $logGroup, $text, $public = true ) { + global $wgDebugLogGroups, $wgDBname; + if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n"; + if( isset( $wgDebugLogGroups[$logGroup] ) ) { + $time = wfTimestamp( TS_DB ); + @error_log( "$time $wgDBname: $text", 3, $wgDebugLogGroups[$logGroup] ); + } else if ( $public === true ) { + wfDebug( $text, true ); + } +} + +/** + * Log for database errors + * @param $text String: database error message. + */ +function wfLogDBError( $text ) { + global $wgDBerrorLog; + if ( $wgDBerrorLog ) { + $host = trim(`hostname`); + $text = date('D M j G:i:s T Y') . "\t$host\t".$text; + error_log( $text, 3, $wgDBerrorLog ); + } +} + +/** + * @todo document + */ +function logProfilingData() { + global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; + global $wgProfiling, $wgUser; + $now = wfTime(); + + $elapsed = $now - $wgRequestTime; + if ( $wgProfiling ) { + $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); + $forward = ''; + if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) + $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR']; + if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) + $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP']; + if( !empty( $_SERVER['HTTP_FROM'] ) ) + $forward .= ' from ' . $_SERVER['HTTP_FROM']; + if( $forward ) + $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; + if( is_object($wgUser) && $wgUser->isAnon() ) + $forward .= ' anon'; + $log = sprintf( "%s\t%04.3f\t%s\n", + gmdate( 'YmdHis' ), $elapsed, + urldecode( $_SERVER['REQUEST_URI'] . $forward ) ); + if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) { + error_log( $log . $prof, 3, $wgDebugLogFile ); + } + } +} + +/** + * Check if the wiki read-only lock file is present. This can be used to lock + * off editing functions, but doesn't guarantee that the database will not be + * modified. + * @return bool + */ +function wfReadOnly() { + global $wgReadOnlyFile, $wgReadOnly; + + if ( !is_null( $wgReadOnly ) ) { + return (bool)$wgReadOnly; + } + if ( '' == $wgReadOnlyFile ) { + return false; + } + // Set $wgReadOnly for faster access next time + if ( is_file( $wgReadOnlyFile ) ) { + $wgReadOnly = file_get_contents( $wgReadOnlyFile ); + } else { + $wgReadOnly = false; + } + return (bool)$wgReadOnly; +} + + +/** + * Get a message from anywhere, for the current user language. + * + * 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 + */ +function wfMsg( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReal( $key, $args, true ); +} + +/** + * Same as above except doesn't transform the message + */ +function wfMsgNoTrans( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReal( $key, $args, true, false ); +} + +/** + * Get a message from anywhere, for the current global language + * set with $wgLanguageCode. + * + * Use this if the message should NOT change dependent on the + * language set in the user's preferences. This is the case for + * most text written into logs, as well as link targets (such as + * the name of the copyright policy page). Link titles, on the + * other hand, should be shown in the UI language. + * + * Note that MediaWiki allows users to change the user interface + * language in their preferences, but a single installation + * typically only contains content in one language. + * + * Be wary of this distinction: If you use wfMsg() where you should + * use wfMsgForContent(), a user of the software may have to + * customize over 70 messages in order to, e.g., fix a link in every + * possible language. + * + * @param $key String: lookup key for the message, usually + * defined in languages/Language.php + */ +function wfMsgForContent( $key ) { + global $wgForceUIMsgAsContentMsg; + $args = func_get_args(); + array_shift( $args ); + $forcontent = true; + if( is_array( $wgForceUIMsgAsContentMsg ) && + in_array( $key, $wgForceUIMsgAsContentMsg ) ) + $forcontent = false; + return wfMsgReal( $key, $args, true, $forcontent ); +} + +/** + * Same as above except doesn't transform the message + */ +function wfMsgForContentNoTrans( $key ) { + global $wgForceUIMsgAsContentMsg; + $args = func_get_args(); + array_shift( $args ); + $forcontent = true; + if( is_array( $wgForceUIMsgAsContentMsg ) && + in_array( $key, $wgForceUIMsgAsContentMsg ) ) + $forcontent = false; + return wfMsgReal( $key, $args, true, $forcontent, false ); +} + +/** + * Get a message from the language file, for the UI elements + */ +function wfMsgNoDB( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReal( $key, $args, false ); +} + +/** + * Get a message from the language file, for the content + */ +function wfMsgNoDBForContent( $key ) { + global $wgForceUIMsgAsContentMsg; + $args = func_get_args(); + array_shift( $args ); + $forcontent = true; + if( is_array( $wgForceUIMsgAsContentMsg ) && + in_array( $key, $wgForceUIMsgAsContentMsg ) ) + $forcontent = false; + return wfMsgReal( $key, $args, false, $forcontent ); +} + + +/** + * Really get a message + * @return $key String: key to get. + * @return $args + * @return $useDB Boolean + * @return String: the requested message. + */ +function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) { + $fname = 'wfMsgReal'; + + $message = wfMsgGetKey( $key, $useDB, $forContent, $transform ); + $message = wfMsgReplaceArgs( $message, $args ); + return $message; +} + +/** + * This function provides the message source for messages to be edited which are *not* stored in the database. + * @param $key String: + */ +function wfMsgWeirdKey ( $key ) { + $subsource = str_replace ( ' ' , '_' , $key ) ; + $source = wfMsgForContentNoTrans( $subsource ) ; + if ( $source == "<{$subsource}>" ) { + # Try again with first char lower case + $subsource = strtolower ( substr ( $subsource , 0 , 1 ) ) . substr ( $subsource , 1 ) ; + $source = wfMsgForContentNoTrans( $subsource ) ; + } + if ( $source == "<{$subsource}>" ) { + # Didn't work either, return blank text + $source = "" ; + } + return $source ; +} + +/** + * Fetch a message string value, but don't replace any keys yet. + * @param string $key + * @param bool $useDB + * @param bool $forContent + * @return string + * @private + */ +function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { + global $wgParser, $wgMsgParserOptions, $wgContLang, $wgMessageCache, $wgLang; + + if ( is_object( $wgMessageCache ) ) + $transstat = $wgMessageCache->getTransform(); + + if( is_object( $wgMessageCache ) ) { + if ( ! $transform ) + $wgMessageCache->disableTransform(); + $message = $wgMessageCache->get( $key, $useDB, $forContent ); + } else { + if( $forContent ) { + $lang = &$wgContLang; + } else { + $lang = &$wgLang; + } + + wfSuppressWarnings(); + + if( is_object( $lang ) ) { + $message = $lang->getMessage( $key ); + } else { + $message = false; + } + wfRestoreWarnings(); + if($message === false) + $message = Language::getMessage($key); + if ( $transform && strstr( $message, '{{' ) !== false ) { + $message = $wgParser->transformMsg($message, $wgMsgParserOptions); + } + } + + if ( is_object( $wgMessageCache ) && ! $transform ) + $wgMessageCache->setTransform( $transstat ); + + return $message; +} + +/** + * Replace message parameter keys on the given formatted output. + * + * @param string $message + * @param array $args + * @return string + * @private + */ +function wfMsgReplaceArgs( $message, $args ) { + # Fix windows line-endings + # Some messages are split with explode("\n", $msg) + $message = str_replace( "\r", '', $message ); + + // 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 ); + } + } + + return $message; +} + +/** + * Return an HTML-escaped version of a message. + * Parameter replacements, if any, are done *after* the HTML-escaping, + * so parameters may contain HTML (eg links or form controls). Be sure + * to pre-escape them if you really do want plaintext, or just wrap + * the whole thing in htmlspecialchars(). + * + * @param string $key + * @param string ... parameters + * @return string + */ +function wfMsgHtml( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key, true ) ), $args ); +} + +/** + * Return an HTML version of message + * Parameter replacements, if any, are done *after* parsing the wiki-text message, + * so parameters may contain HTML (eg links or form controls). Be sure + * to pre-escape them if you really do want plaintext, or just wrap + * the whole thing in htmlspecialchars(). + * + * @param string $key + * @param string ... parameters + * @return string + */ +function wfMsgWikiHtml( $key ) { + global $wgOut; + $args = func_get_args(); + array_shift( $args ); + return wfMsgReplaceArgs( $wgOut->parse( wfMsgGetKey( $key, true ), /* can't be set to false */ true ), $args ); +} + +/** + * Returns message in the requested format + * @param string $key Key of the message + * @param array $options Processing rules: + * parse: parses wikitext to html + * parseinline: parses wikitext to html and removes the surrounding p's added by parser or tidy + * escape: filters message trough htmlspecialchars + * replaceafter: parameters are substituted after parsing or escaping + */ +function wfMsgExt( $key, $options ) { + global $wgOut, $wgMsgParserOptions, $wgParser; + + $args = func_get_args(); + array_shift( $args ); + array_shift( $args ); + + if( !is_array($options) ) { + $options = array($options); + } + + $string = wfMsgGetKey( $key, true, false, false ); + + if( !in_array('replaceafter', $options) ) { + $string = wfMsgReplaceArgs( $string, $args ); + } + + if( in_array('parse', $options) ) { + $string = $wgOut->parse( $string, true, true ); + } elseif ( in_array('parseinline', $options) ) { + $string = $wgOut->parse( $string, true, true ); + $m = array(); + if( preg_match( "~^

    (.*)\n?

    $~", $string, $m ) ) { + $string = $m[1]; + } + } elseif ( in_array('parsemag', $options) ) { + global $wgTitle; + $parser = new Parser(); + $parserOptions = new ParserOptions(); + $parserOptions->setInterfaceMessage( true ); + $parser->startExternalParse( $wgTitle, $parserOptions, OT_MSG ); + $string = $parser->transformMsg( $string, $parserOptions ); + } + + if ( in_array('escape', $options) ) { + $string = htmlspecialchars ( $string ); + } + + if( in_array('replaceafter', $options) ) { + $string = wfMsgReplaceArgs( $string, $args ); + } + + return $string; +} + + +/** + * Just like exit() but makes a note of it. + * Commits open transactions except if the error parameter is set + * + * @obsolete Please return control to the caller or throw an exception + */ +function wfAbruptExit( $error = false ){ + global $wgLoadBalancer; + static $called = false; + if ( $called ){ + exit( -1 ); + } + $called = true; + + if( function_exists( 'debug_backtrace' ) ){ // PHP >= 4.3 + $bt = debug_backtrace(); + for($i = 0; $i < count($bt) ; $i++){ + $file = isset($bt[$i]['file']) ? $bt[$i]['file'] : "unknown"; + $line = isset($bt[$i]['line']) ? $bt[$i]['line'] : "unknown"; + wfDebug("WARNING: Abrupt exit in $file at line $line\n"); + } + } else { + wfDebug('WARNING: Abrupt exit\n'); + } + + wfProfileClose(); + logProfilingData(); + + if ( !$error ) { + $wgLoadBalancer->closeAll(); + } + exit( -1 ); +} + +/** + * @obsolete Please return control the caller or throw an exception + */ +function wfErrorExit() { + wfAbruptExit( true ); +} + +/** + * Print a simple message and die, returning nonzero to the shell if any. + * Plain die() fails to return nonzero to the shell if you pass a string. + * @param string $msg + */ +function wfDie( $msg='' ) { + echo $msg; + die( 1 ); +} + +/** + * Throw a debugging exception. This function previously once exited the process, + * but now throws an exception instead, with similar results. + * + * @param string $msg Message shown when dieing. + */ +function wfDebugDieBacktrace( $msg = '' ) { + throw new MWException( $msg ); +} + +/** + * Fetch server name for use in error reporting etc. + * Use real server name if available, so we know which machine + * in a server farm generated the current page. + * @return string + */ +function wfHostname() { + if ( function_exists( 'posix_uname' ) ) { + // This function not present on Windows + $uname = @posix_uname(); + } else { + $uname = false; + } + if( is_array( $uname ) && isset( $uname['nodename'] ) ) { + return $uname['nodename']; + } else { + # This may be a virtual server. + return $_SERVER['SERVER_NAME']; + } +} + + /** + * Returns a HTML comment with the elapsed time since request. + * This method has no side effects. + * @return string + */ + function wfReportTime() { + global $wgRequestTime; + + $now = wfTime(); + $elapsed = $now - $wgRequestTime; + + $com = sprintf( "", + wfHostname(), $elapsed ); + return $com; + } + +function wfBacktrace() { + global $wgCommandLineMode; + if ( !function_exists( 'debug_backtrace' ) ) { + return false; + } + + if ( $wgCommandLineMode ) { + $msg = ''; + } else { + $msg = "
      \n"; + } + $backtrace = debug_backtrace(); + foreach( $backtrace as $call ) { + if( isset( $call['file'] ) ) { + $f = explode( DIRECTORY_SEPARATOR, $call['file'] ); + $file = $f[count($f)-1]; + } else { + $file = '-'; + } + if( isset( $call['line'] ) ) { + $line = $call['line']; + } else { + $line = '-'; + } + if ( $wgCommandLineMode ) { + $msg .= "$file line $line calls "; + } else { + $msg .= '
    • ' . $file . ' line ' . $line . ' calls '; + } + if( !empty( $call['class'] ) ) $msg .= $call['class'] . '::'; + $msg .= $call['function'] . '()'; + + if ( $wgCommandLineMode ) { + $msg .= "\n"; + } else { + $msg .= "
    • \n"; + } + } + if ( $wgCommandLineMode ) { + $msg .= "\n"; + } else { + $msg .= "
    \n"; + } + + return $msg; +} + + +/* Some generic result counters, pulled out of SearchEngine */ + + +/** + * @todo document + */ +function wfShowingResults( $offset, $limit ) { + global $wgLang; + return wfMsg( 'showingresults', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); +} + +/** + * @todo document + */ +function wfShowingResultsNum( $offset, $limit, $num ) { + global $wgLang; + return wfMsg( 'showingresultsnum', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); +} + +/** + * @todo document + */ +function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { + global $wgLang; + $fmtLimit = $wgLang->formatNum( $limit ); + $prev = wfMsg( 'prevn', $fmtLimit ); + $next = wfMsg( 'nextn', $fmtLimit ); + + if( is_object( $link ) ) { + $title =& $link; + } else { + $title = Title::newFromText( $link ); + if( is_null( $title ) ) { + return false; + } + } + + if ( 0 != $offset ) { + $po = $offset - $limit; + if ( $po < 0 ) { $po = 0; } + $q = "limit={$limit}&offset={$po}"; + if ( '' != $query ) { $q .= '&'.$query; } + $plink = '{$prev}"; + } else { $plink = $prev; } + + $no = $offset + $limit; + $q = 'limit='.$limit.'&offset='.$no; + if ( '' != $query ) { $q .= '&'.$query; } + + if ( $atend ) { + $nlink = $next; + } else { + $nlink = '{$next}"; + } + $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' . + wfNumLink( $offset, 50, $title, $query ) . ' | ' . + wfNumLink( $offset, 100, $title, $query ) . ' | ' . + wfNumLink( $offset, 250, $title, $query ) . ' | ' . + wfNumLink( $offset, 500, $title, $query ); + + return wfMsg( 'viewprevnext', $plink, $nlink, $nums ); +} + +/** + * @todo document + */ +function wfNumLink( $offset, $limit, &$title, $query = '' ) { + global $wgLang; + if ( '' == $query ) { $q = ''; } + else { $q = $query.'&'; } + $q .= 'limit='.$limit.'&offset='.$offset; + + $fmtLimit = $wgLang->formatNum( $limit ); + $s = '{$fmtLimit}"; + return $s; +} + +/** + * @todo document + * @todo FIXME: we may want to blacklist some broken browsers + * + * @return bool Whereas client accept gzip compression + */ +function wfClientAcceptsGzip() { + global $wgUseGzip; + if( $wgUseGzip ) { + # FIXME: we may want to blacklist some broken browsers + if( preg_match( + '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', + $_SERVER['HTTP_ACCEPT_ENCODING'], + $m ) ) { + if( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) return false; + wfDebug( " accepts gzip\n" ); + return true; + } + } + return false; +} + +/** + * Obtain the offset and limit values from the request string; + * used in special pages + * + * @param $deflimit Default limit if none supplied + * @param $optionname Name of a user preference to check against + * @return array + * + */ +function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { + global $wgRequest; + return $wgRequest->getLimitOffset( $deflimit, $optionname ); +} + +/** + * Escapes the given text so that it may be output using addWikiText() + * without any linking, formatting, etc. making its way through. This + * is achieved by substituting certain characters with HTML entities. + * As required by the callers, is not used. It currently does + * not filter out characters which have special meaning only at the + * start of a line, such as "*". + * + * @param string $text Text to be escaped + */ +function wfEscapeWikiText( $text ) { + $text = str_replace( + array( '[', '|', '\'', 'ISBN ' , '://' , "\n=", '{{' ), + array( '[', '|', ''', 'ISBN ', '://' , "\n=", '{{' ), + htmlspecialchars($text) ); + return $text; +} + +/** + * @todo document + */ +function wfQuotedPrintable( $string, $charset = '' ) { + # Probably incomplete; see RFC 2045 + if( empty( $charset ) ) { + global $wgInputEncoding; + $charset = $wgInputEncoding; + } + $charset = strtoupper( $charset ); + $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ? + + $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff='; + $replace = $illegal . '\t ?_'; + if( !preg_match( "/[$illegal]/", $string ) ) return $string; + $out = "=?$charset?Q?"; + $out .= preg_replace( "/([$replace])/e", 'sprintf("=%02X",ord("$1"))', $string ); + $out .= '?='; + return $out; +} + + +/** + * @todo document + * @return float + */ +function wfTime() { + return microtime(true); +} + +/** + * Sets dest to source and returns the original value of dest + * If source is NULL, it just returns the value, it doesn't set the variable + */ +function wfSetVar( &$dest, $source ) { + $temp = $dest; + if ( !is_null( $source ) ) { + $dest = $source; + } + return $temp; +} + +/** + * As for wfSetVar except setting a bit + */ +function wfSetBit( &$dest, $bit, $state = true ) { + $temp = (bool)($dest & $bit ); + if ( !is_null( $state ) ) { + if ( $state ) { + $dest |= $bit; + } else { + $dest &= ~$bit; + } + } + return $temp; +} + +/** + * This function takes two arrays as input, and returns a CGI-style string, e.g. + * "days=7&limit=100". Options in the first array override options in the second. + * Options set to "" will not be output. + */ +function wfArrayToCGI( $array1, $array2 = NULL ) +{ + if ( !is_null( $array2 ) ) { + $array1 = $array1 + $array2; + } + + $cgi = ''; + foreach ( $array1 as $key => $value ) { + if ( '' !== $value ) { + if ( '' != $cgi ) { + $cgi .= '&'; + } + $cgi .= urlencode( $key ) . '=' . urlencode( $value ); + } + } + return $cgi; +} + +/** + * This is obsolete, use SquidUpdate::purge() + * @deprecated + */ +function wfPurgeSquidServers ($urlArr) { + SquidUpdate::purge( $urlArr ); +} + +/** + * Windows-compatible version of escapeshellarg() + * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() + * function puts single quotes in regardless of OS + */ +function wfEscapeShellArg( ) { + $args = func_get_args(); + $first = true; + $retVal = ''; + foreach ( $args as $arg ) { + if ( !$first ) { + $retVal .= ' '; + } else { + $first = false; + } + + if ( wfIsWindows() ) { + // Escaping for an MSVC-style command line parser + // Ref: http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // Double the backslashes before any double quotes. Escape the double quotes. + $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); + $arg = ''; + $delim = false; + foreach ( $tokens as $token ) { + if ( $delim ) { + $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; + } else { + $arg .= $token; + } + $delim = !$delim; + } + // Double the backslashes before the end of the string, because + // we will soon add a quote + if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { + $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); + } + + // Add surrounding quotes + $retVal .= '"' . $arg . '"'; + } else { + $retVal .= escapeshellarg( $arg ); + } + } + return $retVal; +} + +/** + * wfMerge attempts to merge differences between three texts. + * Returns true for a clean merge and false for failure or a conflict. + */ +function wfMerge( $old, $mine, $yours, &$result ){ + global $wgDiff3; + + # This check may also protect against code injection in + # case of broken installations. + if(! file_exists( $wgDiff3 ) ){ + wfDebug( "diff3 not found\n" ); + return false; + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); + $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + fwrite( $oldtextFile, $old ); fclose( $oldtextFile ); + fwrite( $mytextFile, $mine ); fclose( $mytextFile ); + fwrite( $yourtextFile, $yours ); fclose( $yourtextFile ); + + # Check for a conflict + $cmd = $wgDiff3 . ' -a --overlap-only ' . + wfEscapeShellArg( $mytextName ) . ' ' . + wfEscapeShellArg( $oldtextName ) . ' ' . + wfEscapeShellArg( $yourtextName ); + $handle = popen( $cmd, 'r' ); + + if( fgets( $handle, 1024 ) ){ + $conflict = true; + } else { + $conflict = false; + } + pclose( $handle ); + + # Merge differences + $cmd = $wgDiff3 . ' -a -e --merge ' . + wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName ); + $handle = popen( $cmd, 'r' ); + $result = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $result .= $data; + } while ( true ); + pclose( $handle ); + unlink( $mytextName ); unlink( $oldtextName ); unlink( $yourtextName ); + + if ( $result === '' && $old !== '' && $conflict == false ) { + wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); + $conflict = true; + } + return ! $conflict; +} + +/** + * @todo document + */ +function wfVarDump( $var ) { + global $wgOut; + $s = str_replace("\n","
    \n", var_export( $var, true ) . "\n"); + if ( headers_sent() || !@is_object( $wgOut ) ) { + print $s; + } else { + $wgOut->addHTML( $s ); + } +} + +/** + * Provide a simple HTTP error. + */ +function wfHttpError( $code, $label, $desc ) { + global $wgOut; + $wgOut->disable(); + header( "HTTP/1.0 $code $label" ); + header( "Status: $code $label" ); + $wgOut->sendCacheControl(); + + header( 'Content-type: text/html' ); + print "" . + htmlspecialchars( $label ) . + "

    " . + htmlspecialchars( $label ) . + "

    " . + htmlspecialchars( $desc ) . + "

    \n"; +} + +/** + * Converts an Accept-* header into an array mapping string values to quality + * factors + */ +function wfAcceptToPrefs( $accept, $def = '*/*' ) { + # No arg means accept anything (per HTTP spec) + if( !$accept ) { + return array( $def => 1 ); + } + + $prefs = array(); + + $parts = explode( ',', $accept ); + + foreach( $parts as $part ) { + # FIXME: doesn't deal with params like 'text/html; level=1' + @list( $value, $qpart ) = explode( ';', $part ); + if( !isset( $qpart ) ) { + $prefs[$value] = 1; + } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) { + $prefs[$value] = $match[1]; + } + } + + return $prefs; +} + +/** + * Checks if a given MIME type matches any of the keys in the given + * array. Basic wildcards are accepted in the array keys. + * + * Returns the matching MIME type (or wildcard) if a match, otherwise + * NULL if no match. + * + * @param string $type + * @param array $avail + * @return string + * @private + */ +function mimeTypeMatch( $type, $avail ) { + if( array_key_exists($type, $avail) ) { + return $type; + } else { + $parts = explode( '/', $type ); + if( array_key_exists( $parts[0] . '/*', $avail ) ) { + return $parts[0] . '/*'; + } elseif( array_key_exists( '*/*', $avail ) ) { + return '*/*'; + } else { + return NULL; + } + } +} + +/** + * Returns the 'best' match between a client's requested internet media types + * and the server's list of available types. Each list should be an associative + * array of type to preference (preference is a float between 0.0 and 1.0). + * Wildcards in the types are acceptable. + * + * @param array $cprefs Client's acceptable type list + * @param array $sprefs Server's offered types + * @return string + * + * @todo FIXME: doesn't handle params like 'text/plain; charset=UTF-8' + * XXX: generalize to negotiate other stuff + */ +function wfNegotiateType( $cprefs, $sprefs ) { + $combine = array(); + + foreach( array_keys($sprefs) as $type ) { + $parts = explode( '/', $type ); + if( $parts[1] != '*' ) { + $ckey = mimeTypeMatch( $type, $cprefs ); + if( $ckey ) { + $combine[$type] = $sprefs[$type] * $cprefs[$ckey]; + } + } + } + + foreach( array_keys( $cprefs ) as $type ) { + $parts = explode( '/', $type ); + if( $parts[1] != '*' && !array_key_exists( $type, $sprefs ) ) { + $skey = mimeTypeMatch( $type, $sprefs ); + if( $skey ) { + $combine[$type] = $sprefs[$skey] * $cprefs[$type]; + } + } + } + + $bestq = 0; + $besttype = NULL; + + foreach( array_keys( $combine ) as $type ) { + if( $combine[$type] > $bestq ) { + $besttype = $type; + $bestq = $combine[$type]; + } + } + + return $besttype; +} + +/** + * Array lookup + * Returns an array where the values in the first array are replaced by the + * values in the second array with the corresponding keys + * + * @return array + */ +function wfArrayLookup( $a, $b ) { + return array_flip( array_intersect( array_flip( $a ), array_keys( $b ) ) ); +} + +/** + * Convenience function; returns MediaWiki timestamp for the present time. + * @return string + */ +function wfTimestampNow() { + # return NOW + return wfTimestamp( TS_MW, time() ); +} + +/** + * Reference-counted warning suppression + */ +function wfSuppressWarnings( $end = false ) { + static $suppressCount = 0; + static $originalLevel = false; + + if ( $end ) { + if ( $suppressCount ) { + --$suppressCount; + if ( !$suppressCount ) { + error_reporting( $originalLevel ); + } + } + } else { + if ( !$suppressCount ) { + $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE ) ); + } + ++$suppressCount; + } +} + +/** + * Restore error level to previous value + */ +function wfRestoreWarnings() { + wfSuppressWarnings( true ); +} + +# Autodetect, convert and provide timestamps of various types + +/** + * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC + */ +define('TS_UNIX', 0); + +/** + * MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS) + */ +define('TS_MW', 1); + +/** + * MySQL DATETIME (YYYY-MM-DD HH:MM:SS) + */ +define('TS_DB', 2); + +/** + * RFC 2822 format, for E-mail and HTTP headers + */ +define('TS_RFC2822', 3); + +/** + * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z + * + * This is used by Special:Export + */ +define('TS_ISO_8601', 4); + +/** + * An Exif timestamp (YYYY:MM:DD HH:MM:SS) + * + * @url http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the + * DateTime tag and page 36 for the DateTimeOriginal and + * DateTimeDigitized tags. + */ +define('TS_EXIF', 5); + +/** + * Oracle format time. + */ +define('TS_ORACLE', 6); + +/** + * @param mixed $outputtype A timestamp in one of the supported formats, the + * function will autodetect which format is supplied + * and act accordingly. + * @return string Time in the format specified in $outputtype + */ +function wfTimestamp($outputtype=TS_UNIX,$ts=0) { + $uts = 0; + $da = array(); + if ($ts==0) { + $uts=time(); + } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) { + # TS_DB + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match("/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) { + # TS_EXIF + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match("/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D",$ts,$da)) { + # TS_MW + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match("/^(\d{1,13})$/D",$ts,$datearray)) { + # TS_UNIX + $uts = $ts; + } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) { + # TS_ORACLE + $uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", + str_replace("+00:00", "UTC", $ts))); + } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) { + # TS_ISO_8601 + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } else { + # Bogus value; fall back to the epoch... + wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); + $uts = 0; + } + + + switch($outputtype) { + case TS_UNIX: + return $uts; + case TS_MW: + return gmdate( 'YmdHis', $uts ); + case TS_DB: + return gmdate( 'Y-m-d H:i:s', $uts ); + case TS_ISO_8601: + return gmdate( 'Y-m-d\TH:i:s\Z', $uts ); + // This shouldn't ever be used, but is included for completeness + case TS_EXIF: + return gmdate( 'Y:m:d H:i:s', $uts ); + case TS_RFC2822: + return gmdate( 'D, d M Y H:i:s', $uts ) . ' GMT'; + case TS_ORACLE: + return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; + default: + throw new MWException( 'wfTimestamp() called with illegal output type.'); + } +} + +/** + * Return a formatted timestamp, or null if input is null. + * For dealing with nullable timestamp columns in the database. + * @param int $outputtype + * @param string $ts + * @return string + */ +function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { + if( is_null( $ts ) ) { + return null; + } else { + return wfTimestamp( $outputtype, $ts ); + } +} + +/** + * Check if the operating system is Windows + * + * @return bool True if it's Windows, False otherwise. + */ +function wfIsWindows() { + if (substr(php_uname(), 0, 7) == 'Windows') { + return true; + } else { + return false; + } +} + +/** + * Swap two variables + */ +function swap( &$x, &$y ) { + $z = $x; + $x = $y; + $y = $z; +} + +function wfGetCachedNotice( $name ) { + global $wgOut, $parserMemc, $wgDBname; + $fname = 'wfGetCachedNotice'; + wfProfileIn( $fname ); + + $needParse = false; + $notice = wfMsgForContent( $name ); + if( $notice == '<'. $name . ';>' || $notice == '-' ) { + wfProfileOut( $fname ); + return( false ); + } + + $cachedNotice = $parserMemc->get( $wgDBname . ':' . $name ); + if( is_array( $cachedNotice ) ) { + if( md5( $notice ) == $cachedNotice['hash'] ) { + $notice = $cachedNotice['html']; + } else { + $needParse = true; + } + } else { + $needParse = true; + } + + if( $needParse ) { + if( is_object( $wgOut ) ) { + $parsed = $wgOut->parse( $notice ); + $parserMemc->set( $wgDBname . ':' . $name, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); + $notice = $parsed; + } else { + wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); + $notice = ''; + } + } + + wfProfileOut( $fname ); + return $notice; +} + +function wfGetNamespaceNotice() { + global $wgTitle; + + # Paranoia + if ( !isset( $wgTitle ) || !is_object( $wgTitle ) ) + return ""; + + $fname = 'wfGetNamespaceNotice'; + wfProfileIn( $fname ); + + $key = "namespacenotice-" . $wgTitle->getNsText(); + $namespaceNotice = wfGetCachedNotice( $key ); + if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "

    <" ) { + $namespaceNotice = '

    ' . $namespaceNotice . "
    "; + } else { + $namespaceNotice = ""; + } + + wfProfileOut( $fname ); + return $namespaceNotice; +} + +function wfGetSiteNotice() { + global $wgUser, $wgSiteNotice; + $fname = 'wfGetSiteNotice'; + wfProfileIn( $fname ); + $siteNotice = ''; + + if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) { + if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) { + $siteNotice = wfGetCachedNotice( 'sitenotice' ); + $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice; + } else { + $anonNotice = wfGetCachedNotice( 'anonnotice' ); + if( !$anonNotice ) { + $siteNotice = wfGetCachedNotice( 'sitenotice' ); + $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice; + } else { + $siteNotice = $anonNotice; + } + } + } + + wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice ) ); + wfProfileOut( $fname ); + return $siteNotice; +} + +/** Global singleton instance of MimeMagic. This is initialized on demand, +* please always use the wfGetMimeMagic() function to get the instance. +* +* @private +*/ +$wgMimeMagic= NULL; + +/** Factory functions for the global MimeMagic object. +* This function always returns the same singleton instance of MimeMagic. +* That objects will be instantiated on the first call to this function. +* If needed, the MimeMagic.php file is automatically included by this function. +* @return MimeMagic the global MimeMagic objects. +*/ +function &wfGetMimeMagic() { + global $wgMimeMagic; + + if (!is_null($wgMimeMagic)) { + return $wgMimeMagic; + } + + if (!class_exists("MimeMagic")) { + #include on demand + require_once("MimeMagic.php"); + } + + $wgMimeMagic= new MimeMagic(); + + return $wgMimeMagic; +} + + +/** + * Tries to get the system directory for temporary files. + * The TMPDIR, TMP, and TEMP environment variables are checked in sequence, + * and if none are set /tmp is returned as the generic Unix default. + * + * NOTE: When possible, use the tempfile() function to create temporary + * files to avoid race conditions on file creation, etc. + * + * @return string + */ +function wfTempDir() { + foreach( array( 'TMPDIR', 'TMP', 'TEMP' ) as $var ) { + $tmp = getenv( $var ); + if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) { + return $tmp; + } + } + # Hope this is Unix of some kind! + return '/tmp'; +} + +/** + * Make directory, and make all parent directories if they don't exist + */ +function wfMkdirParents( $fullDir, $mode = 0777 ) { + if ( strval( $fullDir ) === '' ) { + return true; + } + + # Go back through the paths to find the first directory that exists + $currentDir = $fullDir; + $createList = array(); + while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) { + # Strip trailing slashes + $currentDir = rtrim( $currentDir, '/\\' ); + + # Add to create list + $createList[] = $currentDir; + + # Find next delimiter searching from the end + $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) ); + if ( $p === false ) { + $currentDir = false; + } else { + $currentDir = substr( $currentDir, 0, $p ); + } + } + + if ( count( $createList ) == 0 ) { + # Directory specified already exists + return true; + } elseif ( $currentDir === false ) { + # Went all the way back to root and it apparently doesn't exist + return false; + } + + # Now go forward creating directories + $createList = array_reverse( $createList ); + foreach ( $createList as $dir ) { + # use chmod to override the umask, as suggested by the PHP manual + if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) { + return false; + } + } + return true; +} + +/** + * Increment a statistics counter + */ + function wfIncrStats( $key ) { + global $wgDBname, $wgMemc; + $key = "$wgDBname:stats:$key"; + if ( is_null( $wgMemc->incr( $key ) ) ) { + $wgMemc->add( $key, 1 ); + } + } + +/** + * @param mixed $nr The number to format + * @param int $acc The number of digits after the decimal point, default 2 + * @param bool $round Whether or not to round the value, default true + * @return float + */ +function wfPercent( $nr, $acc = 2, $round = true ) { + $ret = sprintf( "%.${acc}f", $nr ); + return $round ? round( $ret, $acc ) . '%' : "$ret%"; +} + +/** + * Encrypt a username/password. + * + * @param string $userid ID of the user + * @param string $password Password of the user + * @return string Hashed password + */ +function wfEncryptPassword( $userid, $password ) { + global $wgPasswordSalt; + $p = md5( $password); + + if($wgPasswordSalt) + return md5( "{$userid}-{$p}" ); + else + return $p; +} + +/** + * Appends to second array if $value differs from that in $default + */ +function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { + if ( is_null( $changed ) ) { + throw new MWException('GlobalFunctions::wfAppendToArrayIfNotDefault got null'); + } + if ( $default[$key] !== $value ) { + $changed[$key] = $value; + } +} + +/** + * Since wfMsg() and co suck, they don't return false if the message key they + * looked up didn't exist but a XHTML string, this function checks for the + * nonexistance of messages by looking at wfMsg() output + * + * @param $msg The message key looked up + * @param $wfMsgOut The output of wfMsg*() + * @return bool + */ +function wfEmptyMsg( $msg, $wfMsgOut ) { + return $wfMsgOut === "<$msg>"; +} + +/** + * Find out whether or not a mixed variable exists in a string + * + * @param mixed needle + * @param string haystack + * @return bool + */ +function in_string( $needle, $str ) { + return strpos( $str, $needle ) !== false; +} + +function wfSpecialList( $page, $details ) { + global $wgContLang; + $details = $details ? ' ' . $wgContLang->getDirMark() . "($details)" : ""; + return $page . $details; +} + +/** + * Returns a regular expression of url protocols + * + * @return string + */ +function wfUrlProtocols() { + global $wgUrlProtocols; + + // Support old-style $wgUrlProtocols strings, for backwards compatibility + // with LocalSettings files from 1.5 + if ( is_array( $wgUrlProtocols ) ) { + $protocols = array(); + foreach ($wgUrlProtocols as $protocol) + $protocols[] = preg_quote( $protocol, '/' ); + + return implode( '|', $protocols ); + } else { + return $wgUrlProtocols; + } +} + +/** + * Execute a shell command, with time and memory limits mirrored from the PHP + * configuration if supported. + * @param $cmd Command line, properly escaped for shell. + * @param &$retval optional, will receive the program's exit code. + * (non-zero is usually failure) + * @return collected stdout as a string (trailing newlines stripped) + */ +function wfShellExec( $cmd, &$retval=null ) { + global $IP, $wgMaxShellMemory; + + if( ini_get( '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."; + } + + if ( php_uname( 's' ) == 'Linux' ) { + $time = ini_get( 'max_execution_time' ); + $mem = intval( $wgMaxShellMemory ); + + if ( $time > 0 && $mem > 0 ) { + $script = "$IP/bin/ulimit.sh"; + if ( is_executable( $script ) ) { + $cmd = escapeshellarg( $script ) . " $time $mem $cmd"; + } + } + } elseif ( php_uname( 's' ) == 'Windows NT' ) { + # This is a hack to work around PHP's flawed invocation of cmd.exe + # http://news.php.net/php.internals/21796 + $cmd = '"' . $cmd . '"'; + } + wfDebug( "wfShellExec: $cmd\n" ); + + $output = array(); + $retval = 1; // error by default? + $lastline = exec( $cmd, $output, $retval ); + return implode( "\n", $output ); + +} + +/** + * This function works like "use VERSION" in Perl, the program will die with a + * backtrace if the current version of PHP is less than the version provided + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases, and might depend on other versions of PHP than the main code + * + * Note: PHP might die due to parsing errors in some cases before it ever + * manages to call this function, such is life + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUsePHP( $req_ver ) { + $php_ver = PHP_VERSION; + + if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) + throw new MWException( "PHP $req_ver required--this is only $php_ver" ); +} + +/** + * This function works like "use VERSION" in Perl except it checks the version + * of MediaWiki, the program will die with a backtrace if the current version + * of MediaWiki is less than the version provided. + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUseMW( $req_ver ) { + global $wgVersion; + + if ( version_compare( $wgVersion, (string)$req_ver, '<' ) ) + throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" ); +} + +/** + * Escape a string to make it suitable for inclusion in a preg_replace() + * replacement parameter. + * + * @param string $string + * @return string + */ +function wfRegexReplacement( $string ) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '$', '\\$', $string ); + return $string; +} + +/** + * Return the final portion of a pathname. + * Reimplemented because PHP5's basename() is buggy with multibyte text. + * http://bugs.php.net/bug.php?id=33898 + * + * PHP's basename() only considers '\' a pathchar on Windows and Netware. + * We'll consider it so always, as we don't want \s in our Unix paths either. + * + * @param string $path + * @return string + */ +function wfBaseName( $path ) { + if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) { + return $matches[1]; + } else { + return ''; + } +} + +/** + * Make a URL index, appropriate for the el_index field of externallinks. + */ +function wfMakeUrlIndex( $url ) { + wfSuppressWarnings(); + $bits = parse_url( $url ); + wfRestoreWarnings(); + if ( !$bits || $bits['scheme'] !== 'http' ) { + return false; + } + // Reverse the labels in the hostname, convert to lower case + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + // Add an extra dot to the end + if ( substr( $reversedHost, -1, 1 ) !== '.' ) { + $reversedHost .= '.'; + } + // Reconstruct the pseudo-URL + $index = "http://$reversedHost"; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) $index .= '?' . $bits['query']; + if ( isset( $bits['fragment'] ) ) $index .= '#' . $bits['fragment']; + return $index; +} + +/** + * Do any deferred updates and clear the list + * TODO: This could be in Wiki.php if that class made any sense at all + */ +function wfDoUpdates() +{ + global $wgPostCommitUpdateList, $wgDeferredUpdateList; + foreach ( $wgDeferredUpdateList as $update ) { + $update->doUpdate(); + } + foreach ( $wgPostCommitUpdateList as $update ) { + $update->doUpdate(); + } + $wgDeferredUpdateList = array(); + $wgPostCommitUpdateList = array(); +} + +/** + * More or less "markup-safe" explode() + * Ignores any instances of the separator inside <...> + * @param string $separator + * @param string $text + * @return array + */ +function wfExplodeMarkup( $separator, $text ) { + $placeholder = "\x00"; + + // Just in case... + $text = str_replace( $placeholder, '', $text ); + + // Trim stuff + $replacer = new ReplacerCallback( $separator, $placeholder ); + $cleaned = preg_replace_callback( '/(<.*?>)/', array( $replacer, 'go' ), $text ); + + $items = explode( $separator, $cleaned ); + foreach( $items as $i => $str ) { + $items[$i] = str_replace( $placeholder, $separator, $str ); + } + + return $items; +} + +class ReplacerCallback { + function ReplacerCallback( $from, $to ) { + $this->from = $from; + $this->to = $to; + } + + function go( $matches ) { + return str_replace( $this->from, $this->to, $matches[1] ); + } +} + + +/** + * Convert an arbitrarily-long digit string from one numeric base + * to another, optionally zero-padding to a minimum column width. + * + * Supports base 2 through 36; digit values 10-36 are represented + * as lowercase letters a-z. Input is case-insensitive. + * + * @param $input string of digits + * @param $sourceBase int 2-36 + * @param $destBase int 2-36 + * @param $pad int 1 or greater + * @return string or false on invalid input + */ +function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1 ) { + if( $sourceBase < 2 || + $sourceBase > 36 || + $destBase < 2 || + $destBase > 36 || + $pad < 1 || + $sourceBase != intval( $sourceBase ) || + $destBase != intval( $destBase ) || + $pad != intval( $pad ) || + !is_string( $input ) || + $input == '' ) { + return false; + } + + $digitChars = '0123456789abcdefghijklmnopqrstuvwxyz'; + $inDigits = array(); + $outChars = ''; + + // Decode and validate input string + $input = strtolower( $input ); + for( $i = 0; $i < strlen( $input ); $i++ ) { + $n = strpos( $digitChars, $input{$i} ); + if( $n === false || $n > $sourceBase ) { + return false; + } + $inDigits[] = $n; + } + + // Iterate over the input, modulo-ing out an output digit + // at a time until input is gone. + while( count( $inDigits ) ) { + $work = 0; + $workDigits = array(); + + // Long division... + foreach( $inDigits as $digit ) { + $work *= $sourceBase; + $work += $digit; + + if( $work < $destBase ) { + // Gonna need to pull another digit. + if( count( $workDigits ) ) { + // Avoid zero-padding; this lets us find + // the end of the input very easily when + // length drops to zero. + $workDigits[] = 0; + } + } else { + // Finally! Actual division! + $workDigits[] = intval( $work / $destBase ); + + // Isn't it annoying that most programming languages + // don't have a single divide-and-remainder operator, + // even though the CPU implements it that way? + $work = $work % $destBase; + } + } + + // All that division leaves us with a remainder, + // which is conveniently our next output digit. + $outChars .= $digitChars[$work]; + + // And we continue! + $inDigits = $workDigits; + } + + while( strlen( $outChars ) < $pad ) { + $outChars .= '0'; + } + + return strrev( $outChars ); +} + +/** + * Create an object with a given name and an array of construct parameters + * @param string $name + * @param array $p parameters + */ +function wfCreateObject( $name, $p ){ + $p = array_values( $p ); + switch ( count( $p ) ) { + case 0: + return new $name; + case 1: + return new $name( $p[0] ); + case 2: + return new $name( $p[0], $p[1] ); + case 3: + return new $name( $p[0], $p[1], $p[2] ); + case 4: + return new $name( $p[0], $p[1], $p[2], $p[3] ); + case 5: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4] ); + case 6: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4], $p[5] ); + default: + throw new MWException( "Too many arguments to construtor in wfCreateObject" ); + } +} + +/** + * Aliases for modularized functions + */ +function wfGetHTTP( $url, $timeout = 'default' ) { + return Http::get( $url, $timeout ); +} +function wfIsLocalURL( $url ) { + return Http::isLocalURL( $url ); +} + +?> diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php new file mode 100644 index 00000000..47703b20 --- /dev/null +++ b/includes/HTMLCacheUpdate.php @@ -0,0 +1,230 @@ +mTitle = $titleTo; + $this->mTable = $table; + $this->mRowsPerJob = $wgUpdateRowsPerJob; + $this->mRowsPerQuery = $wgUpdateRowsPerQuery; + } + + function doUpdate() { + # Fetch the IDs + $cond = $this->getToCondition(); + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); + $resWrap = new ResultWrapper( $dbr, $res ); + if ( $dbr->numRows( $res ) != 0 ) { + if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) { + $this->insertJobs( $resWrap ); + } else { + $this->invalidateIDs( $resWrap ); + } + } + $dbr->freeResult( $res ); + } + + function insertJobs( ResultWrapper $res ) { + $numRows = $res->numRows(); + $numBatches = ceil( $numRows / $this->mRowsPerJob ); + $realBatchSize = $numRows / $numBatches; + $boundaries = array(); + $start = false; + $jobs = array(); + do { + for ( $i = 0; $i < $realBatchSize - 1; $i++ ) { + $row = $res->fetchRow(); + if ( $row ) { + $id = $row[0]; + } else { + $id = false; + break; + } + } + if ( $id !== false ) { + // One less on the end to avoid duplicating the boundary + $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, $id - 1 ); + } else { + $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, false ); + } + $jobs[] = $job; + + $start = $id; + } while ( $start ); + + Job::batchInsert( $jobs ); + } + + function getPrefix() { + static $prefixes = array( + 'pagelinks' => 'pl', + 'imagelinks' => 'il', + 'categorylinks' => 'cl', + 'templatelinks' => 'tl', + + # Not needed + # 'externallinks' => 'el', + # 'langlinks' => 'll' + ); + + if ( is_null( $this->mPrefix ) ) { + $this->mPrefix = $prefixes[$this->mTable]; + if ( is_null( $this->mPrefix ) ) { + throw new MWException( "Invalid table type \"{$this->mTable}\" in " . __CLASS__ ); + } + } + return $this->mPrefix; + } + + function getFromField() { + return $this->getPrefix() . '_from'; + } + + function getToCondition() { + 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 'imagelinks': + return array( 'il_to' => $this->mTitle->getDBkey() ); + case 'categorylinks': + return array( 'cl_to' => $this->mTitle->getDBkey() ); + } + throw new MWException( 'Invalid table type in ' . __CLASS__ ); + } + + /** + * Invalidate a set of IDs, right now + */ + function invalidateIDs( ResultWrapper $res ) { + global $wgUseFileCache, $wgUseSquid; + + if ( $res->numRows() == 0 ) { + return; + } + + $dbw =& wfGetDB( DB_MASTER ); + $timestamp = $dbw->timestamp(); + $done = false; + + while ( !$done ) { + # Get all IDs in this query into an array + $ids = array(); + for ( $i = 0; $i < $this->mRowsPerQuery; $i++ ) { + $row = $res->fetchRow(); + if ( $row ) { + $ids[] = $row[0]; + } else { + $done = true; + break; + } + } + + if ( !count( $ids ) ) { + break; + } + + # Update page_touched + $dbw->update( 'page', + array( 'page_touched' => $timestamp ), + array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), + __METHOD__ + ); + + # Update squid + if ( $wgUseSquid || $wgUseFileCache ) { + $titles = Title::newFromIDs( $ids ); + if ( $wgUseSquid ) { + $u = SquidUpdate::newFromTitles( $titles ); + $u->doUpdate(); + } + + # Update file cache + if ( $wgUseFileCache ) { + foreach ( $titles as $title ) { + $cm = new CacheManager($title); + @unlink($cm->fileCacheName()); + } + } + } + } + } +} + +class HTMLCacheUpdateJob extends Job { + var $table, $start, $end; + + /** + * Construct a job + * @param Title $title The title linked to + * @param string $table The name of the link table. + * @param integer $start Beginning page_id or false for open interval + * @param integer $end End page_id or false for open interval + * @param integer $id job_id + */ + function __construct( $title, $table, $start, $end, $id = 0 ) { + $params = array( + 'table' => $table, + 'start' => $start, + 'end' => $end ); + parent::__construct( 'htmlCacheUpdate', $title, $params, $id ); + $this->table = $table; + $this->start = intval( $start ); + $this->end = intval( $end ); + } + + function run() { + $update = new HTMLCacheUpdate( $this->title, $this->table ); + + $fromField = $update->getFromField(); + $conds = $update->getToCondition(); + if ( $this->start ) { + $conds[] = "$fromField >= {$this->start}"; + } + if ( $this->end ) { + $conds[] = "$fromField <= {$this->end}"; + } + + $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/HTMLForm.php b/includes/HTMLForm.php new file mode 100644 index 00000000..c3d74b20 --- /dev/null +++ b/includes/HTMLForm.php @@ -0,0 +1,177 @@ +mRequest = $request; + } + + /** + * @private + * @param $name String: name of the fieldset. + * @param $content String: HTML content to put in. + * @return string HTML fieldset + */ + function fieldset( $name, $content ) { + return "
    ".wfMsg($this->mName.'-'.$name)."\n" . + $content . "\n
    \n"; + } + + /** + * @private + * @param $varname String: name of the checkbox. + * @param $checked Boolean: set true to check the box (default False). + */ + function checkbox( $varname, $checked=false ) { + if ( $this->mRequest->wasPosted() && !is_null( $this->mRequest->getVal( $varname ) ) ) { + $checked = $this->mRequest->getCheck( $varname ); + } + return "
    \n"; + } + + /** + * @private + * @param $varname String: name of the textbox. + * @param $value String: optional value (default empty) + * @param $size Integer: optional size of the textbox (default 20) + */ + function textbox( $varname, $value='', $size=20 ) { + if ( $this->mRequest->wasPosted() ) { + $value = $this->mRequest->getText( $varname, $value ); + } + $value = htmlspecialchars( $value ); + return "
    \n"; + } + + /** + * @private + * @param $varname String: name of the radiobox. + * @param $fields Array: Various fields. + */ + function radiobox( $varname, $fields ) { + foreach ( $fields as $value => $checked ) { + $s .= "
    \n"; + } + return $this->fieldset( $this->mName.'-'.$varname, $s ); + } + + /** + * @private + * @param $varname String: name of the textareabox. + * @param $value String: optional value (default empty) + * @param $size Integer: optional size of the textarea (default 20) + */ + function textareabox ( $varname, $value='', $size=20 ) { + if ( $this->mRequest->wasPosted() ) { + $value = $this->mRequest->getText( $varname, $value ); + } + $value = htmlspecialchars( $value ); + return '
    \n"; + } + + /** + * @private + * @param $varname String: name of the arraybox. + * @param $size Integer: Optional size of the textarea (default 20) + */ + function arraybox( $varname , $size=20 ) { + $s = ''; + if ( $this->mRequest->wasPosted() ) { + $arr = $this->mRequest->getArray( $varname ); + if ( is_array( $arr ) ) { + foreach ( $_POST[$varname] as $index => $element ) { + $s .= htmlspecialchars( $element )."\n"; + } + } + } + return "
    + +
    $warning
    +END + ); + } else { + $wgOut->addWikiText( << +[[Media:$filename|$filename]]$dirmark ($info) + +END + ); + } + } + + if($this->img->fromSharedDirectory) { + $this->printSharedImageText(); + } + } else { + # Image does not exist + + $title = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), + 'wpDestFile=' . urlencode( $this->img->getName() ) ); + $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); + } + } + + function printSharedImageText() { + global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut, $wgUser; + + $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey()); + $sharedtext = "
    " . wfMsgWikiHtml("sharedupload"); + if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) { + + $sk = $wgUser->getSkin(); + $title = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'), + array( 'wpDestFile' => urlencode( $this->img->getName() ))); + $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link); + } + $sharedtext .= "
    "; + $wgOut->addHTML($sharedtext); + + if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { + require_once("HttpFunctions.php"); + $ur = ini_set('allow_url_fopen', true); + $text = wfGetHTTP($url . '?action=render'); + ini_set('allow_url_fopen', $ur); + if ($text) + $this->mExtraDescription = $text; + } + } + + function getUploadUrl() { + global $wgServer; + $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); + return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); + } + + /** + * Print out the various links at the bottom of the image page, e.g. reupload, + * external editing (and instructions link) etc. + */ + function uploadLinksBox() { + global $wgUser, $wgOut; + + if( $this->img->fromSharedDirectory ) + return; + + $sk = $wgUser->getSkin(); + + $wgOut->addHtml( '
      ' ); + + # "Upload a new version of this file" link + if( $wgUser->isAllowed( 'reupload' ) ) { + $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); + $wgOut->addHtml( "
    • {$ulink}
    • " ); + } + + # External editing link + $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); + $wgOut->addHtml( '
    • ' . $elink . '
      ' . wfMsgWikiHtml( 'edit-externally-help' ) . '
    • ' ); + + $wgOut->addHtml( '
    ' ); + } + + function closeShowImage() + { + # For overloading + + } + + /** + * If the page we've just displayed is in the "Image" namespace, + * we follow it with an upload history of the image and its usage. + */ + function imageHistory() + { + global $wgUser, $wgOut, $wgUseExternalEditor; + + $sk = $wgUser->getSkin(); + + $line = $this->img->nextHistoryLine(); + + if ( $line ) { + $list =& new ImageHistoryList( $sk ); + $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, + $line->img_width, $line->img_height + ); + + while ( $line = $this->img->nextHistoryLine() ) { + $s .= $list->imageHistoryLine( false, $line->img_timestamp, + $line->oi_archive_name, $line->img_user, + $line->img_user_text, $line->img_size, $line->img_description, + $line->img_width, $line->img_height + ); + } + $s .= $list->endImageHistoryList(); + } else { $s=''; } + $wgOut->addHTML( $s ); + + # Exist check because we don't want to show this on pages where an image + # doesn't exist along with the noimage message, that would suck. -ævar + if( $wgUseExternalEditor && $this->img->exists() ) { + $this->uploadLinksBox(); + } + + } + + function imageLinks() + { + global $wgUser, $wgOut; + + $wgOut->addHTML( '\n" ); + + $dbr =& wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $imagelinks = $dbr->tableName( 'imagelinks' ); + + $sql = "SELECT page_namespace,page_title FROM $imagelinks,$page WHERE il_to=" . + $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id"; + $sql = $dbr->limitResult($sql, 500, 0); + $res = $dbr->query( $sql, "ImagePage::imageLinks" ); + + if ( 0 == $dbr->numRows( $res ) ) { + $wgOut->addHtml( '

    ' . wfMsg( "nolinkstoimage" ) . "

    \n" ); + return; + } + $wgOut->addHTML( '

    ' . wfMsg( 'linkstoimage' ) . "

    \n
      " ); + + $sk = $wgUser->getSkin(); + while ( $s = $dbr->fetchObject( $res ) ) { + $name = Title::MakeTitle( $s->page_namespace, $s->page_title ); + $link = $sk->makeKnownLinkObj( $name, "" ); + $wgOut->addHTML( "
    • {$link}
    • \n" ); + } + $wgOut->addHTML( "
    \n" ); + } + + function delete() + { + global $wgUser, $wgOut, $wgRequest; + + $confirm = $wgRequest->wasPosted(); + $image = $wgRequest->getVal( 'image' ); + $oldimage = $wgRequest->getVal( 'oldimage' ); + + # Only sysops can delete images. Previously ordinary users could delete + # old revisions, but this is no longer the case. + if ( !$wgUser->isAllowed('delete') ) { + $wgOut->sysopRequired(); + return; + } + if ( $wgUser->isBlocked() ) { + return $this->blockedIPpage(); + } + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + + # Better double-check that it hasn't been deleted yet! + $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); + if ( ( !is_null( $image ) ) + && ( '' == trim( $image ) ) ) { + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + return; + } + + $this->img = new Image( $this->mTitle ); + + # Deleting old images doesn't require confirmation + if ( !is_null( $oldimage ) || $confirm ) { + if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { + $this->doDelete(); + } else { + $wgOut->showFatalError( wfMsg( 'sessionfailure' ) ); + } + return; + } + + if ( !is_null( $image ) ) { + $q = '&image=' . urlencode( $image ); + } else if ( !is_null( $oldimage ) ) { + $q = '&oldimage=' . urlencode( $oldimage ); + } else { + $q = ''; + } + return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) ); + } + + function doDelete() { + global $wgOut, $wgRequest, $wgUseSquid; + global $wgPostCommitUpdateList; + + $fname = 'ImagePage::doDelete'; + + $reason = $wgRequest->getVal( 'wpReason' ); + $oldimage = $wgRequest->getVal( 'oldimage' ); + + $dbw =& wfGetDB( DB_MASTER ); + + if ( !is_null( $oldimage ) ) { + if ( strlen( $oldimage ) < 16 ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); + return; + } + if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); + return; + } + if ( !$this->doDeleteOldImage( $oldimage ) ) { + return; + } + $deleted = $oldimage; + } else { + $ok = $this->img->delete( $reason ); + if( !$ok ) { + # If the deletion operation actually failed, bug out: + $wgOut->showFileDeleteError( $this->img->getName() ); + return; + } + + # Image itself is now gone, and database is cleaned. + # Now we remove the image description page. + + $article = new Article( $this->mTitle ); + $article->doDeleteArticle( $reason ); # ignore errors + + $deleted = $this->img->getName(); + } + + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; + $text = wfMsg( 'deletedtext', $deleted, $loglink ); + + $wgOut->addWikiText( $text ); + + $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); + } + + /** + * @return success + */ + function doDeleteOldImage( $oldimage ) + { + global $wgOut; + + $ok = $this->img->deleteOld( $oldimage, '' ); + if( !$ok ) { + # If we actually have a file and can't delete it, throw an error. + # Something went awry... + $wgOut->showFileDeleteError( "$oldimage" ); + } else { + # Log the deletion + $log = new LogPage( 'delete' ); + $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) ); + } + return $ok; + } + + function revert() { + global $wgOut, $wgRequest, $wgUser; + + $oldimage = $wgRequest->getText( 'oldimage' ); + if ( strlen( $oldimage ) < 16 ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); + return; + } + if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); + return; + } + + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + if( $wgUser->isAnon() ) { + $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + return; + } + if ( ! $this->mTitle->userCanEdit() ) { + $wgOut->sysopRequired(); + return; + } + if ( $wgUser->isBlocked() ) { + return $this->blockedIPpage(); + } + if( !$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { + $wgOut->showErrorPage( 'internalerror', 'sessionfailure' ); + return; + } + $name = substr( $oldimage, 15 ); + + $dest = wfImageDir( $name ); + $archive = wfImageArchiveDir( $name ); + $curfile = "{$dest}/{$name}"; + + if ( !is_dir( $dest ) ) wfMkdirParents( $dest ); + if ( !is_dir( $archive ) ) wfMkdirParents( $archive ); + + if ( ! is_file( $curfile ) ) { + $wgOut->showFileNotFoundError( htmlspecialchars( $curfile ) ); + return; + } + $oldver = wfTimestampNow() . "!{$name}"; + + $dbr =& wfGetDB( DB_SLAVE ); + $size = $dbr->selectField( 'oldimage', 'oi_size', array( 'oi_archive_name' => $oldimage ) ); + + if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) { + $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" ); + return; + } + if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) { + $wgOut->showFileCopyError( "${archive}/{$oldimage}", $curfile ); + return; + } + + # Record upload and update metadata cache + $img = Image::newFromName( $name ); + $img->recordUpload( $oldver, wfMsg( "reverted" ) ); + + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( wfMsg( 'imagereverted' ) ); + + $descTitle = $img->getTitle(); + $wgOut->returnToMain( false, $descTitle->getPrefixedText() ); + } + + function blockedIPpage() { + $edit = new EditPage( $this ); + return $edit->blockedIPpage(); + } + + /** + * Override handling of action=purge + */ + function doPurge() { + $this->img = new Image( $this->mTitle ); + if( $this->img->exists() ) { + wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); + $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); + $update->doUpdate(); + $this->img->purgeCache(); + } else { + wfDebug( "ImagePage::doPurge no image\n" ); + } + parent::doPurge(); + } + +} + +/** + * @todo document + * @package MediaWiki + */ +class ImageHistoryList { + function ImageHistoryList( &$skin ) { + $this->skin =& $skin; + } + + function beginImageHistoryList() { + $s = "\n

    " . wfMsg( 'imghistory' ) . "

    \n" . + "

    " . wfMsg( 'imghistlegend' ) . "

    \n".'
      '; + return $s; + } + + function endImageHistoryList() { + $s = "
    \n"; + return $s; + } + + function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $width, $height ) { + global $wgUser, $wgLang, $wgTitle, $wgContLang; + + $datetime = $wgLang->timeanddate( $timestamp, true ); + $del = wfMsg( 'deleteimg' ); + $delall = wfMsg( 'deleteimgcompletely' ); + $cur = wfMsg( 'cur' ); + + if ( $iscur ) { + $url = Image::imageUrl( $img ); + $rlink = $cur; + if ( $wgUser->isAllowed('delete') ) { + $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() . + '&action=delete' ); + $style = $this->skin->getInternalLinkAttributes( $link, $delall ); + + $dlink = ''.$delall.''; + } else { + $dlink = $del; + } + } else { + $url = htmlspecialchars( wfImageArchiveUrl( $img ) ); + if( $wgUser->getID() != 0 && $wgTitle->userCanEdit() ) { + $token = urlencode( $wgUser->editToken( $img ) ); + $rlink = $this->skin->makeKnownLinkObj( $wgTitle, + wfMsg( 'revertimg' ), 'action=revert&oldimage=' . + urlencode( $img ) . "&wpEditToken=$token" ); + $dlink = $this->skin->makeKnownLinkObj( $wgTitle, + $del, 'action=delete&oldimage=' . urlencode( $img ) . + "&wpEditToken=$token" ); + } else { + # Having live active links for non-logged in users + # means that bots and spiders crawling our site can + # inadvertently change content. Baaaad idea. + $rlink = wfMsg( 'revertimg' ); + $dlink = $del; + } + } + + $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); + $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $size ) ); + $widthheight = wfMsg( 'widthheight', $width, $height ); + $style = $this->skin->getInternalLinkAttributes( $url, $datetime ); + + $s = "
  • ({$dlink}) ({$rlink}) {$datetime} . . {$userlink} . . {$widthheight} ({$nbytes})"; + + $s .= $this->skin->commentBlock( $description, $wgTitle ); + $s .= "
  • \n"; + return $s; + } + +} + + +?> diff --git a/includes/JobQueue.php b/includes/JobQueue.php new file mode 100644 index 00000000..746cf5de --- /dev/null +++ b/includes/JobQueue.php @@ -0,0 +1,267 @@ +selectRow( 'job', '*', '', __METHOD__, + array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ) + ); + + if ( $row === false ) { + wfProfileOut( __METHOD__ ); + return false; + } + + // Try to delete it from the master + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); + $affected = $dbw->affectedRows(); + $dbw->immediateCommit(); + + if ( !$affected ) { + // Failed, someone else beat us to it + // Try getting a random row + $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob', + 'MAX(job_id) as maxjob' ), '', __METHOD__ ); + if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) { + // No jobs to get + wfProfileOut( __METHOD__ ); + return false; + } + // Get the random row + $row = $dbw->selectRow( 'job', '*', + array( 'job_id' => mt_rand( $row->minjob, $row->maxjob ) ), __METHOD__ ); + if ( $row === false ) { + // Random job gone before we got the chance to select it + // Give up + wfProfileOut( __METHOD__ ); + return false; + } + // Delete the random row + $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); + $affected = $dbw->affectedRows(); + $dbw->immediateCommit(); + + if ( !$affected ) { + // Random job gone before we exclusively deleted it + // Give up + wfProfileOut( __METHOD__ ); + return false; + } + } + + // If execution got to here, there's a row in $row that has been deleted from the database + // by this thread. Hence the concurrent pop was successful. + $namespace = $row->job_namespace; + $dbkey = $row->job_title; + $title = Title::makeTitleSafe( $namespace, $dbkey ); + $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); + + // Remove any duplicates it may have later in the queue + $dbw->delete( 'job', $job->insertFields(), __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $job; + } + + /** + * Create an object of a subclass + */ + static function factory( $command, $title, $params = false, $id = 0 ) { + switch ( $command ) { + case 'refreshLinks': + return new RefreshLinksJob( $title, $params, $id ); + case 'htmlCacheUpdate': + case 'html_cache_update': # BC + return new HTMLCacheUpdateJob( $title, $params['table'], $params['start'], $params['end'], $id ); + default: + throw new MWException( "Invalid job command \"$command\"" ); + } + } + + static function makeBlob( $params ) { + if ( $params !== false ) { + return serialize( $params ); + } else { + return ''; + } + } + + static function extractBlob( $blob ) { + if ( (string)$blob !== '' ) { + return unserialize( $blob ); + } else { + return false; + } + } + + /*------------------------------------------------------------------------- + * Non-static functions + *------------------------------------------------------------------------*/ + + function __construct( $command, $title, $params = false, $id = 0 ) { + $this->command = $command; + $this->title = $title; + $this->params = $params; + $this->id = $id; + + // A bit of premature generalisation + // Oh well, the whole class is premature generalisation really + $this->removeDuplicates = true; + } + + /** + * Insert a single job into the queue. + */ + function insert() { + $fields = $this->insertFields(); + + $dbw =& wfGetDB( DB_MASTER ); + + if ( $this->removeDuplicates ) { + $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ ); + if ( $dbw->numRows( $res ) ) { + return; + } + } + $fields['job_id'] = $dbw->nextSequenceValue( 'job_job_id_seq' ); + $dbw->insert( 'job', $fields, __METHOD__ ); + } + + protected function insertFields() { + return array( + 'job_cmd' => $this->command, + 'job_namespace' => $this->title->getNamespace(), + 'job_title' => $this->title->getDBkey(), + 'job_params' => Job::makeBlob( $this->params ) + ); + } + + /** + * Batch-insert a group of jobs into the queue. + * This will be wrapped in a transaction with a forced commit. + * + * This may add duplicate at insert time, but they will be + * removed later on, when the first one is popped. + * + * @param $jobs array of Job objects + */ + static function batchInsert( $jobs ) { + if( count( $jobs ) ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + foreach( $jobs as $job ) { + $rows[] = $job->insertFields(); + } + $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); + $dbw->commit(); + } + } + + /** + * Run the job + * @return boolean success + */ + abstract function run(); + + function toString() { + $paramString = ''; + if ( $this->params ) { + foreach ( $this->params as $key => $value ) { + if ( $paramString != '' ) { + $paramString .= ' '; + } + $paramString .= "$key=$value"; + } + } + + if ( is_object( $this->title ) ) { + $s = "{$this->command} " . $this->title->getPrefixedDBkey(); + if ( $paramString !== '' ) { + $s .= ' ' . $paramString; + } + return $s; + } else { + return "{$this->command} $paramString"; + } + } + + function getLastError() { + return $this->error; + } +} + +class RefreshLinksJob extends Job { + function __construct( $title, $params = '', $id = 0 ) { + parent::__construct( 'refreshLinks', $title, $params, $id ); + } + + /** + * Run a refreshLinks job + * @return boolean success + */ + function run() { + global $wgParser; + wfProfileIn( __METHOD__ ); + + $linkCache =& LinkCache::singleton(); + $linkCache->clear(); + + if ( is_null( $this->title ) ) { + $this->error = "refreshLinks: Invalid title"; + wfProfileOut( __METHOD__ ); + return false; + } + + $revision = Revision::newFromTitle( $this->title ); + if ( !$revision ) { + $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"'; + wfProfileOut( __METHOD__ ); + return false; + } + + wfProfileIn( __METHOD__.'-parse' ); + $options = new ParserOptions; + $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() ); + wfProfileOut( __METHOD__.'-parse' ); + wfProfileIn( __METHOD__.'-update' ); + $update = new LinksUpdate( $this->title, $parserOutput, false ); + $update->doUpdate(); + wfProfileOut( __METHOD__.'-update' ); + wfProfileOut( __METHOD__ ); + return true; + } +} + +?> diff --git a/includes/Licenses.php b/includes/Licenses.php new file mode 100644 index 00000000..aaa44052 --- /dev/null +++ b/includes/Licenses.php @@ -0,0 +1,171 @@ + + * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + */ + +class Licenses { + /**#@+ + * @private + */ + /** + * @var string + */ + var $msg; + + /** + * @var array + */ + var $licenses = array(); + + /** + * @var string + */ + var $html; + /**#@-*/ + + /** + * Constrictor + * + * @param $str String: the string to build the licenses member from, will use + * wfMsgForContent( 'licenses' ) if null (default: null) + */ + function Licenses( $str = null ) { + // PHP sucks, this should be possible in the constructor + $this->msg = is_null( $str ) ? wfMsgForContent( 'licenses' ) : $str; + $this->html = ''; + + $this->makeLicenses(); + $tmp = $this->getLicenses(); + $this->makeHtml( $tmp ); + } + + /**#@+ + * @private + */ + function makeLicenses() { + $levels = array(); + $lines = explode( "\n", $this->msg ); + + foreach ( $lines as $line ) { + if ( strpos( $line, '*' ) !== 0 ) + continue; + else { + list( $level, $line ) = $this->trimStars( $line ); + + if ( strpos( $line, '|' ) !== false ) { + $obj = new License( $line ); + $this->stackItem( $this->licenses, $levels, $obj ); + } else { + if ( $level < count( $levels ) ) + $levels = array_slice( $levels, 0, $level ); + if ( $level == count( $levels ) ) + $levels[$level - 1] = $line; + else if ( $level > count( $levels ) ) + $levels[] = $line; + } + } + } + } + + function trimStars( $str ) { + $i = $count = 0; + + wfSuppressWarnings(); + while ($str[$i++] == '*') + ++$count; + wfRestoreWarnings(); + + return array( $count, ltrim( $str, '* ' ) ); + } + + function stackItem( &$list, $path, $item ) { + $position =& $list; + if ( $path ) + foreach( $path as $key ) + $position =& $position[$key]; + $position[] = $item; + } + + function makeHtml( &$tagset, $depth = 0 ) { + foreach ( $tagset as $key => $val ) + if ( is_array( $val ) ) { + $this->html .= $this->outputOption( + $this->msg( $key ), + array( + 'value' => '', + 'disabled' => 'disabled', + 'style' => 'color: GrayText', // for MSIE + ), + $depth + ); + $this->makeHtml( $val, $depth + 1 ); + } else { + $this->html .= $this->outputOption( + $this->msg( $val->text ), + array( + 'value' => $val->template, + 'title' => '{{' . $val->template . '}}' + ), + $depth + ); + } + } + + function outputOption( $val, $attribs = null, $depth ) { + $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $val; + return str_repeat( "\t", $depth ) . wfElement( 'option', $attribs, $val ) . "\n"; + } + + function msg( $str ) { + $out = wfMsg( $str ); + return wfEmptyMsg( $str, $out ) ? $str : $out; + } + + /**#@-*/ + + /** + * Accessor for $this->licenses + * + * @return array + */ + function getLicenses() { return $this->licenses; } + + /** + * Accessor for $this->html + * + * @return string + */ + function getHtml() { return $this->html; } +} + +class License { + /** + * @var string + */ + var $template; + + /** + * @var string + */ + var $text; + + /** + * Constructor + * + * @param $str String: license name?? + */ + function License( $str ) { + list( $text, $template ) = explode( '|', strrev( $str ), 2 ); + + $this->template = strrev( $template ); + $this->text = strrev( $text ); + } +} +?> diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php new file mode 100644 index 00000000..e0f0f6fd --- /dev/null +++ b/includes/LinkBatch.php @@ -0,0 +1,184 @@ +addObj( $item ); + } + } + + function addObj( $title ) { + if ( is_object( $title ) ) { + $this->add( $title->getNamespace(), $title->getDBkey() ); + } else { + wfDebug( "Warning: LinkBatch::addObj got invalid title object\n" ); + } + } + + function add( $ns, $dbkey ) { + if ( $ns < 0 ) { + return; + } + if ( !array_key_exists( $ns, $this->data ) ) { + $this->data[$ns] = array(); + } + + $this->data[$ns][$dbkey] = 1; + } + + /** + * Set the link list to a given 2-d array + * First key is the namespace, second is the DB key, value arbitrary + */ + function setArray( $array ) { + $this->data = $array; + } + + /** + * Returns true if no pages have been added, false otherwise. + */ + function isEmpty() { + return ($this->getSize() == 0); + } + + /** + * Returns the size of the batch. + */ + function getSize() { + return count( $this->data ); + } + + /** + * Do the query and add the results to the LinkCache object + * Return an array mapping PDBK to ID + */ + function execute() { + $linkCache =& LinkCache::singleton(); + $this->executeInto( $linkCache ); + } + + /** + * Do the query and add the results to a given LinkCache object + * Return an array mapping PDBK to ID + */ + function executeInto( &$cache ) { + $fname = 'LinkBatch::executeInto'; + wfProfileIn( $fname ); + // Do query + $res = $this->doQuery(); + if ( !$res ) { + wfProfileOut( $fname ); + return array(); + } + + // For each returned entry, add it to the list of good links, and remove it from $remaining + + $ids = array(); + $remaining = $this->data; + while ( $row = $res->fetchObject() ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $cache->addGoodLinkObj( $row->page_id, $title ); + $ids[$title->getPrefixedDBkey()] = $row->page_id; + unset( $remaining[$row->page_namespace][$row->page_title] ); + } + $res->free(); + + // The remaining links in $data are bad links, register them as such + foreach ( $remaining as $ns => $dbkeys ) { + foreach ( $dbkeys as $dbkey => $nothing ) { + $title = Title::makeTitle( $ns, $dbkey ); + $cache->addBadLinkObj( $title ); + $ids[$title->getPrefixedDBkey()] = 0; + } + } + wfProfileOut( $fname ); + return $ids; + } + + /** + * Perform the existence test query, return a ResultWrapper with page_id fields + */ + function doQuery() { + $fname = 'LinkBatch::doQuery'; + $namespaces = array(); + + if ( $this->isEmpty() ) { + return false; + } + wfProfileIn( $fname ); + + // Construct query + // This is very similar to Parser::replaceLinkHolders + $dbr =& wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $set = $this->constructSet( 'page', $dbr ); + if ( $set === false ) { + wfProfileOut( $fname ); + return false; + } + $sql = "SELECT page_id, page_namespace, page_title FROM $page WHERE $set"; + + // Do query + $res = new ResultWrapper( $dbr, $dbr->query( $sql, $fname ) ); + wfProfileOut( $fname ); + return $res; + } + + /** + * Construct a WHERE clause which will match all the given titles. + * Give the appropriate table's field name prefix ('page', 'pl', etc). + * + * @param $prefix String: ?? + * @return string + * @public + */ + function constructSet( $prefix, &$db ) { + $first = true; + $firstTitle = true; + $sql = ''; + foreach ( $this->data as $ns => $dbkeys ) { + if ( !count( $dbkeys ) ) { + continue; + } + + if ( $first ) { + $first = false; + } else { + $sql .= ' OR '; + } + $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; + + $firstTitle = true; + foreach( $dbkeys as $dbkey => $nothing ) { + if ( $firstTitle ) { + $firstTitle = false; + } else { + $sql .= ','; + } + $sql .= $db->addQuotes( $dbkey ); + } + + $sql .= '))'; + } + if ( $first && $firstTitle ) { + # No titles added + return false; + } else { + return $sql; + } + } +} + +?> diff --git a/includes/LinkCache.php b/includes/LinkCache.php new file mode 100644 index 00000000..451b3f0c --- /dev/null +++ b/includes/LinkCache.php @@ -0,0 +1,178 @@ +mForUpdate = false; + $this->mPageLinks = array(); + $this->mGoodLinks = array(); + $this->mBadLinks = array(); + } + + /* private */ function getKey( $title ) { + global $wgDBname; + return $wgDBname.':lc:title:'.$title; + } + + /** + * General accessor to get/set whether SELECT FOR UPDATE should be used + */ + function forUpdate( $update = NULL ) { + return wfSetVar( $this->mForUpdate, $update ); + } + + function getGoodLinkID( $title ) { + if ( array_key_exists( $title, $this->mGoodLinks ) ) { + return $this->mGoodLinks[$title]; + } else { + return 0; + } + } + + function isBadLink( $title ) { + return array_key_exists( $title, $this->mBadLinks ); + } + + function addGoodLinkObj( $id, $title ) { + $dbkey = $title->getPrefixedDbKey(); + $this->mGoodLinks[$dbkey] = $id; + $this->mPageLinks[$dbkey] = $title; + } + + function addBadLinkObj( $title ) { + $dbkey = $title->getPrefixedDbKey(); + if ( ! $this->isBadLink( $dbkey ) ) { + $this->mBadLinks[$dbkey] = 1; + $this->mPageLinks[$dbkey] = $title; + } + } + + function clearBadLink( $title ) { + unset( $this->mBadLinks[$title] ); + $this->clearLink( $title ); + } + + function clearLink( $title ) { + global $wgMemc, $wgLinkCacheMemcached; + if( $wgLinkCacheMemcached ) + $wgMemc->delete( $this->getKey( $title ) ); + } + + function getPageLinks() { return $this->mPageLinks; } + function getGoodLinks() { return $this->mGoodLinks; } + function getBadLinks() { return array_keys( $this->mBadLinks ); } + + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * @param $title String: title to add + * @return integer + */ + function addLink( $title ) { + $nt = Title::newFromDBkey( $title ); + if( $nt ) { + return $this->addLinkObj( $nt ); + } else { + return 0; + } + } + + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * @param $nt Title to add. + * @return integer + */ + function addLinkObj( &$nt ) { + global $wgMemc, $wgLinkCacheMemcached, $wgAntiLockFlags; + $title = $nt->getPrefixedDBkey(); + if ( $this->isBadLink( $title ) ) { return 0; } + $id = $this->getGoodLinkID( $title ); + if ( 0 != $id ) { return $id; } + + $fname = 'LinkCache::addLinkObj'; + global $wgProfiling, $wgProfiler; + if ( $wgProfiling && isset( $wgProfiler ) ) { + $fname .= ' (' . $wgProfiler->getCurrentSection() . ')'; + } + + wfProfileIn( $fname ); + + $ns = $nt->getNamespace(); + $t = $nt->getDBkey(); + + if ( '' == $title ) { + wfProfileOut( $fname ); + return 0; + } + + $id = NULL; + if( $wgLinkCacheMemcached ) + $id = $wgMemc->get( $key = $this->getKey( $title ) ); + if( ! is_integer( $id ) ) { + if ( $this->mForUpdate ) { + $db =& wfGetDB( DB_MASTER ); + if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { + $options = array( 'FOR UPDATE' ); + } else { + $options = array(); + } + } else { + $db =& wfGetDB( DB_SLAVE ); + $options = array(); + } + + $id = $db->selectField( 'page', 'page_id', + array( 'page_namespace' => $ns, 'page_title' => $t ), + $fname, $options ); + if ( !$id ) { + $id = 0; + } + if( $wgLinkCacheMemcached ) + $wgMemc->add( $key, $id, 3600*24 ); + } + + if( 0 == $id ) { + $this->addBadLinkObj( $nt ); + } else { + $this->addGoodLinkObj( $id, $nt ); + } + wfProfileOut( $fname ); + return $id; + } + + /** + * Clears cache + */ + function clear() { + $this->mPageLinks = array(); + $this->mGoodLinks = array(); + $this->mBadLinks = array(); + } +} +?> diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php new file mode 100644 index 00000000..e03b59dd --- /dev/null +++ b/includes/LinkFilter.php @@ -0,0 +1,92 @@ + diff --git a/includes/Linker.php b/includes/Linker.php new file mode 100644 index 00000000..4a0eafbd --- /dev/null +++ b/includes/Linker.php @@ -0,0 +1,1101 @@ +checkTitleEncoding( $link ); + $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link ); + $link = htmlspecialchars( $link ); + + $r = ($class != '') ? " class=\"$class\"" : " class=\"external\""; + + $r .= " title=\"{$link}\""; + return $r; + } + + /** @todo document */ + function getInternalLinkAttributes( $link, $text, $broken = false ) { + $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 .= " title=\"{$link}\""; + return $r; + } + + /** + * @param $nt Title object. + * @param $text String: FIXME + * @param $broken Boolean: FIXME, default 'false'. + */ + function getInternalLinkAttributesObj( &$nt, $text, $broken = false ) { + if( $broken == 'stub' ) { + $r = ' class="stub"'; + } else if ( $broken == 'yes' ) { + $r = ' class="new"'; + } else { + $r = ''; + } + + $r .= ' title="' . $nt->getEscapedText() . '"'; + return $r; + } + + /** + * 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. + * + * @param $title String: the text of the title + * @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 makeLink( $title, $text = '', $query = '', $trail = '' ) { + wfProfileIn( 'Linker::makeLink' ); + $nt = Title::newFromText( $title ); + if ($nt) { + $result = $this->makeLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + } else { + wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" ); + $result = $text == "" ? $title : $text; + } + + wfProfileOut( 'Linker::makeLink' ); + return $result; + } + + /** + * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeKnownLinkObj for further documentation. + * + * @param $title String: the text of the title + * @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 makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') { + $nt = Title::newFromText( $title ); + if ($nt) { + return $this->makeKnownLinkObj( Title::newFromText( $title ), $text, $query, $trail, $prefix , $aprops ); + } else { + wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" ); + return $text == '' ? $title : $text; + } + } + + /** + * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeBrokenLinkObj for further documentation. + * + * @param string $title The text of the title + * @param string $text Link text + * @param string $query Optional query part + * @param string $trail 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 makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) { + $nt = Title::newFromText( $title ); + if ($nt) { + return $this->makeBrokenLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + } else { + wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" ); + return $text == '' ? $title : $text; + } + } + + /** + * 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. + * + * @param $title String: the text of the title + * @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 makeStubLink( $title, $text = '', $query = '', $trail = '' ) { + $nt = Title::newFromText( $title ); + if ($nt) { + return $this->makeStubLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + } else { + wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" ); + return $text == '' ? $title : $text; + } + } + + /** + * Make a link for a title which may or may not be in the database. If you need to + * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each + * call to this will result in a DB query. + * + * @param $title String: the text of the title + * @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 makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) { + global $wgUser; + $fname = 'Linker::makeLinkObj'; + wfProfileIn( $fname ); + + # Fail gracefully + if ( ! is_object($nt) ) { + # throw new MWException(); + wfProfileOut( $fname ); + return "{$prefix}{$text}{$trail}"; + } + + $ns = $nt->getNamespace(); + $dbkey = $nt->getDBkey(); + if ( $nt->isExternal() ) { + $u = $nt->getFullURL(); + $link = $nt->getPrefixedURL(); + if ( '' == $text ) { $text = $nt->getPrefixedText(); } + $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' ); + + $inside = ''; + if ( '' != $trail ) { + if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) { + $inside = $m[1]; + $trail = $m[2]; + } + } + + # Check for anchors, normalize the anchor + + $parts = explode( '#', $u, 2 ); + if ( count( $parts ) == 2 ) { + $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $parts[1] ) ) ); + $replacearray = array( + '%3A' => ':', + '%' => '.' + ); + $u = $parts[0] . '#' . + str_replace( array_keys( $replacearray ), + array_values( $replacearray ), + $anchor ); + } + + $t = "{$text}{$inside}"; + + wfProfileOut( $fname ); + 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' ); + # Work out link colour immediately + $aid = $nt->getArticleID() ; + if ( 0 == $aid ) { + $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); + } else { + $threshold = $wgUser->getOption('stubthreshold') ; + if ( $threshold > 0 ) { + $dbr =& wfGetDB( DB_SLAVE ); + $s = $dbr->selectRow( + array( 'page' ), + array( 'page_len', + 'page_namespace', + 'page_is_redirect' ), + array( 'page_id' => $aid ), $fname ) ; + if ( $s !== false ) { + $size = $s->page_len; + if ( $s->page_is_redirect OR $s->page_namespace != NS_MAIN ) { + $size = $threshold*2 ; # Really big + } + } else { + $size = $threshold*2 ; # Really big + } + } else { + $size = 1 ; + } + if ( $size < $threshold ) { + $retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); + } else { + $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); + } + } + wfProfileOut( $fname.'-immediate' ); + } + wfProfileOut( $fname ); + return $retVal; + } + + /** + * Make a link for a title which definitely exists. This is faster than makeLinkObj because + * it doesn't have to do a database query. It's also valid for interwiki titles and special + * pages. + * + * @param $nt Title object of target page + * @param $text String: text to replace the title + * @param $query String: link target + * @param $trail String: text after link + * @param $prefix String: text before link text + * @param $aprops String: extra attributes to the a-element + * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead + * @return the a-element + */ + function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { + + $fname = 'Linker::makeKnownLinkObj'; + wfProfileIn( $fname ); + + if ( !is_object( $nt ) ) { + wfProfileOut( $fname ); + return $text; + } + + $u = $nt->escapeLocalURL( $query ); + if ( $nt->getFragment() != '' ) { + if( $nt->getPrefixedDbkey() == '' ) { + $u = ''; + if ( '' == $text ) { + $text = htmlspecialchars( $nt->getFragment() ); + } + } + $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace( ' ', '_', $nt->getFragment() ) ) ); + $replacearray = array( + '%3A' => ':', + '%' => '.' + ); + $u .= '#' . str_replace(array_keys($replacearray),array_values($replacearray),$anchor); + } + if ( $text == '' ) { + $text = htmlspecialchars( $nt->getPrefixedText() ); + } + if ( $style == '' ) { + $style = $this->getInternalLinkAttributesObj( $nt, $text ); + } + + if ( $aprops !== '' ) $aprops = ' ' . $aprops; + + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $r = "{$prefix}{$text}{$inside}{$trail}"; + wfProfileOut( $fname ); + return $r; + } + + /** + * Make a red link to the edit page of a given title. + * + * @param $title String: The text of the title + * @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 makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + # Fail gracefully + if ( ! isset($nt) ) { + # throw new MWException(); + return "{$prefix}{$text}{$trail}"; + } + + $fname = 'Linker::makeBrokenLinkObj'; + wfProfileIn( $fname ); + + if ( '' == $query ) { + $q = 'action=edit'; + } else { + $q = 'action=edit&'.$query; + } + $u = $nt->escapeLocalURL( $q ); + + if ( '' == $text ) { + $text = htmlspecialchars( $nt->getPrefixedText() ); + } + $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); + + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $s = "{$prefix}{$text}{$inside}{$trail}"; + + wfProfileOut( $fname ); + return $s; + } + + /** + * Make a brown link to a short article. + * + * @param $title String: the text of the title + * @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 makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + $link = $nt->getPrefixedURL(); + + $u = $nt->escapeLocalURL( $query ); + + if ( '' == $text ) { + $text = htmlspecialchars( $nt->getPrefixedText() ); + } + $style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' ); + + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $s = "{$prefix}{$text}{$inside}{$trail}"; + return $s; + } + + /** + * Generate either a normal exists-style link or a stub link, depending + * on the given page size. + * + * @param $size Integer + * @param $nt Title object. + * @param $text String + * @param $query String + * @param $trail String + * @param $prefix String + * @return string HTML of link + */ + 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 ); + } + } + + /** + * Make appropriate markup for a link to the current article. This is currently rendered + * as the bold link text. The calling sequence is the same as the other make*LinkObj functions, + * despite $query not being used. + */ + function makeSelfLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + if ( '' == $text ) { + $text = htmlspecialchars( $nt->getPrefixedText() ); + } + list( $inside, $trail ) = Linker::splitTrail( $trail ); + return "{$prefix}{$text}{$inside}{$trail}"; + } + + /** @todo document */ + function fnamePart( $url ) { + $basename = strrchr( $url, '/' ); + if ( false === $basename ) { + $basename = $url; + } else { + $basename = substr( $basename, 1 ); + } + return htmlspecialchars( $basename ); + } + + /** Obsolete alias */ + function makeImage( $url, $alt = '' ) { + return $this->makeExternalImage( $url, $alt ); + } + + /** @todo document */ + function makeExternalImage( $url, $alt = '' ) { + if ( '' == $alt ) { + $alt = $this->fnamePart( $url ); + } + $s = ''.$alt.''; + return $s; + } + + /** @todo document */ + function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false, + $thumb = false, $manual_thumb = '' ) + { + global $wgContLang, $wgUser, $wgThumbLimits, $wgGenerateThumbnailOnParse; + + $img = new Image( $nt ); + if ( !$img->allowInlineDisplay() && $img->exists() ) { + return $this->makeKnownLinkObj( $nt ); + } + + $url = $img->getViewURL(); + $error = $prefix = $postfix = ''; + + wfDebug( "makeImageLinkObj: '$width'x'$height'\n" ); + + if ( 'center' == $align ) + { + $prefix = '
    '; + $postfix = '
    '; + $align = 'none'; + } + + if ( $thumb || $framed ) { + + # Create a thumbnail. Alignment depends on language + # writing direction, # right aligned for left-to-right- + # languages ("Western languages"), left-aligned + # for right-to-left-languages ("Semitic languages") + # + # If thumbnail width has not been provided, it is set + # to the default user option as specified in Language*.php + if ( $align == '' ) { + $align = $wgContLang->isRTL() ? 'left' : 'right'; + } + + + if ( $width === false ) { + $wopt = $wgUser->getOption( 'thumbsize' ); + + if( !isset( $wgThumbLimits[$wopt] ) ) { + $wopt = User::getDefaultOption( 'thumbsize' ); + } + + $width = min( $img->getWidth(), $wgThumbLimits[$wopt] ); + } + + return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $width, $height, $framed, $manual_thumb ).$postfix; + } + + if ( $width && $img->exists() ) { + + # Create a resized image, without the additional thumbnail + # features + + if ( $height == false ) + $height = -1; + if ( $manual_thumb == '') { + $thumb = $img->getThumbnail( $width, $height, $wgGenerateThumbnailOnParse ); + if ( $thumb ) { + // In most cases, $width = $thumb->width or $height = $thumb->height. + // If not, we're scaling the image larger than it can be scaled, + // so we send to the browser a smaller thumbnail, and let the client do the scaling. + + if ($height != -1 && $width > $thumb->width * $height / $thumb->height) { + // $height is the limiting factor, not $width + // set $width to the largest it can be, such that the resulting + // scaled height is at most $height + $width = floor($thumb->width * $height / $thumb->height); + } + $height = round($thumb->height * $width / $thumb->width); + + wfDebug( "makeImageLinkObj: client-size set to '$width x $height'\n" ); + $url = $thumb->getUrl(); + } else { + $error = htmlspecialchars( $img->getLastError() ); + } + } + } else { + $width = $img->width; + $height = $img->height; + } + + wfDebug( "makeImageLinkObj2: '$width'x'$height'\n" ); + $u = $nt->escapeLocalURL(); + if ( $error ) { + $s = $error; + } elseif ( $url == '' ) { + $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); + //$s .= "
    {$alt}
    {$url}
    \n"; + } else { + $s = '' . + ''.$alt.''; + } + if ( '' != $align ) { + $s = "
    {$s}
    "; + } + return str_replace("\n", ' ',$prefix.$s.$postfix); + } + + /** + * Make HTML for a thumbnail including image, border and caption + * $img is an Image object + */ + function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) { + global $wgStylePath, $wgContLang, $wgGenerateThumbnailOnParse; + $url = $img->getViewURL(); + $thumbUrl = ''; + $error = ''; + + $width = $height = 0; + if ( $img->exists() ) { + $width = $img->getWidth(); + $height = $img->getHeight(); + } + if ( 0 == $width || 0 == $height ) { + $width = $height = 180; + } + if ( $boxwidth == 0 ) { + $boxwidth = 180; + } + if ( $framed ) { + // Use image dimensions, don't scale + $boxwidth = $width; + $boxheight = $height; + $thumbUrl = $url; + } else { + if ( $boxheight === false ) + $boxheight = -1; + if ( '' == $manual_thumb ) { + $thumb = $img->getThumbnail( $boxwidth, $boxheight, $wgGenerateThumbnailOnParse ); + if ( $thumb ) { + $thumbUrl = $thumb->getUrl(); + $boxwidth = $thumb->width; + $boxheight = $thumb->height; + } else { + $error = $img->getLastError(); + } + } + } + $oboxwidth = $boxwidth + 2; + + if ( $manual_thumb != '' ) # Use manually specified thumbnail + { + $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); #new Title ( $manual_thumb ) ; + if( $manual_title ) { + $manual_img = new Image( $manual_title ); + $thumbUrl = $manual_img->getViewURL(); + if ( $manual_img->exists() ) + { + $width = $manual_img->getWidth(); + $height = $manual_img->getHeight(); + $boxwidth = $width ; + $boxheight = $height ; + $oboxwidth = $boxwidth + 2 ; + } + } + } + + $u = $img->getEscapeLocalURL(); + + $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); + $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; + $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; + + $s = "
    "; + if( $thumbUrl == '' ) { + // Couldn't generate thumbnail? Scale the image client-side. + $thumbUrl = $url; + } + if ( $error ) { + $s .= htmlspecialchars( $error ); + $zoomicon = ''; + } elseif( !$img->exists() ) { + $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); + $zoomicon = ''; + } else { + $s .= ''. + ''.$alt.''; + if ( $framed ) { + $zoomicon=""; + } else { + $zoomicon = '
    '. + ''. + ''.$more.'
    '; + } + } + $s .= '
    '.$zoomicon.$label."
    "; + return str_replace("\n", ' ', $s); + } + + /** + * Pass a title object, not a title string + */ + function makeBrokenImageLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + # Fail gracefully + if ( ! isset($nt) ) { + # throw new MWException(); + return "{$prefix}{$text}{$trail}"; + } + + $fname = 'Linker::makeBrokenImageLinkObj'; + wfProfileIn( $fname ); + + $q = 'wpDestFile=' . urlencode( $nt->getDBkey() ); + if ( '' != $query ) { + $q .= "&$query"; + } + $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $url = $uploadTitle->escapeLocalURL( $q ); + + if ( '' == $text ) { + $text = htmlspecialchars( $nt->getPrefixedText() ); + } + $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $s = "{$prefix}{$text}{$inside}{$trail}"; + + wfProfileOut( $fname ); + return $s; + } + + /** @todo document */ + function makeMediaLink( $name, /* wtf?! */ $url, $alt = '' ) { + $nt = Title::makeTitleSafe( NS_IMAGE, $name ); + return $this->makeMediaLinkObj( $nt, $alt ); + } + + /** + * Create a direct link to a given uploaded file. + * + * @param $title Title object. + * @param $text String: pre-sanitized HTML + * @param $nourl Boolean: Mask absolute URLs, so the parser doesn't + * linkify them (it is currently not context-aware) + * @return string HTML + * + * @public + * @todo Handle invalid or missing images better. + */ + function makeMediaLinkObj( $title, $text = '' ) { + if( is_null( $title ) ) { + ### HOTFIX. Instead of breaking, return empty string. + return $text; + } else { + $name = $title->getDBKey(); + $img = new Image( $title ); + if( $img->exists() ) { + $url = $img->getURL(); + $class = 'internal'; + } else { + $upload = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) ); + $class = 'new'; + } + $alt = htmlspecialchars( $title->getText() ); + if( $text == '' ) { + $text = $alt; + } + $u = htmlspecialchars( $url ); + return "{$text}"; + } + } + + /** @todo document */ + function specialLink( $name, $key = '' ) { + global $wgContLang; + + if ( '' == $key ) { $key = strtolower( $name ); } + $pn = $wgContLang->ucfirst( $name ); + return $this->makeKnownLink( $wgContLang->specialPage( $pn ), + wfMsg( $key ) ); + } + + /** @todo document */ + function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) { + $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); + global $wgNoFollowLinks, $wgNoFollowNsExceptions; + if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) { + $style .= ' rel="nofollow"'; + } + $url = htmlspecialchars( $url ); + if( $escape ) { + $text = htmlspecialchars( $text ); + } + return ''.$text.''; + } + + /** + * Make user link (or user contributions for unregistered users) + * @param $userId Integer: user id in database. + * @param $userText String: user name in database + * @return string HTML fragment + * @private + */ + function userLink( $userId, $userText ) { + $encName = htmlspecialchars( $userText ); + if( $userId == 0 ) { + $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); + return $this->makeKnownLinkObj( $contribsPage, + $encName, 'target=' . urlencode( $userText ) ); + } else { + $userPage = Title::makeTitle( NS_USER, $userText ); + return $this->makeLinkObj( $userPage, $encName ); + } + } + + /** + * @param $userId Integer: user id in database. + * @param $userText String: user name in database. + * @return string HTML fragment with talk and/or block links + * @private + */ + function userToolLinks( $userId, $userText ) { + global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; + $talkable = !( $wgDisableAnonTalk && 0 == $userId ); + $blockable = ( $wgSysopUserBans || 0 == $userId ); + + $items = array(); + if( $talkable ) { + $items[] = $this->userTalkLink( $userId, $userText ); + } + if( $userId ) { + $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); + $items[] = $this->makeKnownLinkObj( $contribsPage, + wfMsgHtml( 'contribslink' ), 'target=' . urlencode( $userText ) ); + } + if( $blockable && $wgUser->isAllowed( 'block' ) ) { + $items[] = $this->blockLink( $userId, $userText ); + } + + if( $items ) { + return ' (' . implode( ' | ', $items ) . ')'; + } else { + return ''; + } + } + + /** + * @param $userId Integer: user id in database. + * @param $userText String: user name in database. + * @return string HTML fragment with user talk link + * @private + */ + function userTalkLink( $userId, $userText ) { + global $wgLang; + $talkname = $wgLang->getNsText( NS_TALK ); # use the shorter name + + $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); + $userTalkLink = $this->makeLinkObj( $userTalkPage, $talkname ); + return $userTalkLink; + } + + /** + * @param $userId Integer: userid + * @param $userText String: user name in database. + * @return string HTML fragment with block link + * @private + */ + function blockLink( $userId, $userText ) { + $blockPage = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $blockLink = $this->makeKnownLinkObj( $blockPage, + wfMsgHtml( 'blocklink' ), 'ip=' . urlencode( $userText ) ); + return $blockLink; + } + + /** + * Generate a user link if the current user is allowed to view it + * @param $rev Revision object. + * @return string HTML + */ + function revUserLink( $rev ) { + if( $rev->userCan( Revision::DELETED_USER ) ) { + $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ); + } else { + $link = wfMsgHtml( 'rev-deleted-user' ); + } + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + return '' . $link . ''; + } + return $link; + } + + /** + * Generate a user tool link cluster if the current user is allowed to view it + * @param $rev Revision object. + * @return string HTML + */ + function revUserTools( $rev ) { + if( $rev->userCan( Revision::DELETED_USER ) ) { + $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) . + ' ' . + $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() ); + } else { + $link = wfMsgHtml( 'rev-deleted-user' ); + } + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + return '' . $link . ''; + } + return $link; + } + + /** + * This function is called by all recent changes variants, by the page history, + * and by the user contributions list. It is responsible for formatting edit + * comments. It escapes any HTML in the comment, but adds some CSS to format + * auto-generated comments (from section editing) and formats [[wikilinks]]. + * + * The $title parameter must be a title OBJECT. It is used to generate a + * direct link to the section in the autocomment. + * @author Erik Moeller + * + * Note: there's not always a title to pass to this function. + * Since you can't set a default parameter for a reference, I've turned it + * temporarily to a value pass. Should be adjusted further. --brion + */ + function formatComment($comment, $title = NULL) { + $fname = 'Linker::formatComment'; + wfProfileIn( $fname ); + + global $wgContLang; + $comment = str_replace( "\n", " ", $comment ); + $comment = htmlspecialchars( $comment ); + + # The pattern for autogen comments is / * foo * /, which makes for + # some nasty regex. + # We look for all comments, match any text before and after the comment, + # add a separator where needed and format the comment itself with CSS + while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) { + $pre=$match[1]; + $auto=$match[2]; + $post=$match[3]; + $link=''; + if( $title ) { + $section = $auto; + + # Generate a valid anchor name from the section title. + # Hackish, but should generally work - we strip wiki + # syntax, including the magic [[: that is used to + # "link rather than show" in case of images and + # interlanguage links. + $section = str_replace( '[[:', '', $section ); + $section = str_replace( '[[', '', $section ); + $section = str_replace( ']]', '', $section ); + $sectionTitle = wfClone( $title ); + $sectionTitle->mFragment = $section; + $link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) ); + } + $sep='-'; + $auto=$link.$auto; + if($pre) { $auto = $sep.' '.$auto; } + if($post) { $auto .= ' '.$sep; } + $auto=''.$auto.''; + $comment=$pre.$auto.$post; + } + + # format regular and media links - all other wiki formatting + # is ignored + $medians = $wgContLang->getNsText( NS_MEDIA ) . ':'; + 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]; + } + 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 { + $trail = ""; + } + $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; + if ($match[1][0] == ':') + $match[1] = substr($match[1], 1); + $thelink = $this->makeLink( $match[1], $text, "", $trail ); + } + $comment = preg_replace( $linkRegexp, wfRegexReplacement( $thelink ), $comment, 1 ); + } + wfProfileOut( $fname ); + return $comment; + } + + /** + * Wrap a comment in standard punctuation and formatting if + * it's non-empty, otherwise return empty string. + * + * @param $comment String: the comment. + * @param $title Title object. + * + * @return string + */ + function commentBlock( $comment, $title = NULL ) { + // '*' used to be the comment inserted by the software way back + // in antiquity in case none was provided, here for backwards + // compatability, acc. to brion -ævar + if( $comment == '' || $comment == '*' ) { + return ''; + } else { + $formatted = $this->formatComment( $comment, $title ); + return " ($formatted)"; + } + } + + /** + * Wrap and format the given revision's comment block, if the current + * user is allowed to view it. + * @param $rev Revision object. + * @return string HTML + */ + function revComment( $rev ) { + if( $rev->userCan( Revision::DELETED_COMMENT ) ) { + $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle() ); + } else { + $block = " " . + wfMsgHtml( 'rev-deleted-comment' ) . ""; + } + if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + return " $block"; + } + return $block; + } + + /** @todo document */ + function tocIndent() { + return "\n
      "; + } + + /** @todo document */ + function tocUnindent($level) { + return "\n" . str_repeat( "
    \n\n", $level>0 ? $level : 0 ); + } + + /** + * parameter level defines if we are on an indentation level + */ + function tocLine( $anchor, $tocline, $tocnumber, $level ) { + return "\n
  • ' . + $tocnumber . ' ' . + $tocline . ''; + } + + /** @todo document */ + function tocLineEnd() { + return "
  • \n"; + } + + /** @todo document */ + function tocList($toc) { + global $wgJsMimeType; + $title = wfMsgForContent('toc') ; + return + '
    ' + . '

    ' . $title . "

    \n" + . $toc + # no trailing newline, script should not be wrapped in a + # paragraph + . "\n
    " + . '\n"; + } + + /** @todo document */ + function editSectionLinkForOther( $title, $section ) { + global $wgContLang; + + $title = Title::newFromText( $title ); + $editurl = '§ion='.$section; + $url = $this->makeKnownLinkObj( $title, wfMsg('editsection'), 'action=edit'.$editurl ); + + if( $wgContLang->isRTL() ) { + $farside = 'left'; + $nearside = 'right'; + } else { + $farside = 'right'; + $nearside = 'left'; + } + return "
    [".$url."]
    "; + + } + + /** + * @param $title Title object. + * @param $section Integer: section number. + * @param $hint Link String: title, or default if omitted or empty + */ + function editSectionLink( $nt, $section, $hint='' ) { + global $wgContLang; + + $editurl = '§ion='.$section; + $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"'; + $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint ); + + if( $wgContLang->isRTL() ) { + $farside = 'left'; + $nearside = 'right'; + } else { + $farside = 'right'; + $nearside = 'left'; + } + return "
    [".$url."]
    "; + } + + /** + * Split a link trail, return the "inside" portion and the remainder of the trail + * as a two-element array + * + * @static + */ + function splitTrail( $trail ) { + static $regex = false; + if ( $regex === false ) { + global $wgContLang; + $regex = $wgContLang->linkTrail(); + } + $inside = ''; + if ( '' != $trail ) { + if ( preg_match( $regex, $trail, $m ) ) { + $inside = $m[1]; + $trail = $m[2]; + } + } + return array( $inside, $trail ); + } + +} +?> diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php new file mode 100644 index 00000000..9e25bf07 --- /dev/null +++ b/includes/LinksUpdate.php @@ -0,0 +1,601 @@ +mOptions = array(); + } else { + $this->mOptions = array( 'FOR UPDATE' ); + } + $this->mDb =& wfGetDB( DB_MASTER ); + + if ( !is_object( $title ) ) { + throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " . + "Please see Article::editUpdates() for an invocation example.\n" ); + } + $this->mTitle = $title; + $this->mId = $title->getArticleID(); + + $this->mLinks = $parserOutput->getLinks(); + $this->mImages = $parserOutput->getImages(); + $this->mTemplates = $parserOutput->getTemplates(); + $this->mExternals = $parserOutput->getExternalLinks(); + $this->mCategories = $parserOutput->getCategories(); + + # Convert the format of the interlanguage links + # I didn't want to change it in the ParserOutput, because that array is passed all + # the way back to the skin, so either a skin API break would be required, or an + # inefficient back-conversion. + $ill = $parserOutput->getLanguageLinks(); + $this->mInterlangs = array(); + foreach ( $ill as $link ) { + list( $key, $title ) = explode( ':', $link, 2 ); + $this->mInterlangs[$key] = $title; + } + + $this->mRecursive = $recursive; + } + + /** + * Update link tables with outgoing links from an updated article + */ + function doUpdate() { + global $wgUseDumbLinkUpdate; + if ( $wgUseDumbLinkUpdate ) { + $this->doDumbUpdate(); + } else { + $this->doIncrementalUpdate(); + } + } + + function doIncrementalUpdate() { + $fname = 'LinksUpdate::doIncrementalUpdate'; + wfProfileIn( $fname ); + + # Page links + $existing = $this->getExistingLinks(); + $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ), + $this->getLinkInsertions( $existing ) ); + + # Image links + $existing = $this->getExistingImages(); + $this->incrTableUpdate( 'imagelinks', 'il', $this->getImageDeletions( $existing ), + $this->getImageInsertions( $existing ) ); + + # Invalidate all image description pages which had links added or removed + $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing ); + $this->invalidateImageDescriptions( $imageUpdates ); + + # External links + $existing = $this->getExistingExternals(); + $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ), + $this->getExternalInsertions( $existing ) ); + + # Language links + $existing = $this->getExistingInterlangs(); + $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ), + $this->getInterlangInsertions( $existing ) ); + + # Template links + $existing = $this->getExistingTemplates(); + $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ), + $this->getTemplateInsertions( $existing ) ); + + # Category links + $existing = $this->getExistingCategories(); + $this->incrTableUpdate( 'categorylinks', 'cl', $this->getCategoryDeletions( $existing ), + $this->getCategoryInsertions( $existing ) ); + + # Invalidate all categories which were added, deleted or changed (set symmetric difference) + $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing ); + $this->invalidateCategories( $categoryUpdates ); + + # Refresh links of all pages including this page + # This will be in a separate transaction + if ( $this->mRecursive ) { + $this->queueRecursiveJobs(); + } + + wfProfileOut( $fname ); + } + + /** + * Link update which clears the previous entries and inserts new ones + * May be slower or faster depending on level of lock contention and write speed of DB + * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php + */ + function doDumbUpdate() { + $fname = 'LinksUpdate::doDumbUpdate'; + wfProfileIn( $fname ); + + # Refresh category pages and image description pages + $existing = $this->getExistingCategories(); + $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing ); + $existing = $this->getExistingImages(); + $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing ); + + $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' ); + $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' ); + $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' ); + $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' ); + $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' ); + $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(), 'll_from' ); + + # Update the cache of all the category pages and image description pages which were changed + $this->invalidateCategories( $categoryUpdates ); + $this->invalidateImageDescriptions( $imageUpdates ); + + # Refresh links of all pages including this page + # This will be in a separate transaction + if ( $this->mRecursive ) { + $this->queueRecursiveJobs(); + } + + wfProfileOut( $fname ); + } + + function queueRecursiveJobs() { + wfProfileIn( __METHOD__ ); + + $batchSize = 100; + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'page_id=tl_from', + 'tl_namespace' => $this->mTitle->getNamespace(), + 'tl_title' => $this->mTitle->getDBkey() + ), __METHOD__ + ); + + $done = false; + while ( !$done ) { + $jobs = array(); + for ( $i = 0; $i < $batchSize; $i++ ) { + $row = $dbr->fetchObject( $res ); + if ( !$row ) { + $done = true; + break; + } + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $jobs[] = Job::factory( 'refreshLinks', $title ); + } + Job::batchInsert( $jobs ); + } + $dbr->freeResult( $res ); + wfProfileOut( __METHOD__ ); + } + + /** + * Invalidate the cache of a list of pages from a single namespace + * + * @param integer $namespace + * @param array $dbkeys + */ + function invalidatePages( $namespace, $dbkeys ) { + $fname = 'LinksUpdate::invalidatePages'; + + if ( !count( $dbkeys ) ) { + return; + } + + /** + * Determine which pages need to be updated + * This is necessary to prevent the job queue from smashing the DB with + * large numbers of concurrent invalidations of the same page + */ + $now = $this->mDb->timestamp(); + $ids = array(); + $res = $this->mDb->select( 'page', array( 'page_id' ), + array( + 'page_namespace' => $namespace, + 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), $fname + ); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $ids[] = $row->page_id; + } + if ( !count( $ids ) ) { + return; + } + + /** + * Do the update + * We still need the page_touched condition, in case the row has changed since + * the non-locking select above. + */ + $this->mDb->update( 'page', array( 'page_touched' => $now ), + array( + 'page_id IN (' . $this->mDb->makeList( $ids ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), $fname + ); + } + + function invalidateCategories( $cats ) { + $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) ); + } + + function invalidateImageDescriptions( $images ) { + $this->invalidatePages( NS_IMAGE, array_keys( $images ) ); + } + + function dumbTableUpdate( $table, $insertions, $fromField ) { + $fname = 'LinksUpdate::dumbTableUpdate'; + $this->mDb->delete( $table, array( $fromField => $this->mId ), $fname ); + if ( count( $insertions ) ) { + # The link array was constructed without FOR UPDATE, so there may be collisions + # This may cause minor link table inconsistencies, which is better than + # crippling the site with lock contention. + $this->mDb->insert( $table, $insertions, $fname, array( 'IGNORE' ) ); + } + } + + /** + * Make a WHERE clause from a 2-d NS/dbkey array + * + * @param array $arr 2-d array indexed by namespace and DB key + * @param string $prefix Field name prefix, without the underscore + */ + function makeWhereFrom2d( &$arr, $prefix ) { + $lb = new LinkBatch; + $lb->setArray( $arr ); + return $lb->constructSet( $prefix, $this->mDb ); + } + + /** + * Update a table by doing a delete query then an insert query + * @private + */ + function incrTableUpdate( $table, $prefix, $deletions, $insertions ) { + $fname = 'LinksUpdate::incrTableUpdate'; + $where = array( "{$prefix}_from" => $this->mId ); + if ( $table == 'pagelinks' || $table == 'templatelinks' ) { + $clause = $this->makeWhereFrom2d( $deletions, $prefix ); + if ( $clause ) { + $where[] = $clause; + } else { + $where = false; + } + } else { + if ( $table == 'langlinks' ) { + $toField = 'll_lang'; + } else { + $toField = $prefix . '_to'; + } + if ( count( $deletions ) ) { + $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')'; + } else { + $where = false; + } + } + if ( $where ) { + $this->mDb->delete( $table, $where, $fname ); + } + if ( count( $insertions ) ) { + $this->mDb->insert( $table, $insertions, $fname, 'IGNORE' ); + } + } + + + /** + * Get an array of pagelinks insertions for passing to the DB + * Skips the titles specified by the 2-D array $existing + * @private + */ + function getLinkInsertions( $existing = array() ) { + $arr = array(); + foreach( $this->mLinks as $ns => $dbkeys ) { + # array_diff_key() was introduced in PHP 5.1, there is a compatibility function + # in GlobalFunctions.php + $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = array( + 'pl_from' => $this->mId, + 'pl_namespace' => $ns, + 'pl_title' => $dbk + ); + } + } + return $arr; + } + + /** + * Get an array of template insertions. Like getLinkInsertions() + * @private + */ + function getTemplateInsertions( $existing = array() ) { + $arr = array(); + foreach( $this->mTemplates as $ns => $dbkeys ) { + $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = array( + 'tl_from' => $this->mId, + 'tl_namespace' => $ns, + 'tl_title' => $dbk + ); + } + } + return $arr; + } + + /** + * Get an array of image insertions + * Skips the names specified in $existing + * @private + */ + function getImageInsertions( $existing = array() ) { + $arr = array(); + $diffs = array_diff_key( $this->mImages, $existing ); + foreach( $diffs as $iname => $dummy ) { + $arr[] = array( + 'il_from' => $this->mId, + 'il_to' => $iname + ); + } + return $arr; + } + + /** + * Get an array of externallinks insertions. Skips the names specified in $existing + * @private + */ + function getExternalInsertions( $existing = array() ) { + $arr = array(); + $diffs = array_diff_key( $this->mExternals, $existing ); + foreach( $diffs as $url => $dummy ) { + $arr[] = array( + 'el_from' => $this->mId, + 'el_to' => $url, + 'el_index' => wfMakeUrlIndex( $url ), + ); + } + return $arr; + } + + /** + * Get an array of category insertions + * @param array $existing Array mapping existing category names to sort keys. If both + * match a link in $this, the link will be omitted from the output + * @private + */ + function getCategoryInsertions( $existing = array() ) { + $diffs = array_diff_assoc( $this->mCategories, $existing ); + $arr = array(); + foreach ( $diffs as $name => $sortkey ) { + $arr[] = array( + 'cl_from' => $this->mId, + 'cl_to' => $name, + 'cl_sortkey' => $sortkey, + 'cl_timestamp' => $this->mDb->timestamp() + ); + } + return $arr; + } + + /** + * Get an array of interlanguage link insertions + * @param array $existing Array mapping existing language codes to titles + * @private + */ + function getInterlangInsertions( $existing = array() ) { + $diffs = array_diff_assoc( $this->mInterlangs, $existing ); + $arr = array(); + foreach( $diffs as $lang => $title ) { + $arr[] = array( + 'll_from' => $this->mId, + 'll_lang' => $lang, + 'll_title' => $title + ); + } + return $arr; + } + + /** + * Given an array of existing links, returns those links which are not in $this + * and thus should be deleted. + * @private + */ + function getLinkDeletions( $existing ) { + $del = array(); + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mLinks[$ns] ) ) { + $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] ); + } else { + $del[$ns] = $existing[$ns]; + } + } + return $del; + } + + /** + * Given an array of existing templates, returns those templates which are not in $this + * and thus should be deleted. + * @private + */ + function getTemplateDeletions( $existing ) { + $del = array(); + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mTemplates[$ns] ) ) { + $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] ); + } else { + $del[$ns] = $existing[$ns]; + } + } + return $del; + } + + /** + * Given an array of existing images, returns those images which are not in $this + * and thus should be deleted. + * @private + */ + function getImageDeletions( $existing ) { + return array_diff_key( $existing, $this->mImages ); + } + + /** + * Given an array of existing external links, returns those links which are not + * in $this and thus should be deleted. + * @private + */ + function getExternalDeletions( $existing ) { + return array_diff_key( $existing, $this->mExternals ); + } + + /** + * Given an array of existing categories, returns those categories which are not in $this + * and thus should be deleted. + * @private + */ + function getCategoryDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mCategories ); + } + + /** + * Given an array of existing interlanguage links, returns those links which are not + * in $this and thus should be deleted. + * @private + */ + function getInterlangDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mInterlangs ); + } + + /** + * Get an array of existing links, as a 2-D array + * @private + */ + function getExistingLinks() { + $fname = 'LinksUpdate::getExistingLinks'; + $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ), + array( 'pl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + if ( !isset( $arr[$row->pl_namespace] ) ) { + $arr[$row->pl_namespace] = array(); + } + $arr[$row->pl_namespace][$row->pl_title] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing templates, as a 2-D array + * @private + */ + function getExistingTemplates() { + $fname = 'LinksUpdate::getExistingTemplates'; + $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + if ( !isset( $arr[$row->tl_namespace] ) ) { + $arr[$row->tl_namespace] = array(); + } + $arr[$row->tl_namespace][$row->tl_title] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing images, image names in the keys + * @private + */ + function getExistingImages() { + $fname = 'LinksUpdate::getExistingImages'; + $res = $this->mDb->select( 'imagelinks', array( 'il_to' ), + array( 'il_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->il_to] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing external links, URLs in the keys + * @private + */ + function getExistingExternals() { + $fname = 'LinksUpdate::getExistingExternals'; + $res = $this->mDb->select( 'externallinks', array( 'el_to' ), + array( 'el_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->el_to] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing categories, with the name in the key and sort key in the value. + * @private + */ + function getExistingCategories() { + $fname = 'LinksUpdate::getExistingCategories'; + $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ), + array( 'cl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->cl_to] = $row->cl_sortkey; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing interlanguage links, with the language code in the key and the + * title in the value. + * @private + */ + function getExistingInterlangs() { + $fname = 'LinksUpdate::getExistingInterlangs'; + $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ), + array( 'll_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->ll_lang] = $row->ll_title; + } + return $arr; + } +} +?> diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php new file mode 100644 index 00000000..f985a7b4 --- /dev/null +++ b/includes/LoadBalancer.php @@ -0,0 +1,666 @@ +mServers = array(); + $this->mConnections = array(); + $this->mFailFunction = false; + $this->mReadIndex = -1; + $this->mForce = -1; + $this->mLastIndex = -1; + $this->mErrorConnection = false; + $this->mAllowLag = false; + } + + function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) + { + $lb = new LoadBalancer; + $lb->initialise( $servers, $failFunction, $waitTimeout ); + return $lb; + } + + function initialise( $servers, $failFunction = false, $waitTimeout = 10 ) + { + $this->mServers = $servers; + $this->mFailFunction = $failFunction; + $this->mReadIndex = -1; + $this->mWriteIndex = -1; + $this->mForce = -1; + $this->mConnections = array(); + $this->mLastIndex = 1; + $this->mLoads = array(); + $this->mWaitForFile = false; + $this->mWaitForPos = false; + $this->mWaitTimeout = $waitTimeout; + $this->mLaggedSlaveMode = false; + + foreach( $servers as $i => $server ) { + $this->mLoads[$i] = $server['load']; + if ( isset( $server['groupLoads'] ) ) { + foreach ( $server['groupLoads'] as $group => $ratio ) { + if ( !isset( $this->mGroupLoads[$group] ) ) { + $this->mGroupLoads[$group] = array(); + } + $this->mGroupLoads[$group][$i] = $ratio; + } + } + } + } + + /** + * Given an array of non-normalised probabilities, this function will select + * an element and return the appropriate key + */ + function pickRandom( $weights ) + { + if ( !is_array( $weights ) || count( $weights ) == 0 ) { + return false; + } + + $sum = array_sum( $weights ); + if ( $sum == 0 ) { + # No loads on any of them + # In previous versions, this triggered an unweighted random selection, + # but this feature has been removed as of April 2006 to allow for strict + # separation of query groups. + return false; + } + $max = mt_getrandmax(); + $rand = mt_rand(0, $max) / $max * $sum; + + $sum = 0; + foreach ( $weights as $i => $w ) { + $sum += $w; + if ( $sum >= $rand ) { + break; + } + } + return $i; + } + + function getRandomNonLagged( $loads ) { + # Unset excessively lagged servers + $lags = $this->getLagTimes(); + foreach ( $lags as $i => $lag ) { + if ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { + unset( $loads[$i] ); + } + } + + # Find out if all the slaves with non-zero load are lagged + $sum = 0; + foreach ( $loads as $load ) { + $sum += $load; + } + if ( $sum == 0 ) { + # No appropriate DB servers except maybe the master and some slaves with zero load + # Do NOT use the master + # Instead, this function will return false, triggering read-only mode, + # and a lagged slave will be used instead. + return false; + } + + if ( count( $loads ) == 0 ) { + return false; + } + + #wfDebugLog( 'connect', var_export( $loads, true ) ); + + # Return a random representative of the remainder + return $this->pickRandom( $loads ); + } + + /** + * Get the index of the reader connection, which may be a slave + * This takes into account load ratios and lag times. It should + * always return a consistent index during a given invocation + * + * Side effect: opens connections to databases + */ + function getReaderIndex() { + global $wgReadOnly, $wgDBClusterTimeout; + + $fname = 'LoadBalancer::getReaderIndex'; + wfProfileIn( $fname ); + + $i = false; + if ( $this->mForce >= 0 ) { + $i = $this->mForce; + } else { + if ( $this->mReadIndex >= 0 ) { + $i = $this->mReadIndex; + } else { + # $loads is $this->mLoads except with elements knocked out if they + # don't work + $loads = $this->mLoads; + $done = false; + $totalElapsed = 0; + do { + if ( $wgReadOnly or $this->mAllowLagged ) { + $i = $this->pickRandom( $loads ); + } else { + $i = $this->getRandomNonLagged( $loads ); + if ( $i === false && count( $loads ) != 0 ) { + # All slaves lagged. Switch to read-only mode + $wgReadOnly = wfMsgNoDB( 'readonly_lag' ); + $i = $this->pickRandom( $loads ); + } + } + $serverIndex = $i; + if ( $i !== false ) { + wfDebugLog( 'connect', "$fname: Using reader #$i: {$this->mServers[$i]['host']}...\n" ); + $this->openConnection( $i ); + + if ( !$this->isOpen( $i ) ) { + wfDebug( "$fname: Failed\n" ); + unset( $loads[$i] ); + $sleepTime = 0; + } else { + $status = $this->mConnections[$i]->getStatus("Thread%"); + if ( isset( $this->mServers[$i]['max threads'] ) && + $status['Threads_running'] > $this->mServers[$i]['max threads'] ) + { + # 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 = AVG_STATUS_POLL * $status['Threads_connected']; + + # If we reach the timeout and exit the loop, don't use it + $i = false; + } else { + $done = true; + $sleepTime = 0; + } + } + } else { + $sleepTime = 500000; + } + if ( $sleepTime ) { + $totalElapsed += $sleepTime; + $x = "{$this->mServers[$serverIndex]['host']} [$serverIndex]"; + wfProfileIn( "$fname-sleep $x" ); + usleep( $sleepTime ); + wfProfileOut( "$fname-sleep $x" ); + } + } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $wgDBClusterTimeout ); + + if ( $totalElapsed / 1e6 >= $wgDBClusterTimeout ) { + $this->mErrorConnection = false; + $this->mLastError = 'All servers busy'; + } + + if ( $i !== false && $this->isOpen( $i ) ) { + # Wait for the session master pos for a short time + if ( $this->mWaitForFile ) { + if ( !$this->doWait( $i ) ) { + $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos(); + } + } + if ( $i !== false ) { + $this->mReadIndex = $i; + } + } else { + $i = false; + } + } + } + wfProfileOut( $fname ); + return $i; + } + + /** + * Get a random server to use in a query group + */ + function getGroupIndex( $group ) { + if ( isset( $this->mGroupLoads[$group] ) ) { + $i = $this->pickRandom( $this->mGroupLoads[$group] ); + } else { + $i = false; + } + wfDebug( "Query group $group => $i\n" ); + return $i; + } + + /** + * Set the master wait position + * If a DB_SLAVE connection has been opened already, waits + * Otherwise sets a variable telling it to wait if such a connection is opened + */ + function waitFor( $file, $pos ) { + $fname = 'LoadBalancer::waitFor'; + wfProfileIn( $fname ); + + wfDebug( "User master pos: $file $pos\n" ); + $this->mWaitForFile = false; + $this->mWaitForPos = false; + + if ( count( $this->mServers ) > 1 ) { + $this->mWaitForFile = $file; + $this->mWaitForPos = $pos; + $i = $this->mReadIndex; + + if ( $i > 0 ) { + if ( !$this->doWait( $i ) ) { + $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos(); + $this->mLaggedSlaveMode = true; + } + } + } + wfProfileOut( $fname ); + } + + /** + * Wait for a given slave to catch up to the master pos stored in $this + */ + function doWait( $index ) { + global $wgMemc; + + $retVal = false; + + # Debugging hacks + if ( isset( $this->mServers[$index]['lagged slave'] ) ) { + return false; + } elseif ( isset( $this->mServers[$index]['fake slave'] ) ) { + return true; + } + + $key = 'masterpos:' . $index; + $memcPos = $wgMemc->get( $key ); + if ( $memcPos ) { + list( $file, $pos ) = explode( ' ', $memcPos ); + # If the saved position is later than the requested position, return now + if ( $file == $this->mWaitForFile && $this->mWaitForPos <= $pos ) { + $retVal = true; + } + } + + if ( !$retVal && $this->isOpen( $index ) ) { + $conn =& $this->mConnections[$index]; + wfDebug( "Waiting for slave #$index to catch up...\n" ); + $result = $conn->masterPosWait( $this->mWaitForFile, $this->mWaitForPos, $this->mWaitTimeout ); + + if ( $result == -1 || is_null( $result ) ) { + # Timed out waiting for slave, use master instead + wfDebug( "Timed out waiting for slave #$index pos {$this->mWaitForFile} {$this->mWaitForPos}\n" ); + $retVal = false; + } else { + $retVal = true; + wfDebug( "Done\n" ); + } + } + return $retVal; + } + + /** + * Get a connection by index + */ + function &getConnection( $i, $fail = true, $groups = array() ) + { + global $wgDBtype; + $fname = 'LoadBalancer::getConnection'; + wfProfileIn( $fname ); + + + # Query groups + if ( !is_array( $groups ) ) { + $groupIndex = $this->getGroupIndex( $groups, $i ); + if ( $groupIndex !== false ) { + $i = $groupIndex; + } + } else { + foreach ( $groups as $group ) { + $groupIndex = $this->getGroupIndex( $group, $i ); + if ( $groupIndex !== false ) { + $i = $groupIndex; + break; + } + } + } + + # For now, only go through all this for mysql databases + if ($wgDBtype != 'mysql') { + $i = $this->getWriterIndex(); + } + # Operation-based index + elseif ( $i == DB_SLAVE ) { + $i = $this->getReaderIndex(); + } elseif ( $i == DB_MASTER ) { + $i = $this->getWriterIndex(); + } elseif ( $i == DB_LAST ) { + # Just use $this->mLastIndex, which should already be set + $i = $this->mLastIndex; + if ( $i === -1 ) { + # Oh dear, not set, best to use the writer for safety + wfDebug( "Warning: DB_LAST used when there was no previous index\n" ); + $i = $this->getWriterIndex(); + } + } + # Couldn't find a working server in getReaderIndex()? + if ( $i === false ) { + $this->reportConnectionError( $this->mErrorConnection ); + } + # Now we have an explicit index into the servers array + $this->openConnection( $i, $fail ); + + wfProfileOut( $fname ); + return $this->mConnections[$i]; + } + + /** + * Open a connection to the server given by the specified index + * Index must be an actual index into the array + * Returns success + * @access private + */ + function openConnection( $i, $fail = false ) { + $fname = 'LoadBalancer::openConnection'; + wfProfileIn( $fname ); + $success = true; + + if ( !$this->isOpen( $i ) ) { + $this->mConnections[$i] = $this->reallyOpenConnection( $this->mServers[$i] ); + } + + if ( !$this->isOpen( $i ) ) { + wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" ); + if ( $fail ) { + $this->reportConnectionError( $this->mConnections[$i] ); + } + $this->mErrorConnection = $this->mConnections[$i]; + $this->mConnections[$i] = false; + $success = false; + } + $this->mLastIndex = $i; + wfProfileOut( $fname ); + return $success; + } + + /** + * Test if the specified index represents an open connection + * @access private + */ + function isOpen( $index ) { + if( !is_integer( $index ) ) { + return false; + } + if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) && + $this->mConnections[$index]->isOpen() ) + { + return true; + } else { + return false; + } + } + + /** + * Really opens a connection + * @access private + */ + function reallyOpenConnection( &$server ) { + if( !is_array( $server ) ) { + throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' ); + } + + extract( $server ); + # Get class for this database type + $class = 'Database' . ucfirst( $type ); + if ( !class_exists( $class ) ) { + require_once( "$class.php" ); + } + + # Create object + $db = new $class( $host, $user, $password, $dbname, 1, $flags ); + $db->setLBInfo( $server ); + return $db; + } + + function reportConnectionError( &$conn ) + { + $fname = 'LoadBalancer::reportConnectionError'; + wfProfileIn( $fname ); + # Prevent infinite recursion + + static $reporting = false; + if ( !$reporting ) { + $reporting = true; + if ( !is_object( $conn ) ) { + // No last connection, probably due to all servers being too busy + $conn = new Database; + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + $conn->reportConnectionError( $this->mLastError ); + } else { + // If all servers were busy, mLastError will contain something sensible + throw new DBConnectionError( $conn, $this->mLastError ); + } + } else { + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + } else { + $conn->failFunction( false ); + } + $server = $conn->getProperty( 'mServer' ); + $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); + } + $reporting = false; + } + wfProfileOut( $fname ); + } + + function getWriterIndex() { + return 0; + } + + /** + * Force subsequent calls to getConnection(DB_SLAVE) to return the + * given index. Set to -1 to restore the original load balancing + * behaviour. I thought this was a good idea when I originally + * wrote this class, but it has never been used. + */ + function force( $i ) { + $this->mForce = $i; + } + + /** + * Returns true if the specified index is a valid server index + */ + function haveIndex( $i ) { + return array_key_exists( $i, $this->mServers ); + } + + /** + * Returns true if the specified index is valid and has non-zero load + */ + function isNonZeroLoad( $i ) { + return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0; + } + + /** + * Get the number of defined servers (not the number of open connections) + */ + function getServerCount() { + return count( $this->mServers ); + } + + /** + * Save master pos to the session and to memcached, if the session exists + */ + function saveMasterPos() { + global $wgSessionStarted; + if ( $wgSessionStarted && count( $this->mServers ) > 1 ) { + # If this entire request was served from a slave without opening a connection to the + # master (however unlikely that may be), then we can fetch the position from the slave. + if ( empty( $this->mConnections[0] ) ) { + $conn =& $this->getConnection( DB_SLAVE ); + list( $file, $pos ) = $conn->getSlavePos(); + wfDebug( "Saving master pos fetched from slave: $file $pos\n" ); + } else { + $conn =& $this->getConnection( 0 ); + list( $file, $pos ) = $conn->getMasterPos(); + wfDebug( "Saving master pos: $file $pos\n" ); + } + if ( $file !== false ) { + $_SESSION['master_log_file'] = $file; + $_SESSION['master_pos'] = $pos; + } + } + } + + /** + * Loads the master pos from the session, waits for it if necessary + */ + function loadMasterPos() { + if ( isset( $_SESSION['master_log_file'] ) && isset( $_SESSION['master_pos'] ) ) { + $this->waitFor( $_SESSION['master_log_file'], $_SESSION['master_pos'] ); + } + } + + /** + * Close all open connections + */ + function closeAll() { + foreach( $this->mConnections as $i => $conn ) { + if ( $this->isOpen( $i ) ) { + // Need to use this syntax because $conn is a copy not a reference + $this->mConnections[$i]->close(); + } + } + } + + function commitAll() { + foreach( $this->mConnections as $i => $conn ) { + if ( $this->isOpen( $i ) ) { + // Need to use this syntax because $conn is a copy not a reference + $this->mConnections[$i]->immediateCommit(); + } + } + } + + function waitTimeout( $value = NULL ) { + return wfSetVar( $this->mWaitTimeout, $value ); + } + + function getLaggedSlaveMode() { + return $this->mLaggedSlaveMode; + } + + /* Disables/enables lag checks */ + function allowLagged($mode=null) { + if ($mode===null) + return $this->mAllowLagged; + $this->mAllowLagged=$mode; + } + + function pingAll() { + $success = true; + foreach ( $this->mConnections as $i => $conn ) { + if ( $this->isOpen( $i ) ) { + if ( !$this->mConnections[$i]->ping() ) { + $success = false; + } + } + } + return $success; + } + + /** + * Get the hostname and lag time of the most-lagged slave + * This is useful for maintenance scripts that need to throttle their updates + */ + function getMaxLag() { + $maxLag = -1; + $host = ''; + foreach ( $this->mServers as $i => $conn ) { + if ( $this->openConnection( $i ) ) { + $lag = $this->mConnections[$i]->getLag(); + if ( $lag > $maxLag ) { + $maxLag = $lag; + $host = $this->mServers[$i]['host']; + } + } + } + return array( $host, $maxLag ); + } + + /** + * Get lag time for each DB + * Results are cached for a short time in memcached + */ + function getLagTimes() { + global $wgDBname; + + $expiry = 5; + $requestRate = 10; + + global $wgMemc; + $times = $wgMemc->get( "$wgDBname:lag_times" ); + if ( $times ) { + # Randomly recache with probability rising over $expiry + $elapsed = time() - $times['timestamp']; + $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); + if ( mt_rand( 0, $chance ) != 0 ) { + unset( $times['timestamp'] ); + return $times; + } + } + + # Cache key missing or expired + + $times = array(); + foreach ( $this->mServers as $i => $conn ) { + if ($i==0) { # Master + $times[$i] = 0; + } elseif ( $this->openConnection( $i ) ) { + $times[$i] = $this->mConnections[$i]->getLag(); + } + } + + # Add a timestamp key so we know when it was cached + $times['timestamp'] = time(); + $wgMemc->set( "$wgDBname:lag_times", $times, $expiry ); + + # But don't give the timestamp to the caller + unset($times['timestamp']); + return $times; + } +} + +?> diff --git a/includes/LogPage.php b/includes/LogPage.php new file mode 100644 index 00000000..f588105f --- /dev/null +++ b/includes/LogPage.php @@ -0,0 +1,246 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html + +/** + * Contain log classes + * + * @package MediaWiki + */ + +/** + * Class to simplify the use of log pages. + * The logs are now kept in a table which is easier to manage and trim + * than ever-growing wiki pages. + * + * @package MediaWiki + */ +class LogPage { + /* @access private */ + var $type, $action, $comment, $params, $target; + /* @acess public */ + var $updateRecentChanges; + + /** + * Constructor + * + * @param string $type One of '', 'block', 'protect', 'rights', 'delete', + * 'upload', 'move' + * @param bool $rc Whether to update recent changes as well as the logging table + */ + function LogPage( $type, $rc = true ) { + $this->type = $type; + $this->updateRecentChanges = $rc; + } + + function saveContent() { + if( wfReadOnly() ) return false; + + global $wgUser; + $fname = 'LogPage::saveContent'; + + $dbw =& wfGetDB( DB_MASTER ); + $uid = $wgUser->getID(); + + $this->timestamp = $now = wfTimestampNow(); + $dbw->insert( 'logging', + array( + 'log_type' => $this->type, + 'log_action' => $this->action, + 'log_timestamp' => $dbw->timestamp( $now ), + 'log_user' => $uid, + 'log_namespace' => $this->target->getNamespace(), + 'log_title' => $this->target->getDBkey(), + 'log_comment' => $this->comment, + 'log_params' => $this->params + ), $fname + ); + + # And update recentchanges + if ( $this->updateRecentChanges ) { + $titleObj = Title::makeTitle( NS_SPECIAL, 'Log/' . $this->type ); + $rcComment = $this->actionText; + if( '' != $this->comment ) { + if ($rcComment == '') + $rcComment = $this->comment; + else + $rcComment .= ': ' . $this->comment; + } + + RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '', + $this->type, $this->action, $this->target, $this->comment, $this->params ); + } + return true; + } + + /** + * @static + */ + function validTypes() { + global $wgLogTypes; + return $wgLogTypes; + } + + /** + * @static + */ + function isLogType( $type ) { + return in_array( $type, LogPage::validTypes() ); + } + + /** + * @static + */ + function logName( $type ) { + global $wgLogNames; + + if( isset( $wgLogNames[$type] ) ) { + return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) ); + } else { + // Bogus log types? Perhaps an extension was removed. + return $type; + } + } + + /** + * @fixme: handle missing log types + * @static + */ + function logHeader( $type ) { + global $wgLogHeaders; + return wfMsg( $wgLogHeaders[$type] ); + } + + /** + * @static + */ + function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) { + global $wgLang, $wgContLang, $wgLogActions; + + $key = "$type/$action"; + if( isset( $wgLogActions[$key] ) ) { + if( is_null( $title ) ) { + $rv=wfMsg( $wgLogActions[$key] ); + } else { + if( $skin ) { + + switch( $type ) { + case 'move': + $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); + $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), $params[0] ); + break; + case 'block': + if( substr( $title->getText(), 0, 1 ) == '#' ) { + $titleLink = $title->getText(); + } else { + $titleLink = $skin->makeLinkObj( $title, $title->getText() ); + $titleLink .= ' (' . $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions/' . $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')'; + } + break; + case 'rights': + $text = $wgContLang->ucfirst( $title->getText() ); + $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); + break; + default: + $titleLink = $skin->makeLinkObj( $title ); + } + + } else { + $titleLink = $title->getPrefixedText(); + } + if( $key == 'rights/rights' ) { + if ($skin) { + $rightsnone = wfMsg( 'rightsnone' ); + } else { + $rightsnone = wfMsgForContent( 'rightsnone' ); + } + if( !isset( $params[0] ) || trim( $params[0] ) == '' ) + $params[0] = $rightsnone; + if( !isset( $params[1] ) || trim( $params[1] ) == '' ) + $params[1] = $rightsnone; + } + if( count( $params ) == 0 ) { + if ( $skin ) { + $rv = wfMsg( $wgLogActions[$key], $titleLink ); + } else { + $rv = wfMsgForContent( $wgLogActions[$key], $titleLink ); + } + } else { + array_unshift( $params, $titleLink ); + if ( $translate && $key == 'block/block' ) { + $params[1] = $wgLang->translateBlockExpiry($params[1]); + } + $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ); + } + } + } else { + wfDebug( "LogPage::actionText - unknown action $key\n" ); + $rv = "$action"; + } + if( $filterWikilinks ) { + $rv = str_replace( "[[", "", $rv ); + $rv = str_replace( "]]", "", $rv ); + } + return $rv; + } + + /** + * Add a log entry + * @param string $action one of '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'move_redir' + * @param object &$target A title object. + * @param string $comment Description associated + * @param array $params Parameters passed later to wfMsg.* functions + */ + function addEntry( $action, &$target, $comment, $params = array() ) { + if ( !is_array( $params ) ) { + $params = array( $params ); + } + + $this->action = $action; + $this->target =& $target; + $this->comment = $comment; + $this->params = LogPage::makeParamBlob( $params ); + + $this->actionText = LogPage::actionText( $this->type, $action, $target, NULL, $params ); + + return $this->saveContent(); + } + + /** + * Create a blob from a parameter array + * @static + */ + function makeParamBlob( $params ) { + return implode( "\n", $params ); + } + + /** + * Extract a parameter array from a blob + * @static + */ + function extractParams( $blob ) { + if ( $blob === '' ) { + return array(); + } else { + return explode( "\n", $blob ); + } + } +} + +?> diff --git a/includes/MacBinary.php b/includes/MacBinary.php new file mode 100644 index 00000000..05c3ce5c --- /dev/null +++ b/includes/MacBinary.php @@ -0,0 +1,272 @@ + + * Portions based on Convert::BinHex by Eryq et al + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @package MediaWiki + * @subpackage SpecialPage + */ + +class MacBinary { + function MacBinary( $filename ) { + $this->open( $filename ); + $this->loadHeader(); + } + + /** + * The file must be seekable, such as local filesystem. + * Remote URLs probably won't work. + * + * @param string $filename + */ + function open( $filename ) { + $this->valid = false; + $this->version = 0; + $this->filename = ''; + $this->dataLength = 0; + $this->resourceLength = 0; + $this->handle = fopen( $filename, 'rb' ); + } + + /** + * Does this appear to be a valid MacBinary archive? + * @return bool + */ + function isValid() { + return $this->valid; + } + + /** + * Get length of data fork + * @return int + */ + function dataForkLength() { + return $this->dataLength; + } + + /** + * Copy the data fork to an external file or resource. + * @param resource $destination + * @return bool + */ + function extractData( $destination ) { + if( !$this->isValid() ) { + return false; + } + + // Data fork appears immediately after header + fseek( $this->handle, 128 ); + return $this->copyBytesTo( $destination, $this->dataLength ); + } + + /** + * + */ + function close() { + fclose( $this->handle ); + } + + // -------------------------------------------------------------- + + /** + * Check if the given file appears to be MacBinary-encoded, + * as Internet Explorer on Mac OS may provide for unknown types. + * http://www.lazerware.com/formats/macbinary/macbinary_iii.html + * If ok, load header data. + * + * @return bool + * @access private + */ + function loadHeader() { + $fname = 'MacBinary::loadHeader'; + + fseek( $this->handle, 0 ); + $head = fread( $this->handle, 128 ); + $this->hexdump( $head ); + + if( strlen( $head ) < 128 ) { + wfDebug( "$fname: couldn't read full MacBinary header\n" ); + return false; + } + + if( $head{0} != "\x00" || $head{74} != "\x00" ) { + wfDebug( "$fname: header bytes 0 and 74 not null\n" ); + return false; + } + + $signature = substr( $head, 102, 4 ); + $a = unpack( "ncrc", substr( $head, 124, 2 ) ); + $storedCRC = $a['crc']; + $calculatedCRC = $this->calcCRC( substr( $head, 0, 124 ) ); + if( $storedCRC == $calculatedCRC ) { + if( $signature == 'mBIN' ) { + $this->version = 3; + } else { + $this->version = 2; + } + } else { + $crc = sprintf( "%x != %x", $storedCRC, $calculatedCRC ); + if( $storedCRC == 0 && $head{82} == "\x00" && + substr( $head, 101, 24 ) == str_repeat( "\x00", 24 ) ) { + wfDebug( "$fname: no CRC, looks like MacBinary I\n" ); + $this->version = 1; + } elseif( $signature == 'mBIN' && $storedCRC == 0x185 ) { + // Mac IE 5.0 seems to insert this value in the CRC field. + // 5.2.3 works correctly; don't know about other versions. + wfDebug( "$fname: CRC doesn't match ($crc), looks like Mac IE 5.0\n" ); + $this->version = 3; + } else { + wfDebug( "$fname: CRC doesn't match ($crc) and not MacBinary I\n" ); + return false; + } + } + + $nameLength = ord( $head{1} ); + if( $nameLength < 1 || $nameLength > 63 ) { + wfDebug( "$fname: invalid filename size $nameLength\n" ); + return false; + } + $this->filename = substr( $head, 2, $nameLength ); + + $forks = unpack( "Ndata/Nresource", substr( $head, 83, 8 ) ); + $this->dataLength = $forks['data']; + $this->resourceLength = $forks['resource']; + $maxForkLength = 0x7fffff; + + if( $this->dataLength < 0 || $this->dataLength > $maxForkLength ) { + wfDebug( "$fname: invalid data fork length $this->dataLength\n" ); + return false; + } + + if( $this->resourceLength < 0 || $this->resourceLength > $maxForkLength ) { + wfDebug( "$fname: invalid resource fork size $this->resourceLength\n" ); + return false; + } + + wfDebug( "$fname: appears to be MacBinary $this->version, data length $this->dataLength\n" ); + $this->valid = true; + return true; + } + + /** + * Calculate a 16-bit CRC value as for MacBinary headers. + * Adapted from perl5 Convert::BinHex by Eryq, + * based on the mcvert utility (Doug Moore, April '87), + * with magic array thingy by Jim Van Verth. + * http://search.cpan.org/~eryq/Convert-BinHex-1.119/lib/Convert/BinHex.pm + * + * @param string $data + * @param int $seed + * @return int + * @access private + */ + function calcCRC( $data, $seed = 0 ) { + # An array useful for CRC calculations that use 0x1021 as the "seed": + $MAGIC = array( + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 + ); + $len = strlen( $data ); + $crc = $seed; + for( $i = 0; $i < $len; $i++ ) { + $crc ^= ord( $data{$i} ) << 8; + $crc &= 0xFFFF; + $crc = ($crc << 8) ^ $MAGIC[$crc >> 8]; + $crc &= 0xFFFF; + } + return $crc; + } + + /** + * @param resource $destination + * @param int $bytesToCopy + * @return bool + * @access private + */ + function copyBytesTo( $destination, $bytesToCopy ) { + $bufferSize = 65536; + for( $remaining = $bytesToCopy; $remaining > 0; $remaining -= $bufferSize ) { + $thisChunkSize = min( $remaining, $bufferSize ); + $buffer = fread( $this->handle, $thisChunkSize ); + fwrite( $destination, $buffer ); + } + } + + /** + * Hex dump of the header for debugging + * @access private + */ + function hexdump( $data ) { + global $wgDebugLogFile; + if( !$wgDebugLogFile ) return; + + $width = 16; + $at = 0; + for( $remaining = strlen( $data ); $remaining > 0; $remaining -= $width ) { + $line = sprintf( "%04x:", $at ); + $printable = ''; + for( $i = 0; $i < $width && $remaining - $i > 0; $i++ ) { + $byte = ord( $data{$at++} ); + $line .= sprintf( " %02x", $byte ); + $printable .= ($byte >= 32 && $byte <= 126 ) + ? chr( $byte ) + : '.'; + } + if( $i < $width ) { + $line .= str_repeat( ' ', $width - $i ); + } + wfDebug( "MacBinary: $line $printable\n" ); + } + } +} + +?> \ No newline at end of file diff --git a/includes/MagicWord.php b/includes/MagicWord.php new file mode 100644 index 00000000..c80d2583 --- /dev/null +++ b/includes/MagicWord.php @@ -0,0 +1,448 @@ +match( $text ) ) + * + * Possible future improvements: + * * Simultaneous searching for a number of magic words + * * $wgMagicWords in shared memory + * + * Please avoid reading the data out of one of these objects and then writing + * special case code. If possible, add another match()-like function here. + * + * @package MediaWiki + */ +class MagicWord { + /**#@+ + * @private + */ + var $mId, $mSynonyms, $mCaseSensitive, $mRegex; + var $mRegexStart, $mBaseRegex, $mVariableRegex; + var $mModified; + /**#@-*/ + + function MagicWord($id = 0, $syn = '', $cs = false) { + $this->mId = $id; + $this->mSynonyms = (array)$syn; + $this->mCaseSensitive = $cs; + $this->mRegex = ''; + $this->mRegexStart = ''; + $this->mVariableRegex = ''; + $this->mVariableStartToEndRegex = ''; + $this->mModified = false; + } + + /** + * Factory: creates an object representing an ID + * @static + */ + function &get( $id ) { + global $wgMagicWords; + + if ( !is_array( $wgMagicWords ) ) { + throw new MWException( "Incorrect initialisation order, \$wgMagicWords does not exist\n" ); + } + if (!array_key_exists( $id, $wgMagicWords ) ) { + $mw = new MagicWord(); + $mw->load( $id ); + $wgMagicWords[$id] = $mw; + } + return $wgMagicWords[$id]; + } + + # Initialises this object with an ID + function load( $id ) { + global $wgContLang; + $this->mId = $id; + $wgContLang->getMagic( $this ); + } + + /** + * Preliminary initialisation + * @private + */ + function initRegex() { + #$variableClass = Title::legalChars(); + # This was used for matching "$1" variables, but different uses of the feature will have + # different restrictions, which should be checked *after* the MagicWord has been matched, + # not here. - IMSoP + + $escSyn = array(); + foreach ( $this->mSynonyms as $synonym ) + // In case a magic word contains /, like that's going to happen;) + $escSyn[] = preg_quote( $synonym, '/' ); + $this->mBaseRegex = implode( '|', $escSyn ); + + $case = $this->mCaseSensitive ? '' : 'i'; + $this->mRegex = "/{$this->mBaseRegex}/{$case}"; + $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}"; + $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex ); + $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)", + "/^(?:{$this->mBaseRegex})$/{$case}" ); + } + + /** + * Gets a regex representing matching the word + */ + function getRegex() { + if ($this->mRegex == '' ) { + $this->initRegex(); + } + return $this->mRegex; + } + + /** + * Gets the regexp case modifier to use, i.e. i or nothing, to be used if + * one is using MagicWord::getBaseRegex(), otherwise it'll be included in + * the complete expression + */ + function getRegexCase() { + if ( $this->mRegex === '' ) + $this->initRegex(); + + return $this->mCaseSensitive ? '' : 'i'; + } + + /** + * Gets a regex matching the word, if it is at the string start + */ + function getRegexStart() { + if ($this->mRegex == '' ) { + $this->initRegex(); + } + return $this->mRegexStart; + } + + /** + * regex without the slashes and what not + */ + function getBaseRegex() { + if ($this->mRegex == '') { + $this->initRegex(); + } + return $this->mBaseRegex; + } + + /** + * Returns true if the text contains the word + * @return bool + */ + function match( $text ) { + return preg_match( $this->getRegex(), $text ); + } + + /** + * Returns true if the text starts with the word + * @return bool + */ + function matchStart( $text ) { + return preg_match( $this->getRegexStart(), $text ); + } + + /** + * Returns NULL if there's no match, the value of $1 otherwise + * The return code is the matched string, if there's no variable + * part in the regex and the matched variable part ($1) if there + * is one. + */ + function matchVariableStartToEnd( $text ) { + $matches = array(); + $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches ); + if ( $matchcount == 0 ) { + return NULL; + } elseif ( count($matches) == 1 ) { + return $matches[0]; + } else { + # multiple matched parts (variable match); some will be empty because of + # synonyms. The variable will be the second non-empty one so remove any + # blank elements and re-sort the indices. + $matches = array_values(array_filter($matches)); + return $matches[1]; + } + } + + + /** + * Returns true if the text matches the word, and alters the + * input string, removing all instances of the word + */ + function matchAndRemove( &$text ) { + global $wgMagicFound; + $wgMagicFound = false; + $text = preg_replace_callback( $this->getRegex(), 'pregRemoveAndRecord', $text ); + return $wgMagicFound; + } + + function matchStartAndRemove( &$text ) { + global $wgMagicFound; + $wgMagicFound = false; + $text = preg_replace_callback( $this->getRegexStart(), 'pregRemoveAndRecord', $text ); + return $wgMagicFound; + } + + + /** + * Replaces the word with something else + */ + function replace( $replacement, $subject, $limit=-1 ) { + $res = preg_replace( $this->getRegex(), wfRegexReplacement( $replacement ), $subject, $limit ); + $this->mModified = !($res === $subject); + return $res; + } + + /** + * Variable handling: {{SUBST:xxx}} style words + * Calls back a function to determine what to replace xxx with + * Input word must contain $1 + */ + function substituteCallback( $text, $callback ) { + $res = preg_replace_callback( $this->getVariableRegex(), $callback, $text ); + $this->mModified = !($res === $text); + return $res; + } + + /** + * Matches the word, where $1 is a wildcard + */ + function getVariableRegex() { + if ( $this->mVariableRegex == '' ) { + $this->initRegex(); + } + return $this->mVariableRegex; + } + + /** + * Matches the entire string, where $1 is a wildcard + */ + function getVariableStartToEndRegex() { + if ( $this->mVariableStartToEndRegex == '' ) { + $this->initRegex(); + } + return $this->mVariableStartToEndRegex; + } + + /** + * Accesses the synonym list directly + */ + function getSynonym( $i ) { + return $this->mSynonyms[$i]; + } + + function getSynonyms() { + return $this->mSynonyms; + } + + /** + * Returns true if the last call to replace() or substituteCallback() + * returned a modified text, otherwise false. + */ + function getWasModified(){ + return $this->mModified; + } + + /** + * $magicarr is an associative array of (magic word ID => replacement) + * This method uses the php feature to do several replacements at the same time, + * thereby gaining some efficiency. The result is placed in the out variable + * $result. The return value is true if something was replaced. + * @static + **/ + function replaceMultiple( $magicarr, $subject, &$result ){ + $search = array(); + $replace = array(); + foreach( $magicarr as $id => $replacement ){ + $mw = MagicWord::get( $id ); + $search[] = $mw->getRegex(); + $replace[] = $replacement; + } + + $result = preg_replace( $search, $replace, $subject ); + return !($result === $subject); + } + + /** + * Adds all the synonyms of this MagicWord to an array, to allow quick + * lookup in a list of magic words + */ + function addToArray( &$array, $value ) { + foreach ( $this->mSynonyms as $syn ) { + $array[$syn] = $value; + } + } + + function isCaseSensitive() { + return $this->mCaseSensitive; + } +} + +/** + * Used in matchAndRemove() + * @private + **/ +function pregRemoveAndRecord( $match ) { + global $wgMagicFound; + $wgMagicFound = true; + return ''; +} + +?> diff --git a/includes/Math.php b/includes/Math.php new file mode 100644 index 00000000..f9d6a605 --- /dev/null +++ b/includes/Math.php @@ -0,0 +1,269 @@ + parsing + * @package MediaWiki + */ + +/** + * Takes LaTeX fragments, sends them to a helper program (texvc) for rendering + * to rasterized PNG and HTML and MathML approximations. An appropriate + * rendering form is picked and returned. + * + * by Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004) + * + * @package MediaWiki + */ +class MathRenderer { + var $mode = MW_MATH_MODERN; + var $tex = ''; + var $inputhash = ''; + var $hash = ''; + var $html = ''; + var $mathml = ''; + var $conservativeness = 0; + + function MathRenderer( $tex ) { + $this->tex = $tex; + } + + function setOutputMode( $mode ) { + $this->mode = $mode; + } + + function render() { + global $wgTmpDirectory, $wgInputEncoding; + global $wgTexvc; + $fname = 'MathRenderer::render'; + + if( $this->mode == MW_MATH_SOURCE ) { + # No need to render or parse anything more! + return ('$ '.htmlspecialchars( $this->tex ).' $'); + } + + if( !$this->_recall() ) { + # Ensure that the temp and output directories are available before continuing... + if( !file_exists( $wgTmpDirectory ) ) { + if( !@mkdir( $wgTmpDirectory ) ) { + return $this->_error( 'math_bad_tmpdir' ); + } + } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) { + return $this->_error( 'math_bad_tmpdir' ); + } + + if( function_exists( 'is_executable' ) && !is_executable( $wgTexvc ) ) { + return $this->_error( 'math_notexvc' ); + } + $cmd = $wgTexvc . ' ' . + escapeshellarg( $wgTmpDirectory ).' '. + escapeshellarg( $wgTmpDirectory ).' '. + escapeshellarg( $this->tex ).' '. + escapeshellarg( $wgInputEncoding ); + + if ( wfIsWindows() ) { + # Invoke it within cygwin sh, because texvc expects sh features in its default shell + $cmd = 'sh -c ' . wfEscapeShellArg( $cmd ); + } + + wfDebug( "TeX: $cmd\n" ); + $contents = `$cmd`; + wfDebug( "TeX output:\n $contents\n---\n" ); + + if (strlen($contents) == 0) { + return $this->_error( 'math_unknown_error' ); + } + + $retval = substr ($contents, 0, 1); + $errmsg = ''; + if (($retval == 'C') || ($retval == 'M') || ($retval == 'L')) { + if ($retval == 'C') + $this->conservativeness = 2; + else if ($retval == 'M') + $this->conservativeness = 1; + else + $this->conservativeness = 0; + $outdata = substr ($contents, 33); + + $i = strpos($outdata, "\000"); + + $this->html = substr($outdata, 0, $i); + $this->mathml = substr($outdata, $i+1); + } else if (($retval == 'c') || ($retval == 'm') || ($retval == 'l')) { + $this->html = substr ($contents, 33); + if ($retval == 'c') + $this->conservativeness = 2; + else if ($retval == 'm') + $this->conservativeness = 1; + else + $this->conservativeness = 0; + $this->mathml = NULL; + } else if ($retval == 'X') { + $this->html = NULL; + $this->mathml = substr ($contents, 33); + $this->conservativeness = 0; + } else if ($retval == '+') { + $this->html = NULL; + $this->mathml = NULL; + $this->conservativeness = 0; + } 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 ); + } + } + + if ( !$errmsg ) { + $this->hash = substr ($contents, 1, 32); + } + + $res = wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) ); + + if ( $errmsg ) { + return $errmsg; + } + + if (!preg_match("/^[a-f0-9]{32}$/", $this->hash)) { + return $this->_error( 'math_unknown_error' ); + } + + if( !file_exists( "$wgTmpDirectory/{$this->hash}.png" ) ) { + return $this->_error( 'math_image_error' ); + } + + $hashpath = $this->_getHashPath(); + if( !file_exists( $hashpath ) ) { + if( !@wfMkdirParents( $hashpath, 0755 ) ) { + return $this->_error( 'math_bad_output' ); + } + } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) { + return $this->_error( 'math_bad_output' ); + } + + if( !rename( "$wgTmpDirectory/{$this->hash}.png", "$hashpath/{$this->hash}.png" ) ) { + return $this->_error( 'math_output_error' ); + } + + # Now save it back to the DB: + if ( !wfReadOnly() ) { + $outmd5_sql = pack('H32', $this->hash); + + $md5_sql = pack('H32', $this->md5); # Binary packed, not hex + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->replace( 'math', array( 'math_inputhash' ), + array( + 'math_inputhash' => $md5_sql, + 'math_outputhash' => $outmd5_sql, + 'math_html_conservativeness' => $this->conservativeness, + 'math_html' => $this->html, + 'math_mathml' => $this->mathml, + ), $fname, array( 'IGNORE' ) + ); + } + + } + + return $this->_doRender(); + } + + function _error( $msg, $append = '' ) { + $mf = htmlspecialchars( wfMsg( 'math_failure' ) ); + $errmsg = htmlspecialchars( wfMsg( $msg ) ); + $source = htmlspecialchars( str_replace( "\n", ' ', $this->tex ) ); + return "$mf ($errmsg$append): $source\n"; + } + + function _recall() { + global $wgMathDirectory; + $fname = 'MathRenderer::_recall'; + + $this->md5 = md5( $this->tex ); + $dbr =& wfGetDB( DB_SLAVE ); + $rpage = $dbr->selectRow( 'math', + array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ), + array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex + $fname + ); + + if( $rpage !== false ) { + # Tailing 0x20s can get dropped by the database, add it back on if necessary: + $xhash = unpack( 'H32md5', $rpage->math_outputhash . " " ); + $this->hash = $xhash ['md5']; + + $this->conservativeness = $rpage->math_html_conservativeness; + $this->html = $rpage->math_html; + $this->mathml = $rpage->math_mathml; + + if( file_exists( $this->_getHashPath() . "/{$this->hash}.png" ) ) { + return true; + } + + if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) { + $hashpath = $this->_getHashPath(); + + if( !file_exists( $hashpath ) ) { + if( !@wfMkdirParents( $hashpath, 0755 ) ) { + return false; + } + } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) { + return false; + } + if ( function_exists( "link" ) ) { + return link ( $wgMathDirectory . "/{$this->hash}.png", + $hashpath . "/{$this->hash}.png" ); + } else { + return rename ( $wgMathDirectory . "/{$this->hash}.png", + $hashpath . "/{$this->hash}.png" ); + } + } + + } + + # Missing from the database and/or the render cache + return false; + } + + /** + * Select among PNG, HTML, or MathML output depending on + */ + function _doRender() { + if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) { + return "{$this->mathml}"; + } + if (($this->mode == MW_MATH_PNG) || ($this->html == '') || + (($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) || + (($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) { + return $this->_linkToMathImage(); + } else { + return ''.$this->html.''; + } + } + + function _linkToMathImage() { + global $wgMathPath; + $url = htmlspecialchars( "$wgMathPath/" . substr($this->hash, 0, 1) + .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1) + . "/{$this->hash}.png" ); + $alt = trim(str_replace("\n", ' ', htmlspecialchars( $this->tex ))); + return "\"$alt\""; + } + + function _getHashPath() { + global $wgMathDirectory; + $path = $wgMathDirectory .'/'. substr($this->hash, 0, 1) + .'/'. substr($this->hash, 1, 1) + .'/'. substr($this->hash, 2, 1); + wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" ); + return $path; + } + + function renderMath( $tex ) { + global $wgUser; + $math = new MathRenderer( $tex ); + $math->setOutputMode( $wgUser->getOption('math')); + return $math->render(); + } +} +?> diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php new file mode 100644 index 00000000..af49109c --- /dev/null +++ b/includes/MemcachedSessions.php @@ -0,0 +1,74 @@ +get( memsess_key( $id ) ); + if( ! $data ) return ''; + return $data; +} + +/** + * @todo document + */ +function memsess_write( $id, $data ) { + global $wgMemc; + $wgMemc->set( memsess_key( $id ), $data, 3600 ); + return true; +} + +/** + * @todo document + */ +function memsess_destroy( $id ) { + global $wgMemc; + $wgMemc->delete( memsess_key( $id ) ); + return true; +} + +/** + * @todo document + */ +function memsess_gc( $maxlifetime ) { + # NOP: Memcached performs garbage collection. + return true; +} + +session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' ); + +?> diff --git a/includes/MessageCache.php b/includes/MessageCache.php new file mode 100644 index 00000000..c8b7124c --- /dev/null +++ b/includes/MessageCache.php @@ -0,0 +1,581 @@ +mUseCache = !is_null( $memCached ); + $this->mMemc = &$memCached; + $this->mDisable = !$useDB; + $this->mExpiry = $expiry; + $this->mDisableTransform = false; + $this->mMemcKey = $memcPrefix.':messages'; + $this->mKeys = false; # initialised on demand + $this->mInitialised = true; + + wfProfileIn( __METHOD__.'-parseropt' ); + $this->mParserOptions = new ParserOptions( $u=NULL ); + wfProfileOut( __METHOD__.'-parseropt' ); + $this->mParser = null; + + # When we first get asked for a message, + # then we'll fill up the cache. If we + # can return a cache hit, this saves + # some extra milliseconds + $this->mDeferred = true; + + wfProfileOut( __METHOD__ ); + } + + /** + * Try to load the cache from a local file + */ + function loadFromLocal( $hash ) { + global $wgLocalMessageCache, $wgDBname; + + $this->mCache = false; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-$wgDBname"; + + wfSuppressWarnings(); + $file = fopen( $filename, 'r' ); + wfRestoreWarnings(); + if ( !$file ) { + 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, 1000000 ); + $this->mCache = unserialize( $serialized ); + } + fclose( $file ); + } + + /** + * Save the cache to a local file + */ + function saveToLocal( $serialized, $hash ) { + global $wgLocalMessageCache, $wgDBname; + + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $oldUmask = umask( 0 ); + wfMkdirParents( $wgLocalMessageCache, 0777 ); + umask( $oldUmask ); + + $file = fopen( $filename, 'w' ); + if ( !$file ) { + wfDebug( "Unable to open local cache file for writing\n" ); + return; + } + + fwrite( $file, $hash . $serialized ); + fclose( $file ); + @chmod( $filename, 0666 ); + } + + function loadFromScript( $hash ) { + global $wgLocalMessageCache, $wgDBname; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-$wgDBname"; + + wfSuppressWarnings(); + $file = fopen( $filename, 'r' ); + wfRestoreWarnings(); + if ( !$file ) { + return; + } + $localHash=substr(fread($file,40),8); + fclose($file); + if ($hash!=$localHash) { + return; + } + require("$wgLocalMessageCache/messages-$wgDBname"); + } + + function saveToScript($array, $hash) { + global $wgLocalMessageCache, $wgDBname; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $oldUmask = umask( 0 ); + wfMkdirParents( $wgLocalMessageCache, 0777 ); + umask( $oldUmask ); + $file = fopen( $filename.'.tmp', 'w'); + fwrite($file,"mCache = array("); + + foreach ($array as $key => $message) { + fwrite($file, "'". $this->escapeForScript($key). + "' => '" . $this->escapeForScript($message). + "',\n"); + } + fwrite($file,");\n?>"); + fclose($file); + rename($filename.'.tmp',$filename); + } + + function escapeForScript($string) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '\'', '\\\'', $string ); + return $string; + } + + /** + * Loads messages either from memcached or the database, if not disabled + * On error, quietly switches to a fallback mode + * Returns false for a reportable error, true otherwise + */ + function load() { + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized; + + if ( $this->mDisable ) { + static $shownDisabled = false; + if ( !$shownDisabled ) { + wfDebug( "MessageCache::load(): disabled\n" ); + $shownDisabled = true; + } + return true; + } + $fname = 'MessageCache::load'; + wfProfileIn( $fname ); + $success = true; + + if ( $this->mUseCache ) { + $this->mCache = false; + + # Try local cache + wfProfileIn( $fname.'-fromlocal' ); + $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); + if ( $hash ) { + if ($wgLocalMessageCacheSerialized) { + $this->loadFromLocal( $hash ); + } else { + $this->loadFromScript( $hash ); + } + } + wfProfileOut( $fname.'-fromlocal' ); + + # Try memcached + if ( !$this->mCache ) { + wfProfileIn( $fname.'-fromcache' ); + $this->mCache = $this->mMemc->get( $this->mMemcKey ); + + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + if ( !$hash ) { + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + } + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } + wfProfileOut( $fname.'-fromcache' ); + } + + + # If there's nothing in memcached, load all the messages from the database + if ( !$this->mCache ) { + wfDebug( "MessageCache::load(): loading all messages\n" ); + $this->lock(); + # Other threads don't need to load the messages if another thread is doing it. + $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT ); + if ( $success ) { + wfProfileIn( $fname.'-load' ); + $this->loadFromDB(); + wfProfileOut( $fname.'-load' ); + + # Save in memcached + # Keep trying if it fails, this is kind of important + wfProfileIn( $fname.'-save' ); + for ($i=0; $i<20 && + !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); + $i++ ) { + usleep(mt_rand(500000,1500000)); + } + + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } + + wfProfileOut( $fname.'-save' ); + if ( $i == 20 ) { + $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 ); + wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); + } + } + $this->unlock(); + } + + if ( !is_array( $this->mCache ) ) { + wfDebug( "MessageCache::load(): individual message mode\n" ); + # If it is 'loading' or 'error', switch to individual message mode, otherwise disable + # Causing too much DB load, disabling -- TS + $this->mDisable = true; + /* + if ( $this->mCache == "loading" ) { + $this->mUseCache = false; + } elseif ( $this->mCache == "error" ) { + $this->mUseCache = false; + $success = false; + } else { + $this->mDisable = true; + $success = false; + }*/ + $this->mCache = false; + } + } + wfProfileOut( $fname ); + $this->mDeferred = false; + return $success; + } + + /** + * Loads all or main part of cacheable messages from the database + */ + function loadFromDB() { + global $wgAllMessagesEn, $wgLang; + + $fname = 'MessageCache::loadFromDB'; + $dbr =& wfGetDB( DB_SLAVE ); + if ( !$dbr ) { + throw new MWException( 'Invalid database object' ); + } + $conditions = array( 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI); + $res = $dbr->select( array( 'page', 'revision', 'text' ), + array( 'page_title', 'old_text', 'old_flags' ), + 'page_is_redirect=0 AND page_namespace='.NS_MEDIAWIKI.' AND page_latest=rev_id AND rev_text_id=old_id', + $fname + ); + + $this->mCache = array(); + for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) { + $this->mCache[$row->page_title] = Revision::getRevisionText( $row ); + } + + # Negative caching + # Go through the language array and the extension array and make a note of + # any keys missing from the cache + foreach ( $wgAllMessagesEn as $key => $value ) { + $uckey = $wgLang->ucfirst( $key ); + if ( !array_key_exists( $uckey, $this->mCache ) ) { + $this->mCache[$uckey] = false; + } + } + + # Make sure all extension messages are available + wfLoadAllExtensions(); + + # Add them to the cache + foreach ( $this->mExtensionMessages as $key => $value ) { + $uckey = $wgLang->ucfirst( $key ); + if ( !array_key_exists( $uckey, $this->mCache ) && + ( isset( $this->mExtensionMessages[$key][$wgLang->getCode()] ) || isset( $this->mExtensionMessages[$key]['en'] ) ) ) { + $this->mCache[$uckey] = false; + } + } + + $dbr->freeResult( $res ); + } + + /** + * Not really needed anymore + */ + function getKeys() { + global $wgAllMessagesEn, $wgContLang; + if ( !$this->mKeys ) { + $this->mKeys = array(); + foreach ( $wgAllMessagesEn as $key => $value ) { + $title = $wgContLang->ucfirst( $key ); + array_push( $this->mKeys, $title ); + } + } + return $this->mKeys; + } + + /** + * @deprecated + */ + function isCacheable( $key ) { + return true; + } + + function replace( $title, $text ) { + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc, $wgDBname; + + $this->lock(); + $this->load(); + $parserMemc->delete("$wgDBname:sidebar"); + if ( is_array( $this->mCache ) ) { + $this->mCache[$title] = $text; + $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); + + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } + + + } + $this->unlock(); + } + + /** + * Returns success + * Represents a write lock on the messages key + */ + function lock() { + if ( !$this->mUseCache ) { + return true; + } + + $lockKey = $this->mMemcKey . 'lock'; + for ($i=0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) { + sleep(1); + } + + return $i >= MSG_WAIT_TIMEOUT; + } + + function unlock() { + if ( !$this->mUseCache ) { + return; + } + + $lockKey = $this->mMemcKey . 'lock'; + $this->mMemc->delete( $lockKey ); + } + + function get( $key, $useDB, $forcontent=true, $isfullkey = false ) { + global $wgContLanguageCode; + if( $forcontent ) { + global $wgContLang; + $lang =& $wgContLang; + $langcode = $wgContLanguageCode; + } else { + global $wgLang, $wgLanguageCode; + $lang =& $wgLang; + $langcode = $wgLanguageCode; + } + # If uninitialised, someone is trying to call this halfway through Setup.php + if( !$this->mInitialised ) { + return '<' . htmlspecialchars($key) . '>'; + } + # If cache initialization was deferred, start it now. + if( $this->mDeferred && !$this->mDisable && $useDB ) { + $this->load(); + } + + $message = false; + if( !$this->mDisable && $useDB ) { + $title = $lang->ucfirst( $key ); + if(!$isfullkey && ($langcode != $wgContLanguageCode) ) { + $title .= '/' . $langcode; + } + $message = $this->getFromCache( $title ); + } + # Try the extension array + if( $message === false && array_key_exists( $key, $this->mExtensionMessages ) ) { + if ( isset( $this->mExtensionMessages[$key][$langcode] ) ) { + $message = $this->mExtensionMessages[$key][$langcode]; + } elseif ( isset( $this->mExtensionMessages[$key]['en'] ) ) { + $message = $this->mExtensionMessages[$key]['en']; + } + } + + # Try the array in the language object + if( $message === false ) { + wfSuppressWarnings(); + $message = $lang->getMessage( $key ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } + + # Try the English array + if( $message === false && $langcode != 'en' ) { + wfSuppressWarnings(); + $message = Language::getMessage( $key ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } + + # Is this a custom message? Try the default language in the db... + if( ($message === false || $message === '-' ) && + !$this->mDisable && $useDB && + !$isfullkey && ($langcode != $wgContLanguageCode) ) { + $message = $this->getFromCache( $lang->ucfirst( $key ) ); + } + + # Final fallback + if( $message === false ) { + return '<' . htmlspecialchars($key) . '>'; + } + + # Replace brace tags + $message = $this->transform( $message ); + return $message; + } + + function getFromCache( $title ) { + $message = false; + + # Try the cache + if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) { + return $this->mCache[$title]; + } + + # Try individual message cache + if ( $this->mUseCache ) { + $message = $this->mMemc->get( $this->mMemcKey . ':' . $title ); + if ( $message == '###NONEXISTENT###' ) { + return false; + } elseif( !is_null( $message ) ) { + $this->mCache[$title] = $message; + return $message; + } else { + $message = false; + } + } + + # Call message Hooks, in case they are defined + wfRunHooks('MessagesPreLoad',array($title,&$message)); + + # If it wasn't in the cache, load each message from the DB individually + $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + if( $revision ) { + $message = $revision->getText(); + if ($this->mUseCache) { + $this->mCache[$title]=$message; + /* individual messages may be often + recached until proper purge code exists + */ + $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 ); + } + } else { + # Negative caching + # Use some special text instead of false, because false gets converted to '' somewhere + $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry ); + } + + return $message; + } + + function transform( $message ) { + global $wgParser; + if ( !$this->mParser && isset( $wgParser ) ) { + # Do some initialisation so that we don't have to do it twice + $wgParser->firstCallInit(); + # Clone it and store it + $this->mParser = clone $wgParser; + } + if ( !$this->mDisableTransform && $this->mParser ) { + if( strpos( $message, '{{' ) !== false ) { + $message = $this->mParser->transformMsg( $message, $this->mParserOptions ); + } + } + return $message; + } + + 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; } + + /** + * Add a message to the cache + * + * @param mixed $key + * @param mixed $value + * @param string $lang The messages language, English by default + */ + function addMessage( $key, $value, $lang = 'en' ) { + $this->mExtensionMessages[$key][$lang] = $value; + } + + /** + * Add an associative array of message to the cache + * + * @param array $messages An associative array of key => values to be added + * @param string $lang The messages language, English by default + */ + function addMessages( $messages, $lang = 'en' ) { + wfProfileIn( __METHOD__ ); + foreach ( $messages as $key => $value ) { + $this->addMessage( $key, $value, $lang ); + } + wfProfileOut( __METHOD__ ); + } + + /** + * Clear all stored messages. Mainly used after a mass rebuild. + */ + function clear() { + if( $this->mUseCache ) { + $this->mMemc->delete( $this->mMemcKey ); + } + } +} +?> diff --git a/includes/Metadata.php b/includes/Metadata.php new file mode 100644 index 00000000..af40ab21 --- /dev/null +++ b/includes/Metadata.php @@ -0,0 +1,362 @@ +. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @author Evan Prodromou + * @package MediaWiki + */ + +/** + * + */ +define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1"); + +function wfDublinCoreRdf($article) { + + $url = dcReallyFullUrl($article->mTitle); + + if (rdfSetup()) { + dcPrologue($url); + dcBasics($article); + dcEpilogue(); + } +} + +function wfCreativeCommonsRdf($article) { + + if (rdfSetup()) { + global $wgRightsUrl; + + $url = dcReallyFullUrl($article->mTitle); + + ccPrologue(); + ccSubPrologue('Work', $url); + dcBasics($article); + if (isset($wgRightsUrl)) { + $url = htmlspecialchars( $wgRightsUrl ); + print " \n"; + } + + ccSubEpilogue('Work'); + + if (isset($wgRightsUrl)) { + $terms = ccGetTerms($wgRightsUrl); + if ($terms) { + ccSubPrologue('License', $wgRightsUrl); + ccLicense($terms); + ccSubEpilogue('License'); + } + } + } + + ccEpilogue(); +} + +/** + * @private + */ +function rdfSetup() { + global $wgOut, $_SERVER; + + $rdftype = wfNegotiateType(wfAcceptToPrefs($_SERVER['HTTP_ACCEPT']), wfAcceptToPrefs(RDF_TYPE_PREFS)); + + if (!$rdftype) { + wfHttpError(406, "Not Acceptable", wfMsg("notacceptable")); + return false; + } else { + $wgOut->disable(); + header( "Content-type: {$rdftype}" ); + $wgOut->sendCacheControl(); + return true; + } +} + +/** + * @private + */ +function dcPrologue($url) { + global $wgOutputEncoding; + + $url = htmlspecialchars( $url ); + print "<" . "?xml version=\"1.0\" encoding=\"{$wgOutputEncoding}\" ?" . "> + + + + + + "; +} + +/** + * @private + */ +function dcEpilogue() { + print " + + + "; +} + +/** + * @private + */ +function dcBasics($article) { + global $wgContLanguageCode, $wgSitename; + + dcElement('title', $article->mTitle->getText()); + dcPageOrString('publisher', wfMsg('aboutpage'), $wgSitename); + dcElement('language', $wgContLanguageCode); + dcElement('type', 'Text'); + dcElement('format', 'text/html'); + dcElement('identifier', dcReallyFullUrl($article->mTitle)); + dcElement('date', dcDate($article->getTimestamp())); + + $last_editor = $article->getUser(); + + if ($last_editor == 0) { + dcPerson('creator', 0); + } else { + dcPerson('creator', $last_editor, $article->getUserText(), + User::whoIsReal($last_editor)); + } + + $contributors = $article->getContributors(); + + foreach ($contributors as $user_parts) { + dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]); + } + + dcRights($article); +} + +/** + * @private + */ +function ccPrologue() { + global $wgOutputEncoding; + + echo "<" . "?xml version='1.0' encoding='{$wgOutputEncoding}' ?" . "> + + + "; +} + +/** + * @private + */ +function ccSubPrologue($type, $url) { + $url = htmlspecialchars( $url ); + echo " \n"; +} + +/** + * @private + */ +function ccSubEpilogue($type) { + echo " \n"; +} + +/** + * @private + */ +function ccLicense($terms) { + + foreach ($terms as $term) { + switch ($term) { + case 're': + ccTerm('permits', 'Reproduction'); break; + case 'di': + ccTerm('permits', 'Distribution'); break; + case 'de': + ccTerm('permits', 'DerivativeWorks'); break; + case 'nc': + ccTerm('prohibits', 'CommercialUse'); break; + case 'no': + ccTerm('requires', 'Notice'); break; + case 'by': + ccTerm('requires', 'Attribution'); break; + case 'sa': + ccTerm('requires', 'ShareAlike'); break; + case 'sc': + ccTerm('requires', 'SourceCode'); break; + } + } +} + +/** + * @private + */ +function ccTerm($term, $name) { + print " \n"; +} + +/** + * @private + */ +function ccEpilogue() { + echo "\n"; +} + +/** + * @private + */ +function dcElement($name, $value) { + $value = htmlspecialchars( $value ); + print " {$value}\n"; +} + +/** + * @private + */ +function dcDate($timestamp) { + return substr($timestamp, 0, 4) . '-' + . substr($timestamp, 4, 2) . '-' + . substr($timestamp, 6, 2); +} + +/** + * @private + */ +function dcReallyFullUrl($title) { + return $title->getFullURL(); +} + +/** + * @private + */ +function dcPageOrString($name, $page, $str) { + $nt = Title::newFromText($page); + + if (!$nt || $nt->getArticleID() == 0) { + dcElement($name, $str); + } else { + dcPage($name, $nt); + } +} + +/** + * @private + */ +function dcPage($name, $title) { + dcUrl($name, dcReallyFullUrl($title)); +} + +/** + * @private + */ +function dcUrl($name, $url) { + $url = htmlspecialchars( $url ); + print " \n"; +} + +/** + * @private + */ +function dcPerson($name, $id, $user_name='', $user_real_name='') { + global $wgContLang; + + if ($id == 0) { + dcElement($name, wfMsg('anonymous')); + } else if ( !empty($user_real_name) ) { + dcElement($name, $user_real_name); + } else { + # XXX: This shouldn't happen. + if( empty( $user_name ) ) { + $user_name = User::whoIs($id); + } + dcPageOrString($name, $wgContLang->getNsText(NS_USER) . ':' . $user_name, wfMsg('siteuser', $user_name)); + } +} + +/** + * Takes an arg, for future enhancement with different rights for + * different pages. + * @private + */ +function dcRights($article) { + + global $wgRightsPage, $wgRightsUrl, $wgRightsText; + + if (isset($wgRightsPage) && + ($nt = Title::newFromText($wgRightsPage)) + && ($nt->getArticleID() != 0)) { + dcPage('rights', $nt); + } else if (isset($wgRightsUrl)) { + dcUrl('rights', $wgRightsUrl); + } else if (isset($wgRightsText)) { + dcElement('rights', $wgRightsText); + } +} + +/** + * @private + */ +function ccGetTerms($url) { + global $wgLicenseTerms; + + if (isset($wgLicenseTerms)) { + return $wgLicenseTerms; + } else { + $known = getKnownLicenses(); + return $known[$url]; + } +} + +/** + * @private + */ +function getKnownLicenses() { + + $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc', + 'by-nc-sa', 'by-sa'); + $ccVersions = array('1.0', '2.0'); + $knownLicenses = array(); + + foreach ($ccVersions as $version) { + foreach ($ccLicenses as $license) { + if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) { + # 2.0 dropped the non-attribs licenses + continue; + } + $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/"; + $knownLicenses[$lurl] = explode('-', $license); + $knownLicenses[$lurl][] = 're'; + $knownLicenses[$lurl][] = 'di'; + $knownLicenses[$lurl][] = 'no'; + if (!in_array('nd', $knownLicenses[$lurl])) { + $knownLicenses[$lurl][] = 'de'; + } + } + } + + /* Handle the GPL and LGPL, too. */ + + $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); + $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); + $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); + + return $knownLicenses; +} + +?> diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php new file mode 100644 index 00000000..30861ba3 --- /dev/null +++ b/includes/MimeMagic.php @@ -0,0 +1,695 @@ +mMimeToExt= array(); + $this->mToMime= array(); + + $lines= explode("\n",$types); + foreach ($lines as $s) { + $s= trim($s); + if (empty($s)) continue; + if (strpos($s,'#')===0) continue; + + $s= strtolower($s); + $i= strpos($s,' '); + + if ($i===false) continue; + + #print "processing MIME line $s
    "; + + $mime= substr($s,0,$i); + $ext= trim(substr($s,$i+1)); + + if (empty($ext)) continue; + + if (@$this->mMimeToExt[$mime]) $this->mMimeToExt[$mime] .= ' '.$ext; + else $this->mMimeToExt[$mime]= $ext; + + $extensions= explode(' ',$ext); + + foreach ($extensions as $e) { + $e= trim($e); + if (empty($e)) continue; + + if (@$this->mExtToMime[$e]) $this->mExtToMime[$e] .= ' '.$mime; + else $this->mExtToMime[$e]= $mime; + } + } + + /* + * --- load mime.info --- + */ + + global $wgMimeInfoFile; + + $info= MM_WELL_KNOWN_MIME_INFO; + + if ($wgMimeInfoFile) { + if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) { + wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n"); + + $info.= "\n"; + $info.= file_get_contents($wgMimeInfoFile); + } + else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n"); + } + else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n"); + + $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info); + $info= str_replace("\t"," ",$info); + + $this->mMimeTypeAliases= array(); + $this->mMediaTypes= array(); + + $lines= explode("\n",$info); + foreach ($lines as $s) { + $s= trim($s); + if (empty($s)) continue; + if (strpos($s,'#')===0) continue; + + $s= strtolower($s); + $i= strpos($s,' '); + + if ($i===false) continue; + + #print "processing MIME INFO line $s
    "; + + $match= array(); + if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) { + $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s); + $mtype= trim(strtoupper($match[1])); + } + else $mtype= MEDIATYPE_UNKNOWN; + + $m= explode(' ',$s); + + if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array(); + + foreach ($m as $mime) { + $mime= trim($mime); + if (empty($mime)) continue; + + $this->mMediaTypes[$mtype][]= $mime; + } + + if (sizeof($m)>1) { + $main= $m[0]; + for ($i=1; $imMimeTypeAliases[$mime]= $main; + } + } + } + + } + + /** returns a list of file extensions for a given mime type + * as a space separated string. + */ + function getExtensionsForType($mime) { + $mime= strtolower($mime); + + $r= @$this->mMimeToExt[$mime]; + + if (@!$r and isset($this->mMimeTypeAliases[$mime])) { + $mime= $this->mMimeTypeAliases[$mime]; + $r= @$this->mMimeToExt[$mime]; + } + + return $r; + } + + /** returns a list of mime types for a given file extension + * as a space separated string. + */ + function getTypesForExtension($ext) { + $ext= strtolower($ext); + + $r= @$this->mExtToMime[$ext]; + return $r; + } + + /** returns a single mime type for a given file extension. + * This is always the first type from the list returned by getTypesForExtension($ext). + */ + function guessTypesForExtension($ext) { + $m= $this->getTypesForExtension( $ext ); + if( is_null($m) ) return NULL; + + $m= trim( $m ); + $m= preg_replace('/\s.*$/','',$m); + + return $m; + } + + + /** tests if the extension matches the given mime type. + * returns true if a match was found, NULL if the mime type is unknown, + * and false if the mime type is known but no matches where found. + */ + function isMatchingExtension($extension,$mime) { + $ext= $this->getExtensionsForType($mime); + + if (!$ext) { + return NULL; //unknown + } + + $ext= explode(' ',$ext); + + $extension= strtolower($extension); + if (in_array($extension,$ext)) { + return true; + } + + return false; + } + + /** returns true if the mime type is known to represent + * an image format supported by the PHP GD library. + */ + function isPHPImageType( $mime ) { + #as defined by imagegetsize and image_type_to_mime + static $types = array( + 'image/gif', 'image/jpeg', 'image/png', + 'image/x-bmp', 'image/xbm', 'image/tiff', + 'image/jp2', 'image/jpeg2000', 'image/iff', + 'image/xbm', 'image/x-xbitmap', + 'image/vnd.wap.wbmp', 'image/vnd.xiff', + 'image/x-photoshop', + 'application/x-shockwave-flash', + ); + + return in_array( $mime, $types ); + } + + /** + * Returns true if the extension represents a type which can + * be reliably detected from its content. Use this to determine + * whether strict content checks should be applied to reject + * invalid uploads; if we can't identify the type we won't + * be able to say if it's invalid. + * + * @todo Be more accurate when using fancy mime detector plugins; + * right now this is the bare minimum getimagesize() list. + * @return bool + */ + function isRecognizableExtension( $extension ) { + static $types = array( + 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd', + 'bmp', 'tiff', 'tif', 'jpc', 'jp2', + 'jpx', 'jb2', 'swc', 'iff', 'wbmp', + 'xbm', 'djvu' + ); + return in_array( strtolower( $extension ), $types ); + } + + + /** mime type detection. This uses detectMimeType to detect the mim type of the file, + * but applies additional checks to determine some well known file formats that may be missed + * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG). + * + * @param string $file The file to check + * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * + * @return string the mime type of $file + */ + function guessMimeType( $file, $useExt=true ) { + $fname = 'MimeMagic::guessMimeType'; + $mime= $this->detectMimeType($file,$useExt); + + // Read a chunk of the file + $f = fopen( $file, "rt" ); + if( !$f ) return "unknown/unknown"; + $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 + // 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"; + } + + 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)=="%sim',$head,$match)) $doctype= $match[1]; + if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1]; + + #print "
    ANALYSING $file ($mime): doctype= $doctype; tag= $tag
    "; + + if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg"; + elseif ($tag==="svg") $mime= "image/svg"; + elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html"; + elseif ($tag==="html") $mime= "text/html"; + + $test_more= false; + } + } + + /* + * 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(); + $prog= ""; + + if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) $script= $match[2]; + + $mime= "application/x-$prog"; + } + } + + /* + * look for PHP + */ + if( !$xml_type && !$script_type ) { + + if( ( strpos( $head, 'mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime]; + + wfDebug("$fname: final mime type of $file: $mime\n"); + return $mime; + } + + /** Internal mime type detection, please use guessMimeType() for application code instead. + * Detection is done using an external program, if $wgMimeDetectorCommand is set. + * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available. + * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension. + * If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image. + * If no mime type can be determined, this function returns "unknown/unknown". + * + * @param string $file The file to check + * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * + * @return string the mime type of $file + * @access private + */ + function detectMimeType( $file, $useExt=true ) { + $fname = 'MimeMagic::detectMimeType'; + + global $wgMimeDetectorCommand; + + $m= NULL; + if ($wgMimeDetectorCommand) { + $fn= wfEscapeShellArg($file); + $m= `$wgMimeDetectorCommand $fn`; + } + else if (function_exists("finfo_open") && function_exists("finfo_file")) { + + # This required the fileinfo extension by PECL, + # see http://pecl.php.net/package/fileinfo + # This must be compiled into PHP + # + # finfo is the official replacement for the deprecated + # mime_content_type function, see below. + # + # If you may need to load the fileinfo extension at runtime, set + # $wgLoadFileinfoExtension in LocalSettings.php + + $mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */ + + if ($mime_magic_resource) { + $m= finfo_file($mime_magic_resource, $file); + + finfo_close($mime_magic_resource); + } + else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n"); + } + else if (function_exists("mime_content_type")) { + + # NOTE: this function is available since PHP 4.3.0, but only if + # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic. + # + # On Winodws, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; + # sometimes, this may even be needed under linus/unix. + # + # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above. + # see http://www.php.net/manual/en/ref.mime-magic.php for details. + + $m= mime_content_type($file); + } + else wfDebug("$fname: no magic mime detector found!\n"); + + if ($m) { + #normalize + $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc + $m= trim($m); + $m= strtolower($m); + + if (strpos($m,'unknown')!==false) $m= NULL; + else { + wfDebug("$fname: magic mime type of $file: $m\n"); + return $m; + } + } + + #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("$fname: image mime type of $file: $m\n"); + return $m; + } + else $notAnImage= true; + } else { + // Also test DjVu + $deja = new DjVuImage( $file ); + if( $deja->isValid() ) { + wfDebug("$fname: detected $file as image/vnd.djvu\n"); + return 'image/vnd.djvu'; + } + } + + #if desired, look at extension as a fallback. + if ($useExt) { + $i = strrpos( $file, '.' ); + $e= strtolower( $i ? substr( $file, $i + 1 ) : '' ); + + $m= $this->guessTypesForExtension($e); + + #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("$fname: extension mime type of $file: $m\n"); + return $m; + } + } + + #unknown type + wfDebug("$fname: failed to guess mime type for $file!\n"); + return "unknown/unknown"; + } + + /** + * Determine the media type code for a file, using its mime type, name and possibly + * its contents. + * + * This function relies on the findMediaType(), mapping extensions and mime + * types to media types. + * + * @todo analyse file if need be + * @todo look at multiple extension, separately and together. + * + * @param string $path full path to the image file, in case we have to look at the contents + * (if null, only the mime type is used to determine the media type code). + * @param string $mime mime type. If null it will be guessed using guessMimeType. + * + * @return (int?string?) a value to be used with the MEDIATYPE_xxx constants. + */ + function getMediaType($path=NULL,$mime=NULL) { + if( !$mime && !$path ) return MEDIATYPE_UNKNOWN; + + #if mime type is unknown, guess it + if( !$mime ) $mime= $this->guessMimeType($path,false); + + #special code for ogg - detect if it's video (theora), + #else label it as sound. + if( $mime=="application/ogg" && file_exists($path) ) { + + // Read a chunk of the file + $f = fopen( $path, "rt" ); + if( !$f ) return MEDIATYPE_UNKNOWN; + $head = fread( $f, 256 ); + fclose( $f ); + + $head= strtolower( $head ); + + #This is an UGLY HACK, file should be parsed correctly + if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO; + elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO; + elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO; + elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO; + else return MEDIATYPE_MULTIMEDIA; + } + + #check for entry for full mime type + if( $mime ) { + $type= $this->findMediaType($mime); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + + #check for entry for file extension + $e= NULL; + if( $path ) { + $i = strrpos( $path, '.' ); + $e= strtolower( $i ? substr( $path, $i + 1 ) : '' ); + + #TODO: look at multi-extension if this fails, parse from full path + + $type= $this->findMediaType('.'.$e); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + + #check major mime type + if( $mime ) { + $i= strpos($mime,'/'); + if( $i !== false ) { + $major= substr($mime,0,$i); + $type= $this->findMediaType($major); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + } + + if( !$type ) $type= MEDIATYPE_UNKNOWN; + + return $type; + } + + /** returns a media code matching the given mime type or file extension. + * File extensions are represented by a string starting with a dot (.) to + * distinguish them from mime types. + * + * This funktion relies on the mapping defined by $this->mMediaTypes + * @access private + */ + function findMediaType($extMime) { + + if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types + $m= $this->getTypesForExtension(substr($extMime,1)); + if (!$m) return MEDIATYPE_UNKNOWN; + + $m= explode(' ',$m); + } + else { #normalize mime type + if (isset($this->mMimeTypeAliases[$extMime])) { + $extMime= $this->mMimeTypeAliases[$extMime]; + } + + $m= array($extMime); + } + + foreach ($m as $mime) { + foreach ($this->mMediaTypes as $type => $codes) { + if (in_array($mime,$codes,true)) return $type; + } + } + + return MEDIATYPE_UNKNOWN; + } +} + +?> diff --git a/includes/Namespace.php b/includes/Namespace.php new file mode 100644 index 00000000..ab7511d0 --- /dev/null +++ b/includes/Namespace.php @@ -0,0 +1,129 @@ + 'Media', + NS_SPECIAL => 'Special', + NS_TALK => 'Talk', + NS_USER => 'User', + NS_USER_TALK => 'User_talk', + NS_PROJECT => 'Project', + NS_PROJECT_TALK => 'Project_talk', + NS_IMAGE => 'Image', + NS_IMAGE_TALK => 'Image_talk', + NS_MEDIAWIKI => 'MediaWiki', + NS_MEDIAWIKI_TALK => 'MediaWiki_talk', + NS_TEMPLATE => 'Template', + NS_TEMPLATE_TALK => 'Template_talk', + NS_HELP => 'Help', + NS_HELP_TALK => 'Help_talk', + NS_CATEGORY => 'Category', + NS_CATEGORY_TALK => 'Category_talk', +); + +if( is_array( $wgExtraNamespaces ) ) { + $wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces; +} + +/** + * This is a utility class with only static functions + * for dealing with namespaces that encodes all the + * "magic" behaviors of them based on index. The textual + * names of the namespaces are handled by Language.php. + * + * These are synonyms for the names given in the language file + * Users and translators should not change them + * + * @package MediaWiki + */ +class Namespace { + + /** + * Check if the given namespace might be moved + * @return bool + */ + function isMovable( $index ) { + return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ); + } + + /** + * Check if the given namespace is not a talk page + * @return bool + */ + function isMain( $index ) { + return ! Namespace::isTalk( $index ); + } + + /** + * Check if the give namespace is a talk page + * @return bool + */ + function isTalk( $index ) { + return ($index > NS_MAIN) // Special namespaces are negative + && ($index % 2); // Talk namespaces are odd-numbered + } + + /** + * Get the talk namespace corresponding to the given index + */ + function getTalk( $index ) { + if ( Namespace::isTalk( $index ) ) { + return $index; + } else { + # FIXME + return $index + 1; + } + } + + function getSubject( $index ) { + if ( Namespace::isTalk( $index ) ) { + return $index - 1; + } else { + return $index; + } + } + + /** + * Returns the canonical (English Wikipedia) name for a given index + */ + function getCanonicalName( $index ) { + global $wgCanonicalNamespaceNames; + return $wgCanonicalNamespaceNames[$index]; + } + + /** + * Returns the index for a given canonical name, or NULL + * The input *must* be converted to lower case first + */ + function getCanonicalIndex( $name ) { + global $wgCanonicalNamespaceNames; + static $xNamespaces = false; + if ( $xNamespaces === false ) { + $xNamespaces = array(); + foreach ( $wgCanonicalNamespaceNames as $i => $text ) { + $xNamespaces[strtolower($text)] = $i; + } + } + if ( array_key_exists( $name, $xNamespaces ) ) { + return $xNamespaces[$name]; + } else { + return NULL; + } + } + + /** + * Can this namespace ever have a talk namespace? + * @param $index Namespace index + */ + function canTalk( $index ) { + return( $index >= NS_MAIN ); + } +} +?> diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php new file mode 100644 index 00000000..fe7417d2 --- /dev/null +++ b/includes/ObjectCache.php @@ -0,0 +1,125 @@ + $wgMemCachedPersistent, 'compress_threshold' => 1500 ) ); + $cache =& $wgCaches[CACHE_DB]; + $cache->set_servers( $wgMemCachedServers ); + $cache->set_debug( $wgMemCachedDebug ); + } + } elseif ( $type == CACHE_ACCEL ) { + if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) { + if ( function_exists( 'eaccelerator_get' ) ) { + require_once( 'BagOStuff.php' ); + $wgCaches[CACHE_ACCEL] = new eAccelBagOStuff; + } elseif ( function_exists( 'apc_fetch') ) { + require_once( 'BagOStuff.php' ); + $wgCaches[CACHE_ACCEL] = new APCBagOStuff; + } elseif ( function_exists( 'mmcache_get' ) ) { + require_once( 'BagOStuff.php' ); + $wgCaches[CACHE_ACCEL] = new TurckBagOStuff; + } else { + $wgCaches[CACHE_ACCEL] = false; + } + } + if ( $wgCaches[CACHE_ACCEL] !== false ) { + $cache =& $wgCaches[CACHE_ACCEL]; + } + } + + if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) { + if ( !array_key_exists( CACHE_DB, $wgCaches ) ) { + require_once( 'BagOStuff.php' ); + $wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache'); + } + $cache =& $wgCaches[CACHE_DB]; + } + + if ( $cache === false ) { + if ( !array_key_exists( CACHE_NONE, $wgCaches ) ) { + $wgCaches[CACHE_NONE] = new FakeMemCachedClient; + } + $cache =& $wgCaches[CACHE_NONE]; + } + + return $cache; +} + +function &wfGetMainCache() { + global $wgMainCacheType; + $ret =& wfGetCache( $wgMainCacheType ); + return $ret; +} + +function &wfGetMessageCacheStorage() { + global $wgMessageCacheType; + $ret =& wfGetCache( $wgMessageCacheType ); + return $ret; +} + +function &wfGetParserCacheStorage() { + global $wgParserCacheType; + $ret =& wfGetCache( $wgParserCacheType ); + return $ret; +} + +?> diff --git a/includes/OutputPage.php b/includes/OutputPage.php new file mode 100644 index 00000000..31a0781a --- /dev/null +++ b/includes/OutputPage.php @@ -0,0 +1,1078 @@ +mMetatags = $this->mKeywords = $this->mLinktags = array(); + $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext = + $this->mRedirect = $this->mLastModified = + $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy = + $this->mOnloadHandler = $this->mPageLinkTitle = ''; + $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true; + $this->mSuppressQuickbar = $this->mPrintable = false; + $this->mLanguageLinks = array(); + $this->mCategoryLinks = array(); + $this->mDoNothing = false; + $this->mContainsOldMagic = $this->mContainsNewMagic = 0; + $this->mParserOptions = ParserOptions::newFromUser( $temp = NULL ); + $this->mSquidMaxage = 0; + $this->mScripts = ''; + $this->mETag = false; + $this->mRevisionId = null; + $this->mNewSectionLink = false; + } + + function redirect( $url, $responsecode = '302' ) { + # Strip newlines as a paranoia check for header injection in PHP<5.1.2 + $this->mRedirect = str_replace( "\n", '', $url ); + $this->mRedirectCode = $responsecode; + } + + function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; } + + # To add an http-equiv meta tag, precede the name with "http:" + function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } + function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } + function addScript( $script ) { $this->mScripts .= $script; } + function getScript() { return $this->mScripts; } + + function setETag($tag) { $this->mETag = $tag; } + function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; } + function getArticleBodyOnly($only) { return $this->mArticleBodyOnly; } + + function addLink( $linkarr ) { + # $linkarr should be an associative array of attributes. We'll escape on output. + array_push( $this->mLinktags, $linkarr ); + } + + function addMetadataLink( $linkarr ) { + # note: buggy CC software only reads first "meta" link + static $haveMeta = false; + $linkarr['rel'] = ($haveMeta) ? 'alternate meta' : 'meta'; + $this->addLink( $linkarr ); + $haveMeta = true; + } + + /** + * checkLastModified tells the client to use the client-cached page if + * possible. If sucessful, the OutputPage is disabled so that + * any future call to OutputPage->output() have no effect. The method + * returns true iff cache-ok headers was sent. + */ + function checkLastModified ( $timestamp ) { + global $wgCachePages, $wgCacheEpoch, $wgUser; + $fname = 'OutputPage::checkLastModified'; + + if ( !$timestamp || $timestamp == '19700101000000' ) { + wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); + return; + } + if( !$wgCachePages ) { + wfDebug( "$fname: CACHE DISABLED\n", false ); + return; + } + if( $wgUser->getOption( 'nocache' ) ) { + wfDebug( "$fname: USER DISABLED CACHE\n", false ); + return; + } + + $timestamp=wfTimestamp(TS_MW,$timestamp); + $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); + + if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + # IE sends sizes after the date like this: + # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 + # this breaks strtotime(). + $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + $modsinceTime = strtotime( $modsince ); + $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); + wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); + wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); + if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + # Make sure you're in a place you can leave when you call us! + header( "HTTP/1.0 304 Not Modified" ); + $this->mLastModified = $lastmod; + $this->sendCacheControl(); + wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); + $this->disable(); + @ob_end_clean(); // Don't output compressed blob + return true; + } else { + wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); + $this->mLastModified = $lastmod; + } + } else { + wfDebug( "$fname: client did not send If-Modified-Since header\n", false ); + $this->mLastModified = $lastmod; + } + } + + function getPageTitleActionText () { + global $action; + switch($action) { + case 'edit': + case 'delete': + case 'protect': + case 'unprotect': + case 'watch': + case 'unwatch': + // Display title is already customized + return ''; + case 'history': + return wfMsg('history_short'); + case 'submit': + // FIXME: bug 2735; not correct for special pages etc + return wfMsg('preview'); + case 'info': + return wfMsg('info_short'); + default: + return ''; + } + } + + function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } + function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } + function setPageTitle( $name ) { + global $action, $wgContLang; + $name = $wgContLang->convert($name, true); + $this->mPagetitle = $name; + if(!empty($action)) { + $taction = $this->getPageTitleActionText(); + if( !empty( $taction ) ) { + $name .= ' - '.$taction; + } + } + + $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) ); + } + function getHTMLTitle() { return $this->mHTMLtitle; } + function getPageTitle() { return $this->mPagetitle; } + function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 + function getSubtitle() { return $this->mSubtitle; } + function isArticle() { return $this->mIsarticle; } + function setPrintable() { $this->mPrintable = true; } + function isPrintable() { return $this->mPrintable; } + function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } + function isSyndicated() { return $this->mShowFeedLinks; } + function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } + function getOnloadHandler() { return $this->mOnloadHandler; } + function disable() { $this->mDoNothing = true; } + + function setArticleRelated( $v ) { + $this->mIsArticleRelated = $v; + if ( !$v ) { + $this->mIsarticle = false; + } + } + function setArticleFlag( $v ) { + $this->mIsarticle = $v; + if ( $v ) { + $this->mIsArticleRelated = $v; + } + } + + function isArticleRelated() { return $this->mIsArticleRelated; } + + function getLanguageLinks() { return $this->mLanguageLinks; } + function addLanguageLinks($newLinkArray) { + $this->mLanguageLinks += $newLinkArray; + } + function setLanguageLinks($newLinkArray) { + $this->mLanguageLinks = $newLinkArray; + } + + function getCategoryLinks() { + return $this->mCategoryLinks; + } + + /** + * Add an array of categories, with names in the keys + */ + function addCategoryLinks($categories) { + global $wgUser, $wgContLang; + + if ( !is_array( $categories ) ) { + return; + } + # Add the links to the link cache in a batch + $arr = array( NS_CATEGORY => $categories ); + $lb = new LinkBatch; + $lb->setArray( $arr ); + $lb->execute(); + + $sk =& $wgUser->getSkin(); + foreach ( $categories as $category => $arbitrary ) { + $title = Title::makeTitleSafe( NS_CATEGORY, $category ); + $text = $wgContLang->convertHtml( $title->getText() ); + $this->mCategoryLinks[] = $sk->makeLinkObj( $title, $text ); + } + } + + function setCategoryLinks($categories) { + $this->mCategoryLinks = array(); + $this->addCategoryLinks($categories); + } + + function suppressQuickbar() { $this->mSuppressQuickbar = true; } + function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } + + function addHTML( $text ) { $this->mBodytext .= $text; } + function clearHTML() { $this->mBodytext = ''; } + function getHTML() { return $this->mBodytext; } + function debug( $text ) { $this->mDebugtext .= $text; } + + /* @deprecated */ + function setParserOptions( $options ) { + return $this->ParserOptions( $options ); + } + + function ParserOptions( $options = null ) { + return wfSetVar( $this->mParserOptions, $options ); + } + + /** + * Set the revision ID which will be seen by the wiki text parser + * for things such as embedded {{REVISIONID}} variable use. + * @param mixed $revid an integer, or NULL + * @return mixed previous value + */ + function setRevisionId( $revid ) { + $val = is_null( $revid ) ? null : intval( $revid ); + return wfSetVar( $this->mRevisionId, $val ); + } + + /** + * Convert wikitext to HTML and add it to the buffer + * Default assumes that the current page title will + * be used. + */ + function addWikiText( $text, $linestart = true ) { + global $wgTitle; + $this->addWikiTextTitle($text, $wgTitle, $linestart); + } + + function addWikiTextWithTitle($text, &$title, $linestart = true) { + $this->addWikiTextTitle($text, $title, $linestart); + } + + function addWikiTextTitle($text, &$title, $linestart) { + global $wgParser; + $parserOutput = $wgParser->parse( $text, $title, $this->mParserOptions, + $linestart, true, $this->mRevisionId ); + $this->addParserOutput( $parserOutput ); + } + + function addParserOutputNoText( &$parserOutput ) { + $this->mLanguageLinks += $parserOutput->getLanguageLinks(); + $this->addCategoryLinks( $parserOutput->getCategories() ); + $this->mNewSectionLink = $parserOutput->getNewSection(); + $this->addKeywords( $parserOutput ); + if ( $parserOutput->getCacheTime() == -1 ) { + $this->enableClientCache( false ); + } + if ( $parserOutput->mHTMLtitle != "" ) { + $this->mPagetitle = $parserOutput->mHTMLtitle ; + $this->mSubtitle .= $parserOutput->mSubtitle ; + } + } + + function addParserOutput( &$parserOutput ) { + $this->addParserOutputNoText( $parserOutput ); + $this->addHTML( $parserOutput->getText() ); + } + + /** + * Add wikitext to the buffer, assuming that this is the primary text for a page view + * Saves the text into the parser cache if possible + */ + function addPrimaryWikiText( $text, $article, $cache = true ) { + global $wgParser, $wgUser; + + $this->mParserOptions->setTidy(true); + $parserOutput = $wgParser->parse( $text, $article->mTitle, + $this->mParserOptions, true, true, $this->mRevisionId ); + $this->mParserOptions->setTidy(false); + if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $parserOutput, $article, $wgUser ); + } + + $this->addParserOutputNoText( $parserOutput ); + $text = $parserOutput->getText(); + $this->mNoGallery = $parserOutput->getNoGallery(); + wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) ); + $parserOutput->setText( $text ); + $this->addHTML( $parserOutput->getText() ); + } + + /** + * For anything that isn't primary text or interface message + */ + function addSecondaryWikiText( $text, $linestart = true ) { + global $wgTitle; + $this->mParserOptions->setTidy(true); + $this->addWikiTextTitle($text, $wgTitle, $linestart); + $this->mParserOptions->setTidy(false); + } + + + /** + * Add the output of a QuickTemplate to the output buffer + * @param QuickTemplate $template + */ + function addTemplate( &$template ) { + ob_start(); + $template->execute(); + $this->addHTML( ob_get_contents() ); + ob_end_clean(); + } + + /** + * Parse wikitext and return the HTML. + */ + function parse( $text, $linestart = true, $interface = false ) { + global $wgParser, $wgTitle; + if ( $interface) { $this->mParserOptions->setInterfaceMessage(true); } + $parserOutput = $wgParser->parse( $text, $wgTitle, $this->mParserOptions, + $linestart, true, $this->mRevisionId ); + if ( $interface) { $this->mParserOptions->setInterfaceMessage(false); } + return $parserOutput->getText(); + } + + /** + * @param $article + * @param $user + * + * @return bool + */ + function tryParserCache( &$article, $user ) { + $parserCache =& ParserCache::singleton(); + $parserOutput = $parserCache->get( $article, $user ); + if ( $parserOutput !== false ) { + $this->mLanguageLinks += $parserOutput->getLanguageLinks(); + $this->addCategoryLinks( $parserOutput->getCategories() ); + $this->addKeywords( $parserOutput ); + $this->mNewSectionLink = $parserOutput->getNewSection(); + $this->mNoGallery = $parserOutput->getNoGallery(); + $text = $parserOutput->getText(); + wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) ); + $this->addHTML( $text ); + $t = $parserOutput->getTitleText(); + if( !empty( $t ) ) { + $this->setPageTitle( $t ); + } + return true; + } else { + return false; + } + } + + /** + * Set the maximum cache time on the Squid in seconds + * @param $maxage + */ + function setSquidMaxage( $maxage ) { + $this->mSquidMaxage = $maxage; + } + + /** + * Use enableClientCache(false) to force it to send nocache headers + * @param $state + */ + function enableClientCache( $state ) { + return wfSetVar( $this->mEnableClientCache, $state ); + } + + function uncacheableBecauseRequestvars() { + global $wgRequest; + return $wgRequest->getText('useskin', false) === false + && $wgRequest->getText('uselang', false) === false; + } + + function sendCacheControl() { + global $wgUseSquid, $wgUseESI, $wgSquidMaxage; + $fname = 'OutputPage::sendCacheControl'; + + if ($this->mETag) + 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 + header( 'Vary: Accept-Encoding, Cookie' ); + if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { + if( $wgUseSquid && ! isset( $_COOKIE[ini_get( 'session.name') ] ) && + ! $this->isPrintable() && $this->mSquidMaxage != 0 ) + { + if ( $wgUseESI ) { + # We'll purge the proxy cache explicitly, but require end user agents + # to revalidate against the proxy on each visit. + # Surrogate-Control controls our Squid, Cache-Control downstream caches + 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"'); + header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); + 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. + # IMPORTANT! The Squid needs to replace the Cache-Control header with + # Cache-Control: s-maxage=0, must-revalidate, max-age=0 + 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" ); + 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 ); + header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + header( "Cache-Control: private, must-revalidate, max-age=0" ); + } + if($this->mLastModified) 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. + header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + header( 'Pragma: no-cache' ); + } + } + + /** + * Finally, all the text has been munged and accumulated into + * the object, let's actually output it: + */ + function output() { + global $wgUser, $wgOutputEncoding; + global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; + global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgScriptPath, $wgServer; + + if( $this->mDoNothing ){ + return; + } + $fname = 'OutputPage::output'; + wfProfileIn( $fname ); + $sk = $wgUser->getSkin(); + + if ( $wgUseAjax ) { + $this->addScript( "" ); + $this->addScript( "\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; + } + if( $this->mRedirectCode == '301') { + if( !$wgDebugRedirects ) { + header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); + } + $this->mLastModified = wfTimestamp( TS_RFC2822 ); + } + + $this->sendCacheControl(); + + if( $wgDebugRedirects ) { + $url = htmlspecialchars( $this->mRedirect ); + print "\n\nRedirect\n\n\n"; + print "

    Location: $url

    \n"; + print "\n\n"; + } else { + header( 'Location: '.$this->mRedirect ); + } + wfProfileOut( $fname ); + return; + } + elseif ( $this->mStatusCode ) + { + $statusMessage = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Request Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 507 => 'Insufficient Storage' + ); + + if ( $statusMessage[$this->mStatusCode] ) + header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); + } + + # Buffer output; final headers may depend on later processing + ob_start(); + + # Disable temporary placeholders, so that the skin produces HTML + $sk->postParseLinkColour( false ); + + header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); + header( 'Content-language: '.$wgContLanguageCode ); + + if ($this->mArticleBodyOnly) { + $this->out($this->mBodytext); + } else { + wfProfileIn( 'Output-skin' ); + $sk->outputPage( $this ); + wfProfileOut( 'Output-skin' ); + } + + $this->sendCacheControl(); + ob_end_flush(); + wfProfileOut( $fname ); + } + + function out( $ins ) { + global $wgInputEncoding, $wgOutputEncoding, $wgContLang; + if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) { + $outs = $ins; + } else { + $outs = $wgContLang->iconv( $wgInputEncoding, $wgOutputEncoding, $ins ); + if ( false === $outs ) { $outs = $ins; } + } + print $outs; + } + + function setEncodings() { + global $wgInputEncoding, $wgOutputEncoding; + global $wgUser, $wgContLang; + + $wgInputEncoding = strtolower( $wgInputEncoding ); + + if( $wgUser->getOption( 'altencoding' ) ) { + $wgContLang->setAltEncoding(); + return; + } + + if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) { + $wgOutputEncoding = strtolower( $wgOutputEncoding ); + return; + } + $wgOutputEncoding = $wgInputEncoding; + } + + /** + * Returns a HTML comment with the elapsed time since request. + * This method has no side effects. + * Use wfReportTime() instead. + * @return string + * @deprecated + */ + function reportTime() { + $time = wfReportTime(); + return $time; + } + + /** + * Produce a "user is blocked" page + */ + function blockedPage( $return = true ) { + global $wgUser, $wgContLang, $wgTitle; + + $this->setPageTitle( wfMsg( 'blockedtitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + + $id = $wgUser->blockedBy(); + $reason = $wgUser->blockedFor(); + $ip = wfGetIP(); + + if ( is_numeric( $id ) ) { + $name = User::whoIs( $id ); + } else { + $name = $id; + } + $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; + + $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name ) ); + + # Don't auto-return to special pages + if( $return ) { + $return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL; + $this->returnToMain( false, $return ); + } + } + + /** + * Note: these arguments are keys into wfMsg(), not text! + */ + function showErrorPage( $title, $msg ) { + global $wgTitle; + + $this->mDebugtext .= 'Original title: ' . + $wgTitle->getPrefixedText() . "\n"; + $this->setPageTitle( wfMsg( $title ) ); + $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + + $this->mBodytext = ''; + $this->addWikiText( wfMsg( $msg ) ); + $this->returnToMain( false ); + } + + /** @obsolete */ + function errorpage( $title, $msg ) { + throw new ErrorPageError( $title, $msg ); + } + + /** + * Display an error page indicating that a given version of MediaWiki is + * required to use it + * + * @param mixed $version The version of MediaWiki needed to use the page + */ + function versionRequired( $version ) { + $this->setPageTitle( wfMsg( 'versionrequired', $version ) ); + $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->mBodytext = ''; + + $this->addWikiText( wfMsg( 'versionrequiredtext', $version ) ); + $this->returnToMain(); + } + + /** + * Display an error page noting that a given permission bit is required. + * This should generally replace the sysopRequired, developerRequired etc. + * @param string $permission key required + */ + function permissionRequired( $permission ) { + global $wgUser; + + $this->setPageTitle( wfMsg( 'badaccess' ) ); + $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->mBodytext = ''; + + $sk = $wgUser->getSkin(); + $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ) ); + $this->addHTML( wfMsgHtml( 'badaccesstext', $ap, $permission ) ); + $this->returnToMain(); + } + + /** + * @deprecated + */ + function sysopRequired() { + global $wgUser; + + $this->setPageTitle( wfMsg( 'sysoptitle' ) ); + $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->mBodytext = ''; + + $sk = $wgUser->getSkin(); + $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); + $this->addHTML( wfMsgHtml( 'sysoptext', $ap ) ); + $this->returnToMain(); + } + + /** + * @deprecated + */ + function developerRequired() { + global $wgUser; + + $this->setPageTitle( wfMsg( 'developertitle' ) ); + $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->mBodytext = ''; + + $sk = $wgUser->getSkin(); + $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); + $this->addHTML( wfMsgHtml( 'developertext', $ap ) ); + $this->returnToMain(); + } + + /** + * Produce the stock "please login to use the wiki" page + */ + function loginToUse() { + global $wgUser, $wgTitle, $wgContLang; + $skin = $wgUser->getSkin(); + + $this->setPageTitle( wfMsg( 'loginreqtitle' ) ); + $this->setHtmlTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotPolicy( 'noindex,nofollow' ); + $this->setArticleFlag( false ); + + $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); + $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); + $this->addHtml( "\n" ); + + $this->returnToMain(); + } + + /** @obsolete */ + function databaseError( $fname, $sql, $error, $errno ) { + throw new MWException( "OutputPage::databaseError is obsolete\n" ); + } + + function readOnlyPage( $source = null, $protected = false ) { + global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; + + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + + if( $protected ) { + $skin = $wgUser->getSkin(); + $this->setPageTitle( wfMsg( 'viewsource' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); + + # Determine if protection is due to the page being a system message + # and show an appropriate explanation + if( $wgTitle->getNamespace() == NS_MEDIAWIKI && !$wgUser->isAllowed( 'editinterface' ) ) { + $this->addWikiText( wfMsg( 'protectedinterface' ) ); + } else { + $this->addWikiText( wfMsg( 'protectedtext' ) ); + } + } else { + $this->setPageTitle( wfMsg( 'readonly' ) ); + if ( $wgReadOnly ) { + $reason = $wgReadOnly; + } else { + $reason = file_get_contents( $wgReadOnlyFile ); + } + $this->addWikiText( wfMsg( 'readonlytext', $reason ) ); + } + + if( is_string( $source ) ) { + if( strcmp( $source, '' ) == 0 ) { + global $wgTitle; + if ( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { + $source = wfMsgWeirdKey ( $wgTitle->getText() ); + } else { + $source = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + } + } + $rows = $wgUser->getIntOption( 'rows' ); + $cols = $wgUser->getIntOption( 'cols' ); + + $text = "\n"; + $this->addHTML( $text ); + } + + $this->returnToMain( false ); + } + + /** @obsolete */ + function fatalError( $message ) { + throw new FatalError( $message ); + } + + /** @obsolete */ + function unexpectedValueError( $name, $val ) { + throw new FatalError( wfMsg( 'unexpected', $name, $val ) ); + } + + /** @obsolete */ + function fileCopyError( $old, $new ) { + throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) ); + } + + /** @obsolete */ + function fileRenameError( $old, $new ) { + throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) ); + } + + /** @obsolete */ + function fileDeleteError( $name ) { + throw new FatalError( wfMsg( 'filedeleteerror', $name ) ); + } + + /** @obsolete */ + function fileNotFoundError( $name ) { + throw new FatalError( wfMsg( 'filenotfound', $name ) ); + } + + function showFatalError( $message ) { + $this->setPageTitle( wfMsg( "internalerror" ) ); + $this->setRobotpolicy( "noindex,nofollow" ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + $this->mBodytext = $message; + } + + function showUnexpectedValueError( $name, $val ) { + $this->showFatalError( wfMsg( 'unexpected', $name, $val ) ); + } + + function showFileCopyError( $old, $new ) { + $this->showFatalError( wfMsg( 'filecopyerror', $old, $new ) ); + } + + function showFileRenameError( $old, $new ) { + $this->showFatalError( wfMsg( 'filerenameerror', $old, $new ) ); + } + + function showFileDeleteError( $name ) { + $this->showFatalError( wfMsg( 'filedeleteerror', $name ) ); + } + + function showFileNotFoundError( $name ) { + $this->showFatalError( wfMsg( 'filenotfound', $name ) ); + } + + /** + * return from error messages or notes + * @param $auto automatically redirect the user after 10 seconds + * @param $returnto page title to return to. Default is Main Page. + */ + function returnToMain( $auto = true, $returnto = NULL ) { + global $wgUser, $wgOut, $wgRequest; + + if ( $returnto == NULL ) { + $returnto = $wgRequest->getText( 'returnto' ); + } + + if ( '' === $returnto ) { + $returnto = wfMsgForContent( 'mainpage' ); + } + + if ( is_object( $returnto ) ) { + $titleObj = $returnto; + } else { + $titleObj = Title::newFromText( $returnto ); + } + if ( !is_object( $titleObj ) ) { + $titleObj = Title::newMainPage(); + } + + $sk = $wgUser->getSkin(); + $link = $sk->makeLinkObj( $titleObj, '' ); + + $r = wfMsg( 'returnto', $link ); + if ( $auto ) { + $wgOut->addMeta( 'http:Refresh', '10;url=' . $titleObj->escapeFullURL() ); + } + $wgOut->addHTML( "\n

    $r

    \n" ); + } + + /** + * This function takes the title (first item of mGoodLinks), categories, existing and broken links for the page + * and uses the first 10 of them for META keywords + */ + function addKeywords( &$parserOutput ) { + global $wgTitle; + $this->addKeyword( $wgTitle->getPrefixedText() ); + $count = 1; + $links2d =& $parserOutput->getLinks(); + if ( !is_array( $links2d ) ) { + return; + } + foreach ( $links2d as $ns => $dbkeys ) { + foreach( $dbkeys as $dbkey => $id ) { + $this->addKeyword( $dbkey ); + if ( ++$count > 10 ) { + break 2; + } + } + } + } + + /** + * @access private + * @return string + */ + function headElement() { + global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType; + global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle; + + if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) { + $ret = "\n"; + } else { + $ret = ''; + } + + $ret .= "\n"; + + if ( '' == $this->getHTMLTitle() ) { + $this->setHTMLTitle( wfMsg( 'pagetitle', $this->getPageTitle() )); + } + + $rtl = $wgContLang->isRTL() ? " dir='RTL'" : ''; + $ret .= "\n"; + $ret .= "\n" . htmlspecialchars( $this->getHTMLTitle() ) . "\n"; + array_push( $this->mMetatags, array( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ) ); + + $ret .= $this->getHeadLinks(); + global $wgStylePath; + if( $this->isPrintable() ) { + $media = ''; + } else { + $media = "media='print'"; + } + $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css" ); + $ret .= "\n"; + + $sk = $wgUser->getSkin(); + $ret .= $sk->getHeadScripts(); + $ret .= $this->mScripts; + $ret .= $sk->getUserStyles(); + + if ($wgUseTrackbacks && $this->isArticleRelated()) + $ret .= $wgTitle->trackbackRDF(); + + $ret .= "\n"; + return $ret; + } + + function getHeadLinks() { + global $wgRequest; + $ret = ''; + foreach ( $this->mMetatags as $tag ) { + if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) { + $a = 'http-equiv'; + $tag[0] = substr( $tag[0], 5 ); + } else { + $a = 'name'; + } + $ret .= "\n"; + } + + $p = $this->mRobotpolicy; + if( $p !== '' && $p != 'index,follow' ) { + // http://www.robotstxt.org/wc/meta-user.html + // Only show if it's different from the default robots policy + $ret .= "\n"; + } + + if ( count( $this->mKeywords ) > 0 ) { + $strip = array( + "/<.*?>/" => '', + "/_/" => ' ' + ); + $ret .= "mKeywords ))) . "\" />\n"; + } + foreach ( $this->mLinktags as $tag ) { + $ret .= ' $val ) { + $ret .= " $attr=\"" . htmlspecialchars( $val ) . "\""; + } + $ret .= " />\n"; + } + if( $this->isSyndicated() ) { + # FIXME: centralize the mime-type and name information in Feed.php + $link = $wgRequest->escapeAppendQuery( 'feed=rss' ); + $ret .= "\n"; + $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); + $ret .= "\n"; + } + + return $ret; + } + + /** + * Turn off regular page output and return an error reponse + * for when rate limiting has triggered. + * @todo i18n + * @access 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.' ); + } + + /** + * Show an "add new section" link? + * + * @return bool True if the parser output instructs us to add one + */ + function showNewSectionLink() { + return $this->mNewSectionLink; + } + +} +?> diff --git a/includes/PageHistory.php b/includes/PageHistory.php new file mode 100644 index 00000000..de006285 --- /dev/null +++ b/includes/PageHistory.php @@ -0,0 +1,685 @@ +history() to print the + * history. + * + * @package MediaWiki + */ + +class PageHistory { + const DIR_PREV = 0; + const DIR_NEXT = 1; + + var $mArticle, $mTitle, $mSkin; + var $lastdate; + var $linesonpage; + var $mNotificationTimestamp; + var $mLatestId = null; + + /** + * Construct a new PageHistory. + * + * @param Article $article + * @returns nothing + */ + function PageHistory($article) { + global $wgUser; + + $this->mArticle =& $article; + $this->mTitle =& $article->mTitle; + $this->mNotificationTimestamp = NULL; + $this->mSkin = $wgUser->getSkin(); + + $this->defaultLimit = 50; + } + + /** + * Print the history page for an article. + * + * @returns nothing + */ + function history() { + global $wgOut, $wgRequest, $wgTitle; + + /* + * Allow client caching. + */ + + if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) ) + /* Client cache fresh and headers sent, nothing more to do. */ + return; + + $fname = 'PageHistory::history'; + wfProfileIn( $fname ); + + /* + * Setup page variables. + */ + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setArticleFlag( false ); + $wgOut->setArticleRelated( true ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSyndicated( true ); + + $logPage = Title::makeTitle( NS_SPECIAL, 'Log' ); + $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); + + $subtitle = wfMsgHtml( 'revhistory' ) . '
    ' . $logLink; + $wgOut->setSubtitle( $subtitle ); + + $feedType = $wgRequest->getVal( 'feed' ); + if( $feedType ) { + wfProfileOut( $fname ); + return $this->feed( $feedType ); + } + + /* + * Fail if article doesn't exist. + */ + if( !$this->mTitle->exists() ) { + $wgOut->addWikiText( wfMsg( 'nohistory' ) ); + wfProfileOut( $fname ); + return; + } + + $dbr =& wfGetDB(DB_SLAVE); + + /* + * Extract limit, the number of revisions to show, and + * offset, the timestamp to begin at, from the URL. + */ + $limit = $wgRequest->getInt('limit', $this->defaultLimit); + if ( $limit <= 0 ) { + $limit = $this->defaultLimit; + } elseif ( $limit > 50000 ) { + # Arbitrary maximum + # Any more than this and we'll probably get an out of memory error + $limit = 50000; + } + + $offset = $wgRequest->getText('offset'); + + /* Offset must be an integral. */ + if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset)) + $offset = 0; +# $offset = $dbr->timestamp($offset); + $dboffset = $offset === 0 ? 0 : $dbr->timestamp($offset); + /* + * "go=last" means to jump to the last history page. + */ + if (($gowhere = $wgRequest->getText("go")) !== NULL) { + $gourl = null; + switch ($gowhere) { + case "first": + if (($lastid = $this->getLastOffsetForPaging($this->mTitle->getArticleID(), $limit)) === NULL) + break; + $gourl = $wgTitle->getLocalURL("action=history&limit={$limit}&offset=". + wfTimestamp(TS_MW, $lastid)); + break; + } + + if (!is_null($gourl)) { + $wgOut->redirect($gourl); + return; + } + } + + /* + * Fetch revisions. + * + * If the user clicked "previous", we retrieve the revisions backwards, + * then reverse them. This is to avoid needing to know the timestamp of + * previous revisions when generating the URL. + */ + $direction = $this->getDirection(); + $revisions = $this->fetchRevisions($limit, $dboffset, $direction); + $navbar = $this->makeNavbar($revisions, $offset, $limit, $direction); + + /* + * We fetch one more revision than needed to get the timestamp of the + * one after this page (and to know if it exists). + * + * linesonpage stores the actual number of lines. + */ + if (count($revisions) < $limit + 1) + $this->linesonpage = count($revisions); + else + $this->linesonpage = count($revisions) - 1; + + /* Un-reverse revisions */ + if ($direction == PageHistory::DIR_PREV) + $revisions = array_reverse($revisions); + + /* + * Print the top navbar. + */ + $s = $navbar; + $s .= $this->beginHistoryList(); + $counter = 1; + + /* + * Print each revision, excluding the one-past-the-end, if any. + */ + foreach (array_slice($revisions, 0, $limit) as $i => $line) { + $latest = !$i && $offset == 0; + $firstInList = !$i; + $next = isset( $revisions[$i + 1] ) ? $revisions[$i + 1 ] : null; + $s .= $this->historyLine($line, $next, $counter, $this->getNotificationTimestamp(), $latest, $firstInList); + $counter++; + } + + /* + * End navbar. + */ + $s .= $this->endHistoryList(); + $s .= $navbar; + + $wgOut->addHTML( $s ); + wfProfileOut( $fname ); + } + + /** @todo document */ + function beginHistoryList() { + global $wgTitle; + $this->lastdate = ''; + $s = wfMsgExt( 'histlegend', array( 'parse') ); + $s .= '
    '; + $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey()); + + // The following line is SUPPOSED to have double-quotes around the + // $prefixedkey variable, because htmlspecialchars() doesn't escape + // single-quotes. + // + // On at least two occasions people have changed it to single-quotes, + // which creates invalid HTML and incorrect display of the resulting + // link. + // + // Please do not break this a third time. Thank you for your kind + // consideration and cooperation. + // + $s .= "\n"; + + $s .= $this->submitButton(); + $s .= '
      ' . "\n"; + return $s; + } + + /** @todo document */ + function endHistoryList() { + $s = '
    '; + $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); + $s .= '
    '; + return $s; + } + + /** @todo document */ + function submitButton( $bits = array() ) { + return ( $this->linesonpage > 0 ) + ? wfElement( 'input', array_merge( $bits, + array( + 'class' => 'historysubmit', + 'type' => 'submit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + 'value' => wfMsg( 'compareselectedversions' ), + ) ) ) + : ''; + } + + /** @todo document */ + function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) { + global $wgUser; + $rev = new Revision( $row ); + $rev->setTitle( $this->mTitle ); + + $s = '
  • '; + $curlink = $this->curLink( $rev, $latest ); + $lastlink = $this->lastLink( $rev, $next, $counter ); + $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); + $link = $this->revLink( $rev ); + + $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() ) + . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() ); + + $s .= "($curlink) ($lastlink) $arbitrary"; + + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $revdel = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' ); + if( $firstInList ) { + // We don't currently handle well changing the top revision's settings + $del = wfMsgHtml( 'rev-delundel' ); + } else { + $del = $this->mSkin->makeKnownLinkObj( $revdel, + wfMsg( 'rev-delundel' ), + 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $rev->getId() ) ); + } + $s .= "($del) "; + } + + $s .= " $link $user"; + + if( $row->rev_minor_edit ) { + $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); + } + + $s .= $this->mSkin->revComment( $rev ); + if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) { + $s .= ' ' . wfMsgHtml( 'updatedmarker' ) . ''; + } + if( $row->rev_deleted & Revision::DELETED_TEXT ) { + $s .= ' ' . wfMsgHtml( 'deletedrev' ); + } + $s .= "
  • \n"; + + return $s; + } + + /** @todo document */ + function revLink( $rev ) { + global $wgLang; + $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true ); + if( $rev->userCan( Revision::DELETED_TEXT ) ) { + $link = $this->mSkin->makeKnownLinkObj( + $this->mTitle, $date, "oldid=" . $rev->getId() ); + } else { + $link = $date; + } + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + return '' . $link . ''; + } + return $link; + } + + /** @todo document */ + function curLink( $rev, $latest ) { + $cur = wfMsgExt( 'cur', array( 'escape') ); + if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { + return $cur; + } else { + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, $cur, + 'diff=' . $this->getLatestID() . + "&oldid=" . $rev->getId() ); + } + } + + /** @todo document */ + function lastLink( $rev, $next, $counter ) { + $last = wfMsgExt( 'last', array( 'escape' ) ); + if( is_null( $next ) ) { + if( $rev->getTimestamp() == $this->getEarliestOffset() ) { + return $last; + } else { + // Cut off by paging; there are more behind us... + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid=prev" ); + } + } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) { + return $last; + } else { + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid={$next->rev_id}" + /*, + '', + '', + "tabindex={$counter}"*/ ); + } + } + + /** @todo document */ + 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 ) ) { + $radio['disabled'] = 'disabled'; + } + + /** @todo: move title texts to javascript */ + if ( $firstInList ) { + $first = wfElement( 'input', array_merge( + $radio, + array( + 'style' => 'visibility:hidden', + 'name' => 'oldid' ) ) ); + $checkmark = array( 'checked' => 'checked' ); + } else { + if( $counter == 2 ) { + $checkmark = array( 'checked' => 'checked' ); + } else { + $checkmark = array(); + } + $first = wfElement( 'input', array_merge( + $radio, + $checkmark, + array( 'name' => 'oldid' ) ) ); + $checkmark = array(); + } + $second = wfElement( 'input', array_merge( + $radio, + $checkmark, + array( 'name' => 'diff' ) ) ); + return $first . $second; + } else { + return ''; + } + } + + /** @todo document */ + function getLatestOffset( $id = null ) { + if ( $id === null) $id = $this->mTitle->getArticleID(); + return $this->getExtremeOffset( $id, 'max' ); + } + + /** @todo document */ + function getEarliestOffset( $id = null ) { + if ( $id === null) $id = $this->mTitle->getArticleID(); + return $this->getExtremeOffset( $id, 'min' ); + } + + /** @todo document */ + function getExtremeOffset( $id, $func ) { + $db =& wfGetDB(DB_SLAVE); + return $db->selectField( 'revision', + "$func(rev_timestamp)", + array( 'rev_page' => $id ), + 'PageHistory::getExtremeOffset' ); + } + + /** @todo document */ + function getLatestId() { + if( is_null( $this->mLatestId ) ) { + $id = $this->mTitle->getArticleID(); + $db =& wfGetDB(DB_SLAVE); + $this->mLatestId = $db->selectField( 'revision', + "max(rev_id)", + array( 'rev_page' => $id ), + 'PageHistory::getLatestID' ); + } + return $this->mLatestId; + } + + /** @todo document */ + function getLastOffsetForPaging( $id, $step ) { + $fname = 'PageHistory::getLastOffsetForPaging'; + + $dbr =& wfGetDB(DB_SLAVE); + $res = $dbr->select( + 'revision', + 'rev_timestamp', + "rev_page=$id", + $fname, + array('ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $step)); + + $n = $dbr->numRows( $res ); + $last = null; + while( $obj = $dbr->fetchObject( $res ) ) { + $last = $obj->rev_timestamp; + } + $dbr->freeResult( $res ); + return $last; + } + + /** + * @return returns the direction of browsing watchlist + */ + function getDirection() { + global $wgRequest; + if ($wgRequest->getText("dir") == "prev") + return PageHistory::DIR_PREV; + else + return PageHistory::DIR_NEXT; + } + + /** @todo document */ + function fetchRevisions($limit, $offset, $direction) { + $fname = 'PageHistory::fetchRevisions'; + + $dbr =& wfGetDB( DB_SLAVE ); + + if ($direction == PageHistory::DIR_PREV) + list($dirs, $oper) = array("ASC", ">="); + else /* $direction == PageHistory::DIR_NEXT */ + list($dirs, $oper) = array("DESC", "<="); + + if ($offset) + $offsets = array("rev_timestamp $oper '$offset'"); + else + $offsets = array(); + + $page_id = $this->mTitle->getArticleID(); + + $res = $dbr->select( + 'revision', + array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text', + 'rev_timestamp', 'rev_minor_edit', 'rev_deleted'), + array_merge(array("rev_page=$page_id"), $offsets), + $fname, + array('ORDER BY' => "rev_timestamp $dirs", + 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit) + ); + + $result = array(); + while (($obj = $dbr->fetchObject($res)) != NULL) + $result[] = $obj; + + return $result; + } + + /** @todo document */ + function getNotificationTimestamp() { + global $wgUser, $wgShowUpdatedMarker; + $fname = 'PageHistory::getNotficationTimestamp'; + + if ($this->mNotificationTimestamp !== NULL) + return $this->mNotificationTimestamp; + + if ($wgUser->isAnon() || !$wgShowUpdatedMarker) + return $this->mNotificationTimestamp = false; + + $dbr =& wfGetDB(DB_SLAVE); + + $this->mNotificationTimestamp = $dbr->selectField( + 'watchlist', + 'wl_notificationtimestamp', + array( 'wl_namespace' => $this->mTitle->getNamespace(), + 'wl_title' => $this->mTitle->getDBkey(), + 'wl_user' => $wgUser->getID() + ), + $fname); + + // Don't use the special value reserved for telling whether the field is filled + if ( is_null( $this->mNotificationTimestamp ) ) { + $this->mNotificationTimestamp = false; + } + + return $this->mNotificationTimestamp; + } + + /** @todo document */ + function makeNavbar($revisions, $offset, $limit, $direction) { + global $wgLang; + + $revisions = array_slice($revisions, 0, $limit); + + $latestTimestamp = wfTimestamp(TS_MW, $this->getLatestOffset()); + $earliestTimestamp = wfTimestamp(TS_MW, $this->getEarliestOffset()); + + /* + * When we're displaying previous revisions, we need to reverse + * the array, because it's queried in reverse order. + */ + if ($direction == PageHistory::DIR_PREV) + $revisions = array_reverse($revisions); + + /* + * lowts is the timestamp of the first revision on this page. + * hights is the timestamp of the last revision. + */ + + $lowts = $hights = 0; + + if( count( $revisions ) ) { + $latestShown = wfTimestamp(TS_MW, $revisions[0]->rev_timestamp); + $earliestShown = wfTimestamp(TS_MW, $revisions[count($revisions) - 1]->rev_timestamp); + } else { + $latestShown = null; + $earliestShown = null; + } + + /* Don't announce the limit everywhere if it's the default */ + $usefulLimit = $limit == $this->defaultLimit ? '' : $limit; + + $urls = array(); + foreach (array(20, 50, 100, 250, 500) as $num) { + $urls[] = $this->MakeLink( $wgLang->formatNum($num), + array('offset' => $offset == 0 ? '' : wfTimestamp(TS_MW, $offset), 'limit' => $num, ) ); + } + + $bits = implode($urls, ' | '); + + wfDebug("latestShown=$latestShown latestTimestamp=$latestTimestamp\n"); + if( $latestShown < $latestTimestamp ) { + $prevtext = $this->MakeLink( wfMsgHtml("prevn", $limit), + array( 'dir' => 'prev', 'offset' => $latestShown, 'limit' => $usefulLimit ) ); + $lasttext = $this->MakeLink( wfMsgHtml('histlast'), + array( 'limit' => $usefulLimit ) ); + } else { + $prevtext = wfMsgHtml("prevn", $limit); + $lasttext = wfMsgHtml('histlast'); + } + + wfDebug("earliestShown=$earliestShown earliestTimestamp=$earliestTimestamp\n"); + if( $earliestShown > $earliestTimestamp ) { + $nexttext = $this->MakeLink( wfMsgHtml("nextn", $limit), + array( 'offset' => $earliestShown, 'limit' => $usefulLimit ) ); + $firsttext = $this->MakeLink( wfMsgHtml('histfirst'), + array( 'go' => 'first', 'limit' => $usefulLimit ) ); + } else { + $nexttext = wfMsgHtml("nextn", $limit); + $firsttext = wfMsgHtml('histfirst'); + } + + $firstlast = "($lasttext | $firsttext)"; + + return "$firstlast " . wfMsgHtml("viewprevnext", $prevtext, $nexttext, $bits); + } + + function MakeLink($text, $query = NULL) { + if ( $query === null ) return $text; + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, $text, + wfArrayToCGI( $query, array( 'action' => 'history' ))); + } + + + /** + * Output a subscription feed listing recent edits to this page. + * @param string $type + */ + function feed( $type ) { + require_once 'SpecialRecentchanges.php'; + + global $wgFeedClasses; + if( !isset( $wgFeedClasses[$type] ) ) { + global $wgOut; + $wgOut->addWikiText( wfMsg( 'feed-invalid' ) ); + return; + } + + $feed = new $wgFeedClasses[$type]( + $this->mTitle->getPrefixedText() . ' - ' . + wfMsgForContent( 'history-feed-title' ), + wfMsgForContent( 'history-feed-description' ), + $this->mTitle->getFullUrl( 'action=history' ) ); + + $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT); + $feed->outHeader(); + if( $items ) { + foreach( $items as $row ) { + $feed->outItem( $this->feedItem( $row ) ); + } + } else { + $feed->outItem( $this->feedEmpty() ); + } + $feed->outFooter(); + } + + function feedEmpty() { + global $wgOut; + return new FeedItem( + wfMsgForContent( 'nohistory' ), + $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ), + $this->mTitle->getFullUrl(), + wfTimestamp( TS_MW ), + '', + $this->mTitle->getTalkPage()->getFullUrl() ); + } + + /** + * Generate a FeedItem object from a given revision table row + * Borrows Recent Changes' feed generation functions for formatting; + * includes a diff to the previous revision (if any). + * + * @param $row + * @return FeedItem + */ + function feedItem( $row ) { + $rev = new Revision( $row ); + $rev->setTitle( $this->mTitle ); + $text = rcFormatDiffRow( $this->mTitle, + $this->mTitle->getPreviousRevisionID( $rev->getId() ), + $rev->getId(), + $rev->getTimestamp(), + $rev->getComment() ); + + if( $rev->getComment() == '' ) { + global $wgContLang; + $title = wfMsgForContent( 'history-feed-item-nocomment', + $rev->getUserText(), + $wgContLang->timeanddate( $rev->getTimestamp() ) ); + } else { + $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() ); + } + + return new FeedItem( + $title, + $text, + $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ), + $rev->getTimestamp(), + $rev->getUserText(), + $this->mTitle->getTalkPage()->getFullUrl() ); + } + + /** + * Quickie hack... strip out wikilinks to more legible form from the comment. + */ + function stripComment( $text ) { + return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); + } + + +} + +?> diff --git a/includes/Parser.php b/includes/Parser.php new file mode 100644 index 00000000..31976baf --- /dev/null +++ b/includes/Parser.php @@ -0,0 +1,4727 @@ +-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 + * + *
    + * There are three main entry points into the Parser class:
    + * parse()
    + *   produces HTML output
    + * preSaveTransform().
    + *   produces altered wiki markup.
    + * transformMsg()
    + *   performs brace substitution on MediaWiki messages
    + *
    + * Globals used:
    + *    objects:   $wgLang, $wgContLang
    + *
    + * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
    + *
    + * settings:
    + *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
    + *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
    + *  $wgLocaltimezone, $wgAllowSpecialInclusion*
    + *
    + *  * only within ParserOptions
    + * 
    + * + * @package MediaWiki + */ +class Parser +{ + /**#@+ + * @private + */ + # Persistent: + var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables; + + # Cleared with clearState(): + var $mOutput, $mAutonumber, $mDTopen, $mStripState = array(); + var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; + var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; + 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 + $mRevisionId; // ID to display in {{REVISIONID}} tags + + /**#@-*/ + + /** + * Constructor + * + * @public + */ + function Parser() { + $this->mTagHooks = array(); + $this->mFunctionHooks = array(); + $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); + $this->mFirstCall = true; + } + + /** + * Do various kinds of initialisation on the first call of the parser + */ + function firstCallInit() { + if ( !$this->mFirstCall ) { + return; + } + + wfProfileIn( __METHOD__ ); + global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; + + $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); + + $this->setFunctionHook( MAG_NS, array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_URLENCODE, array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_LCFIRST, array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_UCFIRST, array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_LC, array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_UC, array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_LOCALURL, array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_LOCALURLE, array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_FULLURL, array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_FULLURLE, array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_FORMATNUM, array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_GRAMMAR, array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_PLURAL, array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_NUMBEROFPAGES, array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_NUMBEROFUSERS, array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_NUMBEROFARTICLES, array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_NUMBEROFFILES, array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_NUMBEROFADMINS, array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); + $this->setFunctionHook( MAG_LANGUAGE, array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); + + if ( $wgAllowDisplayTitle ) { + $this->setFunctionHook( MAG_DISPLAYTITLE, array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); + } + if ( $wgAllowSlowParserFunctions ) { + $this->setFunctionHook( MAG_PAGESINNAMESPACE, array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); + } + + $this->initialiseVariables(); + + $this->mFirstCall = false; + wfProfileOut( __METHOD__ ); + } + + /** + * Clear Parser state + * + * @private + */ + function clearState() { + if ( $this->mFirstCall ) { + $this->firstCallInit(); + } + $this->mOutput = new ParserOutput; + $this->mAutonumber = 0; + $this->mLastSection = ''; + $this->mDTopen = false; + $this->mIncludeCount = array(); + $this->mStripState = array(); + $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->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" . Parser::getRandomString(); + + # Clear these on every parse, bug 4549 + $this->mTemplates = array(); + $this->mTemplatePath = array(); + + $this->mShowToc = true; + $this->mForceTocPosition = false; + + wfRunHooks( 'ParserClearState', array( &$this ) ); + } + + /** + * Accessor for mUniqPrefix. + * + * @public + */ + function UniqPrefix() { + return $this->mUniqPrefix; + } + + /** + * Convert wikitext to HTML + * Do not call this function recursively. + * + * @private + * @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 + */ + function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { + /** + * First pass--just handle sections, pass the rest off + * to internalParse() which does all the real work. + */ + + global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; + $fname = 'Parser::parse'; + wfProfileIn( $fname ); + + if ( $clearState ) { + $this->clearState(); + } + + $this->mOptions = $options; + $this->mTitle =& $title; + $this->mRevisionId = $revid; + $this->mOutputType = OT_HTML; + + //$text = $this->strip( $text, $this->mStripState ); + // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5. + $x =& $this->mStripState; + + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); + $text = $this->strip( $text, $x ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); + + # Hook to suspend the parser in this state + if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) { + wfProfileOut( $fname ); + return $text ; + } + + $text = $this->internalParse( $text ); + + $text = $this->unstrip( $text, $this->mStripState ); + + # Clean up special characters, only run once, next-to-last before doBlockLevels + $fixtags = array( + # french spaces, last one Guillemet-left + # only if there is something before the space + '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1 \\2', + # french spaces, Guillemet-right + '/(\\302\\253) /' => '\\1 ', + ); + $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); + + # only once and last + $text = $this->doBlockLevels( $text, $linestart ); + + $this->replaceLinkHolders( $text ); + + # the position of the parserConvert() call should not be changed. it + # assumes that the links are all replaced and the only thing left + # is the mark. + # Side-effects: this calls $this->mOutput->setTitleText() + $text = $wgContLang->parserConvert( $text, $this ); + + $text = $this->unstripNoWiki( $text, $this->mStripState ); + + wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); + + $text = Sanitizer::normalizeCharReferences( $text ); + + if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { + $text = Parser::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''] --> + # Somethingcool> + '/(<([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>/' => + '\\1\\2\\3\\1\\4', + # 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>)([^<]*)(<\/\\2>)/' => + '\\1\\3<div\\5>\\6</div>\\8\\9', + # remove empty italic or bold tag pairs, some + # introduced by rules above + '/<([bi])><\/\\1>/' => '' + ); + + $text = preg_replace( + array_keys( $tidyregs ), + array_values( $tidyregs ), + $text ); + } + + wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); + + $this->mOutput->setText( $text ); + wfProfileOut( $fname ); + + return $this->mOutput; + } + + /** + * 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' ), + * 'tag content' ) ) + * + * @param $elements list of element names. Comments are always extracted. + * @param $text Source text string. + * @param $uniq_prefix + * + * @private + * @static + */ + function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ + $rand = Parser::getRandomString(); + $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-$rand" . sprintf('%08X', $n++) . '-QINU'; + $stripped .= $marker; + + if ( $close === '/>' ) { + // Empty element 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() + * If the $state is already a valid strip state, it adds to the state + * + * @param bool $stripcomments when set, HTML comments + * 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 when saving (fixes bug 2700) + * + * @private + */ + function strip( $text, &$state, $stripcomments = false , $dontstrip = array () ) { + $render = ($this->mOutputType == OT_HTML); + + # Replace any instances of the placeholders + $uniq_prefix = $this->mUniqPrefix; + #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text ); + $commentState = 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 = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + if( $render ) { + $tagName = strtolower( $element ); + 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 = wfEscapeHTMLTagsOnly( $content ); + break; + case 'math': + $output = MathRenderer::renderMath( $content ); + 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" ); + } + } + } else { + // Just stripping tags; keep the source + $output = $tag; + } + if( !$stripcomments && $element == '!--' ) { + $commentState[$marker] = $output; + } else { + $state[$element][$marker] = $output; + } + } + + # 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 = strtr( $text, $commentState ); + } + + return $text; + } + + /** + * Restores pre, math, and other extensions removed by strip() + * + * always call unstripNoWiki() after this one + * @private + */ + function unstrip( $text, &$state ) { + if ( !is_array( $state ) ) { + return $text; + } + + $replacements = array(); + foreach( $state as $tag => $contentDict ) { + if( $tag != 'nowiki' && $tag != 'html' ) { + foreach( $contentDict as $uniq => $content ) { + $replacements[$uniq] = $content; + } + } + } + $text = strtr( $text, $replacements ); + + return $text; + } + + /** + * Always call this after unstrip() to preserve the order + * + * @private + */ + function unstripNoWiki( $text, &$state ) { + if ( !is_array( $state ) ) { + return $text; + } + + $replacements = array(); + foreach( $state as $tag => $contentDict ) { + if( $tag == 'nowiki' || $tag == 'html' ) { + foreach( $contentDict as $uniq => $content ) { + $replacements[$uniq] = $content; + } + } + } + $text = strtr( $text, $replacements ); + + return $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' . Parser::getRandomString(); + if ( !$state ) { + $state = array(); + } + $state['item'][$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 = ''. +'test'.$text.''; + if( $wgTidyInternal ) { + $correctedtext = Parser::internalTidy( $wrappedtext ); + } else { + $correctedtext = Parser::externalTidy( $wrappedtext ); + } + if( is_null( $correctedtext ) ) { + wfDebug( "Tidy error detected!\n" ); + return $text . "\n\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', '/dev/null', '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. Currently written to + * the PHP 4.3.x version of the extension, may not work on PHP 5. + * + * 'pear install tidy' should be able to compile the extension module. + * + * @private + * @static + */ + function internalTidy( $text ) { + global $wgTidyConf; + $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 ) { + // 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(); + } + wfProfileOut( $fname ); + return $cleansource; + } + + /** + * parse the wiki syntax used to render tables + * + * @private + */ + function doTableStuff ( $t ) { + $fname = 'Parser::doTableStuff'; + wfProfileIn( $fname ); + + $t = explode ( "\n" , $t ) ; + $td = array () ; # Is currently a td tag open? + $ltd = array () ; # Was it TD or TH? + $tr = array () ; # Is currently a tr tag open? + $ltr = array () ; # tr attributes + $has_opened_tr = array(); # Did this table open a element? + $indent_level = 0; # indent level of the table + foreach ( $t AS $k => $x ) + { + $x = trim ( $x ) ; + $fc = substr ( $x , 0 , 1 ) ; + if ( preg_match( '/^(:*)\{\|(.*)$/', $x, $matches ) ) { + $indent_level = strlen( $matches[1] ); + + $attributes = $this->unstripForHTML( $matches[2] ); + + $t[$k] = str_repeat( '
    ', $indent_level ) . + '' ; + array_push ( $td , false ) ; + array_push ( $ltd , '' ) ; + array_push ( $tr , false ) ; + array_push ( $ltr , '' ) ; + array_push ( $has_opened_tr, false ); + } + else if ( count ( $td ) == 0 ) { } # Don't do any of the following + else if ( '|}' == substr ( $x , 0 , 2 ) ) { + $z = "" . substr ( $x , 2); + $l = array_pop ( $ltd ) ; + if ( !array_pop ( $has_opened_tr ) ) $z = "" . $z ; + if ( array_pop ( $tr ) ) $z = '' . $z ; + if ( array_pop ( $td ) ) $z = '' . $z ; + array_pop ( $ltr ) ; + $t[$k] = $z . str_repeat( '
    ', $indent_level ); + } + else if ( '|-' == substr ( $x , 0 , 2 ) ) { # Allows for |--------------- + $x = substr ( $x , 1 ) ; + while ( $x != '' && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ; + $z = '' ; + $l = array_pop ( $ltd ) ; + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ) ; + if ( array_pop ( $tr ) ) $z = '' . $z ; + if ( array_pop ( $td ) ) $z = '' . $z ; + array_pop ( $ltr ) ; + $t[$k] = $z ; + array_push ( $tr , false ) ; + array_push ( $td , false ) ; + array_push ( $ltd , '' ) ; + $attributes = $this->unstripForHTML( $x ); + array_push ( $ltr , Sanitizer::fixTagAttributes ( $attributes, 'tr' ) ) ; + } + else if ( '|' == $fc || '!' == $fc || '|+' == substr ( $x , 0 , 2 ) ) { # Caption + # $x is a table row + if ( '|+' == substr ( $x , 0 , 2 ) ) { + $fc = '+' ; + $x = substr ( $x , 1 ) ; + } + $after = substr ( $x , 1 ) ; + if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ; + + // 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 "||". + $after = wfExplodeMarkup( '||', $after ); + + $t[$k] = '' ; + + # Loop through each table cell + foreach ( $after AS $theline ) + { + $z = '' ; + if ( $fc != '+' ) + { + $tra = array_pop ( $ltr ) ; + if ( !array_pop ( $tr ) ) $z = '\n" ; + array_push ( $tr , true ) ; + array_push ( $ltr , '' ) ; + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ) ; + } + + $l = array_pop ( $ltd ) ; + if ( array_pop ( $td ) ) $z = '' . $z ; + if ( $fc == '|' ) $l = 'td' ; + else if ( $fc == '!' ) $l = 'th' ; + else if ( $fc == '+' ) $l = 'caption' ; + else $l = '' ; + array_push ( $ltd , $l ) ; + + # Cell parameters + $y = explode ( '|' , $theline , 2 ) ; + # Note that a '|' inside an invalid link should not + # be mistaken as delimiting cell parameters + if ( strpos( $y[0], '[[' ) !== false ) { + $y = array ($theline); + } + if ( count ( $y ) == 1 ) + $y = "{$z}<{$l}>{$y[0]}" ; + else { + $attributes = $this->unstripForHTML( $y[0] ); + $y = "{$z}<{$l}".Sanitizer::fixTagAttributes($attributes, $l).">{$y[1]}" ; + } + $t[$k] .= $y ; + array_push ( $td , true ) ; + } + } + } + + # Closing open td, tr && table + while ( count ( $td ) > 0 ) + { + $l = array_pop ( $ltd ) ; + if ( array_pop ( $td ) ) $t[] = '' ; + if ( array_pop ( $tr ) ) $t[] = '' ; + if ( !array_pop ( $has_opened_tr ) ) $t[] = "" ; + $t[] = '' ; + } + + $t = implode ( "\n" , $t ) ; + # special case: don't return empty table + if($t == "\n\n
    ") + $t = ''; + wfProfileOut( $fname ); + return $t ; + } + + /** + * 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 ); + + # Remove tags and sections + $text = strtr( $text, array( '' => '' , '' => '' ) ); + $text = strtr( $text, array( '' => '', '' => '') ); + $text = preg_replace( '/.*?<\/includeonly>/s', '', $text ); + + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); + + $text = $this->replaceVariables( $text, $args ); + + // 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
    ', $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 ) { + $text = $this->magicISBN( $text ); + $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' ); + $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' ); + 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", + "\\1\\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() + * @private + */ + 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 .= '
    '; $state = ''; } + else if ($state == 'bi') + { $output .= '
    '; $state = 'b'; } + else if ($state == 'ib') + { $output .= '
    '; $state = 'b'; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = 'b'; } + else # $state can be 'b' or '' + { $output .= ''; $state .= 'i'; } + } + else if (strlen ($r) == 3) + { + if ($state == 'b') + { $output .= ''; $state = ''; } + else if ($state == 'bi') + { $output .= ''; $state = 'i'; } + else if ($state == 'ib') + { $output .= ''; $state = 'i'; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = 'i'; } + else # $state can be 'i' or '' + { $output .= ''; $state .= 'b'; } + } + else if (strlen ($r) == 5) + { + if ($state == 'b') + { $output .= ''; $state = 'i'; } + else if ($state == 'i') + { $output .= ''; $state = 'b'; } + else if ($state == 'bi') + { $output .= ''; $state = ''; } + else if ($state == 'ib') + { $output .= ''; $state = ''; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = ''; } + else # ($state == '') + { $buffer = ''; $state = 'both'; } + } + } + $i++; + } + # Now close all remaining tags. Notice that the order is important. + if ($state == 'b' || $state == 'ib') + $output .= ''; + if ($state == 'i' || $state == 'bi' || $state == 'ib') + $output .= ''; + if ($state == 'bi') + $output .= ''; + if ($state == 'both') + $output .= ''.$buffer.''; + 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( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + + $s = $this->replaceFreeExternalLinks( array_shift( $bits ) ); + + $i = 0; + while ( $i' (which were escaped by + # removeHTMLtags()) should not be included in + # URLs, per RFC 2396. + 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 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); + + # Normalize any HTML entities in input. They will be + # re-escaped by makeExternalLink(). + $url = Sanitizer::decodeCharReferences( $url ); + + # Escape any control characters introduced by the above step + $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + + # Process the trail (i.e. everything after this link up until start of the next link), + # replacing any non-bracketed links + $trail = $this->replaceFreeExternalLinks( $trail ); + + # Use the encoded URL + # This means that users can paste URLs directly into the text + # Funny characters like ö aren't valid in URLs anyway + # This was changed in August 2004 + $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; + + # Register link in the output object. + # Replace unnecessary URL escape codes with the referenced character + # This prevents spammers from hiding links from the filters + $pasteurized = Parser::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++]; + + if ( preg_match( '/^('.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( '/^('.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. + 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 ); + } + + # Normalize any HTML entities in input. They will be + # re-escaped by makeExternalLink() or maybeMakeExternalImage() + $url = Sanitizer::decodeCharReferences( $url ); + + # Escape any control characters introduced by the above step + $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $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 = Parser::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 + * @fixme 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. + */ + function replaceUnusualEscapes( $url ) { + return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', + array( 'Parser', 'replaceUnusualEscapesCallback' ), $url ); + } + + /** + * Callback function used in replaceUnusualEscapes(). + * Replaces unusual URL escape codes with their equivalent character + * @static + * @private + */ + 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( 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( 'nooo' ); + } + $nottalk = !$this->mTitle->isTalkPage(); + + if ( $useLinkPrefixExtension ) { + if ( preg_match( $e2, $s, $m ) ) { + $first_prefix = $m[2]; + } else { + $first_prefix = false; + } + } else { + $prefix = ''; + } + + $selflink = $this->mTitle->getPrefixedText(); + wfProfileOut( $fname.'-setup' ); + + $checkVariantLink = sizeof($wgContLang->getVariants())>1; + $useSubpages = $this->areSubpagesAllowed(); + + # 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; + + 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 !== '' && + preg_match( "/^\](.*)/s", $m[3], $n ) && + strpos($text, '[') !== false + ) + { + $text .= ']'; # so that replaceExternalLinks($text) works later + $m[3] = $n[1]; + } + # fix up urlencoded title texts + if(preg_match('/%/', $m[1] )) + # Should anchors '#' also be rejected? + $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + $trail = $m[3]; + } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption + $might_be_img = true; + $text = $m[2]; + if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]); + $trail = ""; + } else { # Invalid form; output directly + $s .= $prefix . '[[' . $line ; + continue; + } + + # 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); + } + + $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) ); + if( !$nt ) { + $s .= $prefix . '[[' . $line; + continue; + } + + #check other language variants of the link + #if the article does not exist + if( $checkVariantLink + && $nt->getArticleID() == 0 ) { + $wgContLang->findVariantLink($link, $nt); + } + + $ns = $nt->getNamespace(); + $iw = $nt->getInterWiki(); + + if ($might_be_img) { # if this is actually an invalid link + 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 ); + if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) { + # the first ]] closes the inner link, the second the image + $found = true; + $text .= '[[' . $m[1]; + $trail = $m[2]; + break; + } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) { + #if there's exactly one ]] that's fine, we'll keep looking + $text .= '[[' . $m[0]; + } 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 + 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 + continue; + } + } + + $wasblank = ( '' == $text ); + if( $wasblank ) $text = $link; + + + # Link not escaped by : , create the various objects + if( $noforce ) { + + # Interwikis + if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { + $this->mOutput->addLanguageLink( $nt->getFullText() ); + $s = rtrim($s . "\n"); + $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + continue; + } + + if ( $ns == NS_IMAGE ) { + wfProfileIn( "$fname-image" ); + if ( !wfIsBadImage( $nt->getDBkey() ) ) { + # 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 ) { + if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { + $sortkey = $this->mTitle->getText(); + } else { + $sortkey = $this->mTitle->getPrefixedText(); + } + } 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; + } + } + + if( ( $nt->getPrefixedText() === $selflink ) && + ( $nt->getFragment() === '' ) ) { + # Self-links are handled specially; generally de-link and change to bold. + $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 ) { + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + continue; + } elseif( $ns == NS_IMAGE ) { + $img = Image::newFromTitle( $nt ); + if( $img->exists() ) { + // 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 ); + 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 extistence checks and + * article length checks (for stub links) to be bundled into a single query. + * + */ + function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + if ( ! is_object($nt) ) { + # Fail gracefully + $retVal = "{$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 = '{$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 = '{$trail}"; + } + } + 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() ) { + # Look at the first character + if( $target != '' && $target{0} == '/' ) { + # / at end means we don't want the slash to be shown + if( substr( $target, -1, 1 ) == '/' ) { + $target = substr( $target, 1, -1 ); + $noslash = $target; + } else { + $noslash = substr( $target, 1 ); + } + + $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash); + if( '' === $text ) { + $text = $target; + } # 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; + } + } + $nodotdot = trim( $nodotdot ); + if( $nodotdot != '' ) { + $ret .= '/' . $nodotdot; + } + } + } + } + } + + wfProfileOut( $fname ); + return $ret; + } + + /**#@+ + * Used by doBlockLevels() + * @private + */ + /* private */ function closeParagraph() { + $result = ''; + if ( '' != $this->mLastSection ) { + $result = '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 .= '
    • '; } + else if ( '#' == $char ) { $result .= '
      1. '; } + else if ( ':' == $char ) { $result .= '
        '; } + else if ( ';' == $char ) { + $result .= '
        '; + $this->mDTopen = true; + } + else { $result = ''; } + + return $result; + } + + /* private */ function nextItem( $char ) { + if ( '*' == $char || '#' == $char ) { return '
      2. '; } + else if ( ':' == $char || ';' == $char ) { + $close = ''; + if ( $this->mDTopen ) { $close = ''; } + if ( ';' == $char ) { + $this->mDTopen = true; + return $close . '
        '; + } else { + $this->mDTopen = false; + return $close . '
        '; + } + } + return ''; + } + + /* private */ function closeList( $char ) { + if ( '*' == $char ) { $text = '
    '; } + else if ( '#' == $char ) { $text = ''; } + else if ( ':' == $char ) { + if ( $this->mDTopen ) { + $this->mDTopen = false; + $text = ''; + } else { + $text = ''; + } + } + else { return ''; } + 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('/
    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('/(mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|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().'
    ';
    +							$this->mLastSection = 'pre';
    +						}
    +						$t = substr( $t, 1 );
    +					} else {
    +						// paragraph
    +						if ( '' == trim($t) ) {
    +							if ( $paragraphStack ) {
    +								$output .= $paragraphStack.'
    '; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else { + if ($this->mLastSection != 'p' ) { + $output .= $this->closeParagraph(); + $this->mLastSection = ''; + $paragraphStack = '

    '; + } else { + $paragraphStack = '

    '; + } + } + } else { + if ( $paragraphStack ) { + $output .= $paragraphStack; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else if ($this->mLastSection != 'p') { + $output .= $this->closeParagraph().'

    '; + $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 .= '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 = MW_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: // MW_COLON_STATE_TEXT: + switch( $c ) { + case "<": + // Could be either a tag or an tag + $state = MW_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 = MW_COLON_STATE_TAGSTART; + } + break; + case 1: // MW_COLON_STATE_TAG: + // In a + switch( $c ) { + case ">": + $stack++; + $state = MW_COLON_STATE_TEXT; + break; + case "/": + // Slash may be followed by >? + $state = MW_COLON_STATE_TAGSLASH; + break; + default: + // ignore + } + break; + case 2: // MW_COLON_STATE_TAGSTART: + switch( $c ) { + case "/": + $state = MW_COLON_STATE_CLOSETAG; + break; + case "!": + $state = MW_COLON_STATE_COMMENT; + break; + case ">": + // Illegal early close? This shouldn't happen D: + $state = MW_COLON_STATE_TEXT; + break; + default: + $state = MW_COLON_STATE_TAG; + } + break; + case 3: // MW_COLON_STATE_CLOSETAG: + // In a + if( $c == ">" ) { + $stack--; + if( $stack < 0 ) { + wfDebug( "Invalid input in $fname; too many close tags\n" ); + wfProfileOut( $fname ); + return false; + } + $state = MW_COLON_STATE_TEXT; + } + break; + case MW_COLON_STATE_TAGSLASH: + if( $c == ">" ) { + // Yes, a self-closed tag + $state = MW_COLON_STATE_TEXT; + } else { + // Probably we're jumping the gun, and this is an attribute + $state = MW_COLON_STATE_TAG; + } + break; + case 5: // MW_COLON_STATE_COMMENT: + if( $c == "-" ) { + $state = MW_COLON_STATE_COMMENTDASH; + } + break; + case MW_COLON_STATE_COMMENTDASH: + if( $c == "-" ) { + $state = MW_COLON_STATE_COMMENTDASHDASH; + } else { + $state = MW_COLON_STATE_COMMENT; + } + break; + case MW_COLON_STATE_COMMENTDASHDASH: + if( $c == ">" ) { + $state = MW_COLON_STATE_TEXT; + } else { + $state = MW_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 ) ); + + switch ( $index ) { + case MAG_CURRENTMONTH: + return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) ); + case MAG_CURRENTMONTHNAME: + return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) ); + case MAG_CURRENTMONTHNAMEGEN: + return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) ); + case MAG_CURRENTMONTHABBREV: + return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) ); + case MAG_CURRENTDAY: + return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) ); + case MAG_CURRENTDAY2: + return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) ); + case MAG_PAGENAME: + return $this->mTitle->getText(); + case MAG_PAGENAMEE: + return $this->mTitle->getPartialURL(); + case MAG_FULLPAGENAME: + return $this->mTitle->getPrefixedText(); + case MAG_FULLPAGENAMEE: + return $this->mTitle->getPrefixedURL(); + case MAG_SUBPAGENAME: + return $this->mTitle->getSubpageText(); + case MAG_SUBPAGENAMEE: + return $this->mTitle->getSubpageUrlForm(); + case MAG_BASEPAGENAME: + return $this->mTitle->getBaseText(); + case MAG_BASEPAGENAMEE: + return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ); + case MAG_TALKPAGENAME: + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return $talkPage->getPrefixedText(); + } else { + return ''; + } + case MAG_TALKPAGENAMEE: + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return $talkPage->getPrefixedUrl(); + } else { + return ''; + } + case MAG_SUBJECTPAGENAME: + $subjPage = $this->mTitle->getSubjectPage(); + return $subjPage->getPrefixedText(); + case MAG_SUBJECTPAGENAMEE: + $subjPage = $this->mTitle->getSubjectPage(); + return $subjPage->getPrefixedUrl(); + case MAG_REVISIONID: + return $this->mRevisionId; + case MAG_NAMESPACE: + return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case MAG_NAMESPACEE: + return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case MAG_TALKSPACE: + return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : ''; + case MAG_TALKSPACEE: + return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; + case MAG_SUBJECTSPACE: + return $this->mTitle->getSubjectNsText(); + case MAG_SUBJECTSPACEE: + return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); + case MAG_CURRENTDAYNAME: + return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 ); + case MAG_CURRENTYEAR: + return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true ); + case MAG_CURRENTTIME: + return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); + case MAG_CURRENTWEEK: + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)date( 'W', $ts ) ); + case MAG_CURRENTDOW: + return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) ); + case MAG_NUMBEROFARTICLES: + return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() ); + case MAG_NUMBEROFFILES: + return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() ); + case MAG_NUMBEROFUSERS: + return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() ); + case MAG_NUMBEROFPAGES: + return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() ); + case MAG_NUMBEROFADMINS: + return $varCache[$index] = $wgContLang->formatNum( wfNumberOfAdmins() ); + case MAG_CURRENTTIMESTAMP: + return $varCache[$index] = wfTimestampNow(); + case MAG_CURRENTVERSION: + global $wgVersion; + return $wgVersion; + case MAG_SITENAME: + return $wgSitename; + case MAG_SERVER: + return $wgServer; + case MAG_SERVERNAME: + return $wgServerName; + case MAG_SCRIPTPATH: + return $wgScriptPath; + case MAG_DIRECTIONMARK: + return $wgContLang->getDirMark(); + case MAG_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 ); + global $wgVariableIDs; + + $this->mVariables = array(); + foreach ( $wgVariableIDs 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 + * 4 => callback # replacement callback to call if {{{{..}}}} is found + * ) + * ) + * @private + */ + function replace_callback ($text, $callbacks) { + wfProfileIn( __METHOD__ . '-self' ); + $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet + $lastOpeningBrace = -1; # last not closed parentheses + + for ($i = 0; $i < strlen($text); $i++) { + # check for any opening brace + $rule = null; + $nextPos = -1; + foreach ($callbacks as $key => $value) { + $pos = strpos ($text, $key, $i); + if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)) { + $rule = $value; + $nextPos = $pos; + } + } + + if ($lastOpeningBrace >= 0) { + $pos = strpos ($text, $openingBraceStack[$lastOpeningBrace]['braceEnd'], $i); + + if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){ + $rule = null; + $nextPos = $pos; + } + + $pos = strpos ($text, '|', $i); + + if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){ + $rule = null; + $nextPos = $pos; + } + } + + if ($nextPos == -1) + break; + + $i = $nextPos; + + # found openning brace, lets add it to parentheses stack + if (null != $rule) { + $piece = array('brace' => $text[$i], + 'braceEnd' => $rule['end'], + 'count' => 1, + 'title' => '', + 'parts' => null); + + # count openning brace characters + while ($i+1 < strlen($text) && $text[$i+1] == $piece['brace']) { + $piece['count']++; + $i++; + } + + $piece['startAt'] = $i+1; + $piece['partStart'] = $i+1; + + # we need to add to stack only if openning brace count is enough for any given rule + foreach ($rule['cb'] as $cnt => $fn) { + if ($piece['count'] >= $cnt) { + $lastOpeningBrace ++; + $openingBraceStack[$lastOpeningBrace] = $piece; + break; + } + } + + continue; + } + else if ($lastOpeningBrace >= 0) { + # first check if it is a closing brace + if ($openingBraceStack[$lastOpeningBrace]['braceEnd'] == $text[$i]) { + # lets check if it is enough characters for closing brace + $count = 1; + while ($i+$count < strlen($text) && $text[$i+$count] == $text[$i]) + $count++; + + # if there are more closing parentheses than opening ones, we parse less + if ($openingBraceStack[$lastOpeningBrace]['count'] < $count) + $count = $openingBraceStack[$lastOpeningBrace]['count']; + + # 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; + foreach ($callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]['cb'] as $cnt => $fn) { + if ($count >= $cnt && $matchingCount < $cnt) { + $matchingCount = $cnt; + $matchingCallback = $fn; + } + } + + if ($matchingCount == 0) { + $i += $count - 1; + continue; + } + + # lets 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 + wfProfileOut( __METHOD__ . '-self' ); + $replaceWith = call_user_func( $matchingCallback, $cbArgs ); + wfProfileIn( __METHOD__ . '-self' ); + $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); + $i = $pieceStart + strlen($replaceWith) - 1; + } + else { + # null value for callback means that parentheses should be parsed, but not replaced + $i += $matchingCount - 1; + } + + # reset last openning 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? + foreach ($callbacks[$piece['brace']]['cb'] as $cnt => $fn) { + if ($piece['count'] >= $cnt) { + $lastOpeningBrace ++; + $openingBraceStack[$lastOpeningBrace] = $piece; + break; + } + } + } + continue; + } + + # lets set a title if it is a first separator, or next part otherwise + if ($text[$i] == '|') { + 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 + 1; + } + } + } + + wfProfileOut( __METHOD__ . '-self' ); + 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: + * OT_WIKI: only {{subst:}} templates + * OT_MSG: only magic variables + * 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 ) > MAX_INCLUDE_SIZE ) { + return $text; + } + + $fname = 'Parser::replaceVariables'; + 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_HTML || $this->mOutputType == OT_WIKI ) { + $braceCallbacks[3] = array( &$this, 'argSubstitution' ); + } + $callbacks = array(); + $callbacks['{'] = array('end' => '}', 'cb' => $braceCallbacks); + $callbacks['['] = array('end' => ']', 'cb' => array(2=>null)); + $text = $this->replace_callback ($text, $callbacks); + + array_pop( $this->mArgStack ); + + wfProfileOut( $fname ); + return $text; + } + + /** + * Replace magic variables + * @private + */ + function variableSubstitution( $matches ) { + $fname = 'Parser::variableSubstitution'; + $varname = $matches[1]; + wfProfileIn( $fname ); + $skip = false; + if ( $this->mOutputType == OT_WIKI ) { + # Do only magic variables prefixed by SUBST + $mwSubst =& MagicWord::get( MAG_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]; + $text = $this->getVariableValue( $id ); + $this->mOutput->mContainsOldMagic = true; + } else { + $text = $matches[0]; + } + wfProfileOut( $fname ); + return $text; + } + + # Split template arguments + function getTemplateArgs( $argsString ) { + if ( $argsString === '' ) { + return array(); + } + + $args = explode( '|', substr( $argsString, 1 ) ); + + # If any of the arguments contains a '[[' but no ']]', it needs to be + # merged with the next arg because the '|' character between belongs + # to the link syntax and not the template parameter syntax. + $argc = count($args); + + for ( $i = 0; $i < $argc-1; $i++ ) { + if ( substr_count ( $args[$i], '[[' ) != substr_count ( $args[$i], ']]' ) ) { + $args[$i] .= '|'.$args[$i+1]; + array_splice($args, $i+1, 1); + $i--; + $argc--; + } + } + + return $args; + } + + /** + * 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, $action; + $fname = 'Parser::braceSubstitution'; + wfProfileIn( $fname ); + + # 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 + $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 + + $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']; + $argc = count( $args ); + + # SUBST + if ( !$found ) { + $mwSubst =& MagicWord::get( MAG_SUBST ); + if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == 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, INT and RAW + if ( !$found ) { + # Check for MSGNW: + $mwMsgnw =& MagicWord::get( MAG_MSGNW ); + if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { + $nowiki = true; + } else { + # Remove obsolete MSG: + $mwMsg =& MagicWord::get( MAG_MSG ); + $mwMsg->matchStartAndRemove( $part1 ); + } + + # Check for RAW: + $mwRaw =& MagicWord::get( MAG_RAW ); + if ( $mwRaw->matchStartAndRemove( $part1 ) ) { + $forceRawInterwiki = true; + } + + # Check if it is an internal message + $mwInt =& MagicWord::get( MAG_INT ); + if ( $mwInt->matchStartAndRemove( $part1 ) ) { + if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) { + $text = $linestart . wfMsgReal( $part1, $args, true ); + $found = true; + } + } + } + + # 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 . '}}' . + ''; + wfDebug( "$fname: template loop broken at '$part1'\n" ); + } else { + # set $text to cached message. + $text = $linestart . $this->mTemplates[$piece['title']]; + } + } + + # Load from database + $lastPathLevel = $this->mTemplatePath; + 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 ) ) { + $checkVariantLink = sizeof($wgContLang->getVariants())>1; + # Check for language variants if the template is not found + if($checkVariantLink && $title->getArticleID() == 0){ + $wgContLang->findVariantLink($part1, $title); + } + + if ( !$title->isExternal() ) { + # Check for excessive inclusion + $dbk = $title->getPrefixedDBkey(); + if ( $this->incrementIncludeCount( $dbk ) ) { + if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->mOutputType != OT_WIKI ) { + $text = SpecialPage::capturePath( $title ); + if ( is_string( $text ) ) { + $found = true; + $noparse = true; + $noargs = true; + $isHTML = true; + $this->disableCache(); + } + } else { + $articleContent = $this->fetchTemplate( $title ); + if ( $articleContent !== false ) { + $found = true; + $text = $articleContent; + $replaceHeadings = true; + } + } + } + + # If the title is valid but undisplayable, make a link to it + if ( $this->mOutputType == OT_HTML && !$found ) { + $text = '[['.$title->getPrefixedText().']]'; + $found = true; + } + } elseif ( $title->isTrans() ) { + // Interwiki transclusion + if ( $this->mOutputType == 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' ); + } + + # Recursive parsing, escaping and link table handling + # Only for HTML output + if ( $nowiki && $found && $this->mOutputType == OT_HTML ) { + $text = wfEscapeWikiText( $text ); + } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found ) { + if ( $noargs ) { + $assocArgs = array(); + } else { + # Clean up argument array + $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; + } + } + } + + # Add a new element to the templace recursion path + $this->mTemplatePath[$part1] = 1; + } + + if ( !$noparse ) { + # If there are any tags, only include them + if ( in_string( '', $text ) && in_string( '', $text ) ) { + preg_match_all( '/(.*?)\n?<\/onlyinclude>/s', $text, $m ); + $text = ''; + foreach ($m[1] as $piece) + $text .= $piece; + } + # Remove sections and tags + $text = preg_replace( '/.*?<\/noinclude>/s', '', $text ); + $text = strtr( $text, array( '' => '' , '' => '' ) ); + + if( $this->mOutputType == OT_HTML ) { + # Strip ,

    , etc.
    +					$text = $this->strip( $text, $this->mStripState );
    +					$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
    +				}
    +				$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 ) {
    +			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->mOutputType != OT_WIKI && $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 = 0;
    +					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, "" . $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 fetchTemplate( $title ) {
    +		$text = false;
    +		// Loop to fetch the article, with up to 1 redirect
    +		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
    +			$rev = Revision::newFromTitle( $title );
    +			$this->mOutput->addTemplate( $title, $title->getArticleID() );
    +			if ( !$rev ) {
    +				break;
    +			}
    +			$text = $rev->getText();
    +			if ( $text === false ) {
    +				break;
    +			}
    +			// Redirect?
    +			$title = Title::newFromRedirect( $text );
    +		}
    +		return $text;
    +	}
    +
    +	/**
    +	 * Transclude an interwiki link.
    +	 */
    +	function interwikiTransclude( $title, $action ) {
    +		global $wgEnableScaryTranscluding, $wgCanonicalNamespaceNames;
    +
    +		if (!$wgEnableScaryTranscluding)
    +			return wfMsg('scarytranscludedisabled');
    +
    +		// The namespace will actually only be 0 or 10, depending on whether there was a leading :
    +		// But we'll handle it generally anyway
    +		if ( $title->getNamespace() ) {
    +			// Use the canonical namespace, which should work anywhere
    +			$articleName = $wgCanonicalNamespaceNames[$title->getNamespace()] . ':' . $title->getDBkey();
    +		} else {
    +			$articleName = $title->getDBkey();
    +		}
    +
    +		$url = str_replace('$1', urlencode($articleName), Title::getInterwikiLink($title->getInterwiki()));
    +		$url .= "?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 == OT_HTML && null != $matches['parts'] && count($matches['parts']) > 0) {
    +			$text = $matches['parts'][0];
    +		}
    +
    +		return $text;
    +	}
    +
    +	/**
    +	 * Returns true if the function is allowed to include this entity
    +	 * @private
    +	 */
    +	function incrementIncludeCount( $dbk ) {
    +		if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
    +			$this->mIncludeCount[$dbk] = 0;
    +		}
    +		if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
    +			return true;
    +		} else {
    +			return false;
    +		}
    +	}
    +
    +	/**
    +	 * 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( MAG_NOGALLERY );
    +		$this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
    +	}
    +
    +	/**
    +	 * Detect __TOC__ magic word and set a placeholder
    +	 */
    +	function stripToc( $text ) {
    +		# if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
    +		# do not add TOC
    +		$mw = MagicWord::get( MAG_NOTOC );
    +		if( $mw->matchAndRemove( $text ) ) {
    +			$this->mShowToc = false;
    +		}
    +		
    +		$mw = MagicWord::get( MAG_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( '', $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 logged in users who have enabled the option
    +	 * 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->userCanEdit() ) {
    +			$showEditLink = 0;
    +		} else {
    +			$showEditLink = $this->mOptions->getEditSection();
    +		}
    +
    +		# Inhibit editsection links if requested in the page
    +		$esw =& MagicWord::get( MAG_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
    +		$numMatches = preg_match_all( '/)(.*?)<\/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( MAG_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( MAG_FORCETOC );
    +		if ($mw->matchAndRemove( $text ) ) {
    +			$this->mShowToc = true;
    +			$enoughToc = true;
    +		}
    +
    +		# Never ever show TOC if no headers
    +		if( $numMatches < 1 ) {
    +			$enoughToc = false;
    +		}
    +
    +		# We need this to perform operations on the HTML
    +		$sk =& $this->mOptions->getSkin();
    +
    +		# headline counter
    +		$headlineCount = 0;
    +		$sectionCount = 0; # headlineCount excluding template sections
    +
    +		# 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;
    +
    +		foreach( $matches[3] as $headline ) {
    +			$istemplate = 0;
    +			$templatetitle = '';
    +			$templatesection = 0;
    +			$numbering = '';
    +
    +			if (preg_match("//", $headline, $mat)) {
    +				$istemplate = 1;
    +				$templatetitle = base64_decode($mat[1]);
    +				$templatesection = 1 + (int)base64_decode($mat[2]);
    +				$headline = preg_replace("//", "", $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 ) {
    +						$toc .= $sk->tocIndent();
    +					}
    +				}
    +				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 ) {
    +						$toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
    +					}
    +				}
    +				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  by expanding the relevant sections
    +			$canonized_headline = $this->unstrip( $headline, $this->mStripState );
    +			$canonized_headline = $this->unstripNoWiki( $canonized_headline, $this->mStripState );
    +
    +			# Remove link placeholders by the link text.
    +			#     
    +			# turns into
    +			#     link text with suffix
    +			$canonized_headline = preg_replace( '//e',
    +							    "\$this->mLinkHolders['texts'][\$1]",
    +							    $canonized_headline );
    +			$canonized_headline = preg_replace( '//e',
    +							    "\$this->mInterwikiLinkHolders['texts'][\$1]",
    +							    $canonized_headline );
    +
    +			# strip out HTML
    +			$canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline );
    +			$tocline = trim( $canonized_headline );
    +			# Save headline for section edit hint before it's escaped
    +			$headline_hint = trim( $canonized_headline );
    +			$canonized_headline = Sanitizer::escapeId( $tocline );
    +			$refers[$headlineCount] = $canonized_headline;
    +
    +			# count how many in assoc. array so we can track dupes in anchors
    +			@$refers[$canonized_headline]++;
    +			$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);
    +			}
    +			if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
    +				if ( empty( $head[$headlineCount] ) ) {
    +					$head[$headlineCount] = '';
    +				}
    +				if( $istemplate )
    +					$head[$headlineCount] .= $sk->editSectionLinkForOther($templatetitle, $templatesection);
    +				else
    +					$head[$headlineCount] .= $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
    +			}
    +
    +			# give headline the correct  tag
    +			@$head[$headlineCount] .= "';
    +
    +			$headlineCount++;
    +			if( !$istemplate )
    +				$sectionCount++;
    +		}
    +
    +		if( $enoughToc ) {
    +			if( $toclevel<$wgMaxTocLevel ) {
    +				$toc .= $sk->tocUnindent( $toclevel - 1 );
    +			}
    +			$toc = $sk->tocList( $toc );
    +		}
    +
    +		# split up and insert constructed headlines
    +
    +		$blocks = preg_split( '/.*?<\/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( '', $toc, $full );
    +		} else {
    +			return $full;
    +		}
    +	}
    +
    +	/**
    +	 * Return an HTML link for the "ISBN 123456" text
    +	 * @private
    +	 */
    +	function magicISBN( $text ) {
    +		$fname = 'Parser::magicISBN';
    +		wfProfileIn( $fname );
    +
    +		$a = split( 'ISBN ', ' '.$text );
    +		if ( count ( $a ) < 2 ) {
    +			wfProfileOut( $fname );
    +			return $text;
    +		}
    +		$text = substr( array_shift( $a ), 1);
    +		$valid = '0123456789-Xx';
    +
    +		foreach ( $a as $x ) {
    +			# hack: don't replace inside thumbnail title/alt
    +			# attributes
    +			if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
    +				$text .= "ISBN $x";
    +				continue;
    +			}
    +
    +			$isbn = $blank = '' ;
    +			while ( $x !== '' && ' ' == $x{0} ) {
    +				$blank .= ' ';
    +				$x = substr( $x, 1 );
    +			}
    +			if ( $x == '' ) { # blank isbn
    +				$text .= "ISBN $blank";
    +				continue;
    +			}
    +			while ( strstr( $valid, $x{0} ) != false ) {
    +				$isbn .= $x{0};
    +				$x = substr( $x, 1 );
    +			}
    +			$num = str_replace( '-', '', $isbn );
    +			$num = str_replace( ' ', '', $num );
    +			$num = str_replace( 'x', 'X', $num );
    +
    +			if ( '' == $num ) {
    +				$text .= "ISBN $blank$x";
    +			} else {
    +				$titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' );
    +				$text .= 'ISBN $isbn";
    +				$text .= $x;
    +			}
    +		}
    +		wfProfileOut( $fname );
    +		return $text;
    +	}
    +
    +	/**
    +	 * Return an HTML link for the "RFC 1234" text
    +	 *
    +	 * @private
    +	 * @param string $text     Text to be processed
    +	 * @param string $keyword  Magic keyword to use (default RFC)
    +	 * @param string $urlmsg   Interface message to use (default rfcurl)
    +	 * @return string
    +	 */
    +	function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl'  ) {
    +
    +		$valid = '0123456789';
    +		$internal = false;
    +
    +		$a = split( $keyword, ' '.$text );
    +		if ( count ( $a ) < 2 ) {
    +			return $text;
    +		}
    +		$text = substr( array_shift( $a ), 1);
    +
    +		/* Check if keyword is preceed by [[.
    +		 * This test is made here cause of the array_shift above
    +		 * that prevent the test to be done in the foreach.
    +		 */
    +		if ( substr( $text, -2 ) == '[[' ) {
    +			$internal = true;
    +		}
    +
    +		foreach ( $a as $x ) {
    +			/* token might be empty if we have RFC RFC 1234 */
    +			if ( $x=='' ) {
    +				$text.=$keyword;
    +				continue;
    +				}
    +
    +			# hack: don't replace inside thumbnail title/alt
    +			# attributes
    +			if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
    +				$text .= $keyword . $x;
    +				continue;
    +			}
    +			
    +			$id = $blank = '' ;
    +
    +			/** remove and save whitespaces in $blank */
    +			while ( $x{0} == ' ' ) {
    +				$blank .= ' ';
    +				$x = substr( $x, 1 );
    +			}
    +
    +			/** remove and save the rfc number in $id */
    +			while ( strstr( $valid, $x{0} ) != false ) {
    +				$id .= $x{0};
    +				$x = substr( $x, 1 );
    +			}
    +
    +			if ( $id == '' ) {
    +				/* call back stripped spaces*/
    +				$text .= $keyword.$blank.$x;
    +			} elseif( $internal ) {
    +				/* normal link */
    +				$text .= $keyword.$id.$x;
    +			} else {
    +				/* build the external link*/
    +				$url = wfMsg( $urlmsg, $id);
    +				$sk =& $this->mOptions->getSkin();
    +				$la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
    +				$text .= "{$keyword}{$id}{$x}";
    +			}
    +
    +			/* Check if the next RFC keyword is preceed by [[ */
    +			$internal = ( substr($x,-2) == '[[' );
    +		}
    +		return $text;
    +	}
    +
    +	/**
    +	 * 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->mOutputType = OT_WIKI;
    +
    +		if ( $clearState ) {
    +			$this->clearState();
    +		}
    +
    +		$stripState = false;
    +		$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 = $this->unstrip( $text, $stripState );
    +		$text = $this->unstripNoWiki( $text, $stripState );
    +		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  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]";
    +		$np = str_replace( array( '(', ')' ), array( '', '' ), $tc ); # No parens
    +
    +		$namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
    +		$conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
    +
    +		$p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/";		# [[page (context)|]]
    +		$p2 = "/\[\[\\|({$tc}+)]]/";					# [[|page]]
    +		$p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/";		# [[namespace:page|]] and [[:namespace:page|]]
    +		$p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]]
    +		$context = '';
    +		$t = $this->mTitle->getText();
    +		if ( preg_match( $conpat, $t, $m ) ) {
    +			$context = $m[2];
    +		}
    +		$text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text );
    +		$text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text );
    +		$text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text );
    +
    +		if ( '' == $context ) {
    +			$text = preg_replace( $p2, '[[\\1]]', $text );
    +		} else {
    +			$text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
    +		}
    +
    +		# Trim trailing whitespace
    +		# MAG_END (__END__) tag allows for trailing
    +		# whitespace to be deliberately included
    +		$text = rtrim( $text );
    +		$mw =& MagicWord::get( MAG_END );
    +		$mw->matchAndRemove( $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 ) {
    +		$username = $user->getName();
    +		$nickname = $user->getOption( 'nickname' );
    +		$nickname = $nickname === '' ? $username : $nickname;
    +	
    +		if( $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
    +		$userpage = $user->getUserPage();
    +		return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' );
    +	}
    +
    +	/**
    +	 * 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( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG );
    +	
    +		$substWord = MagicWord::get( MAG_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->mOutputType = $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);
    +
    +		$this->mTitle = $wgTitle;
    +		$this->mOptions = $options;
    +		$this->mOutputType = OT_MSG;
    +		$this->clearState();
    +		$text = $this->replaceVariables( $text );
    +
    +		$executing = false;
    +		wfProfileOut($fname);
    +		return $text;
    +	}
    +
    +	/**
    +	 * Create an HTML-style tag, e.g. special text
    +	 * 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 
    +	 * @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 = @$this->mTagHooks[$tag];
    +		$this->mTagHooks[$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 mixed $id The magic word ID, or (deprecated) the function name. Function names are case-insensitive.
    +	 * @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 ) {
    +		if( is_string( $id ) ) {
    +			$id = strtolower( $id );
    +		}
    +		$oldVal = @$this->mFunctionHooks[$id];
    +		$this->mFunctionHooks[$id] = $callback;
    +
    +		# Add to function cache
    +		if ( is_int( $id ) ) {
    +			$mw = MagicWord::get( $id );
    +			$synonyms = $mw->getSynonyms();
    +			$sensitive = intval( $mw->isCaseSensitive() );
    +		} else {
    +			$synonyms = array( $id );
    +			$sensitive = 0;
    +		}
    +
    +		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;
    +	}
    +
    +	/**
    +	 * Replace  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 $wgOutputReplace;
    +
    +		$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;
    +			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;
    +				} 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 );
    +
    +					if ( $threshold >  0 ) {
    +						$size = $s->page_len;
    +						if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
    +							$colours[$pdbk] = 1;
    +						} else {
    +							$colours[$pdbk] = 2;
    +						}
    +					} else {
    +						$colours[$pdbk] = 1;
    +					}
    +				}
    +			}
    +			wfProfileOut( $fname.'-check' );
    +
    +			# Construct search and replace arrays
    +			wfProfileIn( $fname.'-construct' );
    +			$wgOutputReplace = array();
    +			foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
    +				$pdbk = $pdbks[$key];
    +				$searchkey = "";
    +				$title = $this->mLinkHolders['titles'][$key];
    +				if ( empty( $colours[$pdbk] ) ) {
    +					$linkCache->addBadLinkObj( $title );
    +					$colours[$pdbk] = 0;
    +					$this->mOutput->addLink( $title, 0 );
    +					$wgOutputReplace[$searchkey] = $sk->makeBrokenLinkObj( $title,
    +									$this->mLinkHolders['texts'][$key],
    +									$this->mLinkHolders['queries'][$key] );
    +				} elseif ( $colours[$pdbk] == 1 ) {
    +					$wgOutputReplace[$searchkey] = $sk->makeKnownLinkObj( $title,
    +									$this->mLinkHolders['texts'][$key],
    +									$this->mLinkHolders['queries'][$key] );
    +				} elseif ( $colours[$pdbk] == 2 ) {
    +					$wgOutputReplace[$searchkey] = $sk->makeStubLinkObj( $title,
    +									$this->mLinkHolders['texts'][$key],
    +									$this->mLinkHolders['queries'][$key] );
    +				}
    +			}
    +			wfProfileOut( $fname.'-construct' );
    +
    +			# Do the thing
    +			wfProfileIn( $fname.'-replace' );
    +
    +			$text = preg_replace_callback(
    +				'/()/',
    +				"wfOutputReplaceMatches",
    +				$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
    +			$wgOutputReplace = array();
    +			foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
    +				$title = $this->mInterwikiLinkHolders['titles'][$key];
    +				$wgOutputReplace[$key] = $sk->makeLinkObj( $title, $link );
    +			}
    +
    +			$text = preg_replace_callback(
    +				'//',
    +				"wfOutputReplaceMatches",
    +				$text );
    +			wfProfileOut( $fname.'-interwiki' );
    +		}
    +
    +		wfProfileOut( $fname );
    +		return $colours;
    +	}
    +
    +	/**
    +	 * Replace  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(
    +			'//',
    +			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, $parser ) {
    +		// Backwards-compatibility hack
    +		$content = preg_replace( '!(.*?)!is', '\\1', $text );
    +		
    +		$attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
    +		return wfOpenElement( 'pre', $attribs ) .
    +			wfEscapeHTMLTagsOnly( $content ) .
    +			'
    '; + } + + /** + * 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->setShowBytes( false ); + $ig->setShowFilename( false ); + $ig->setParsing(); + $ig->useSkin( $this->mOptions->getSkin() ); + + if( isset( $params['caption'] ) ) + $ig->setCaption( $params['caption'] ); + + $lines = explode( "\n", $text ); + foreach ( $lines as $line ) { + # match lines like these: + # Image:someimage.jpg|This is some image + preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); + # Skip empty lines + if ( count( $matches ) == 0 ) { + continue; + } + $nt =& Title::newFromText( $matches[1] ); + 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( new Image( $nt ), $html ); + + # Only add real images (bug #5586) + if ( $nt->getNamespace() == NS_IMAGE ) { + $this->mOutput->addImage( $nt->getDBkey() ); + } + } + return $ig->toHTML(); + } + + /** + * Parse image options text and use it to make an image + */ + function makeImage( &$nt, $options ) { + global $wgUseImageResize; + + $align = ''; + + # 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. + + $part = explode( '|', $options); + + $mwThumb =& MagicWord::get( MAG_IMG_THUMBNAIL ); + $mwManualThumb =& MagicWord::get( MAG_IMG_MANUALTHUMB ); + $mwLeft =& MagicWord::get( MAG_IMG_LEFT ); + $mwRight =& MagicWord::get( MAG_IMG_RIGHT ); + $mwNone =& MagicWord::get( MAG_IMG_NONE ); + $mwWidth =& MagicWord::get( MAG_IMG_WIDTH ); + $mwCenter =& MagicWord::get( MAG_IMG_CENTER ); + $mwFramed =& MagicWord::get( MAG_IMG_FRAMED ); + $caption = ''; + + $width = $height = $framed = $thumb = false; + $manual_thumb = '' ; + + foreach( $part as $key => $val ) { + if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { + $thumb=true; + } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) { + # use manually specified thumbnail + $thumb=true; + $manual_thumb = $match; + } elseif ( ! is_null( $mwRight->matchVariableStartToEnd($val) ) ) { + # remember to set an alignment, don't render immediately + $align = 'right'; + } elseif ( ! is_null( $mwLeft->matchVariableStartToEnd($val) ) ) { + # remember to set an alignment, don't render immediately + $align = 'left'; + } elseif ( ! is_null( $mwCenter->matchVariableStartToEnd($val) ) ) { + # remember to set an alignment, don't render immediately + $align = 'center'; + } elseif ( ! is_null( $mwNone->matchVariableStartToEnd($val) ) ) { + # remember to set an alignment, don't render immediately + $align = 'none'; + } elseif ( $wgUseImageResize && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { + wfDebug( "MAG_IMG_WIDTH match: $match\n" ); + # $match is the image width in pixels + if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { + $width = intval( $m[1] ); + $height = intval( $m[2] ); + } else { + $width = intval($match); + } + } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { + $framed=true; + } else { + $caption = $val; + } + } + # 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->unstrip($alt, $this->mStripState); + $alt = Sanitizer::stripAllTags( $alt ); + + # Linker does the rest + $sk =& $this->mOptions->getSkin(); + return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb ); + } + + /** + * 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->unstripForHTML( $text ); + return $text; + } + + function unstripForHTML( $text ) { + $text = $this->unstrip( $text, $this->mStripState ); + $text = $this->unstripNoWiki( $text, $this->mStripState ); + 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_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='' ) { + # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML + # comments to be stripped as well) + $striparray = array(); + + $oldOutputType = $this->mOutputType; + $oldOptions = $this->mOptions; + $this->mOptions = new ParserOptions(); + $this->mOutputType = OT_WIKI; + + $striptext = $this->strip( $text, $striparray, true ); + + $this->mOutputType = $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)"; + $secs = preg_split( + /* + "/ + ^( + (?:$comment|<\/?noinclude>)* # Initial comments will be stripped + (?: + (=+) # Should this be limited to 6? + .+? # Section title... + \\2 # Ending = count must match start + | + ^ + + .*? + <\/h\\3\s*> + ) + (?:$comment|<\/?noinclude>|\s+)* # Trailing whitespace ok + )$ + /mix", + */ + "/ + ( + ^ + (?:$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\\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 { + $rv = ""; + } + } 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; + } + } + } + } + # reinsert stripped tags + $rv = $this->unstrip( $rv, $striparray ); + $rv = $this->unstripNoWiki( $rv, $striparray ); + $rv = trim( $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 \Heading\, 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 + * @return string text of the requested section + */ + function getSection( $text, $section ) { + return $this->extractSections( $text, $section, "get" ); + } + + function replaceSection( $oldtext, $section, $text ) { + return $this->extractSections( $oldtext, $section, "replace", $text ); + } + +} + +/** + * @todo document + * @package MediaWiki + */ +class ParserOutput +{ + var $mText, # The output text + $mLanguageLinks, # List of the full text of language links, in the order they appear + $mCategories, # Map of category names to sort keys + $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. + $mVersion, # Compatibility check + $mTitleText, # title text of the chosen language variant + $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. + $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. + $mImages, # DB keys of the images used, in the array key only + $mExternalLinks, # External link URLs, in the key only + $mHTMLtitle, # Display HTML title + $mSubtitle, # Additional subtitle + $mNewSection, # Show a new section link? + $mNoGallery; # No gallery on category page? (__NOGALLERY__) + + function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(), + $containsOldMagic = false, $titletext = '' ) + { + $this->mText = $text; + $this->mLanguageLinks = $languageLinks; + $this->mCategories = $categoryLinks; + $this->mContainsOldMagic = $containsOldMagic; + $this->mCacheTime = ''; + $this->mVersion = MW_PARSER_VERSION; + $this->mTitleText = $titletext; + $this->mLinks = array(); + $this->mTemplates = array(); + $this->mImages = array(); + $this->mExternalLinks = array(); + $this->mHTMLtitle = "" ; + $this->mSubtitle = "" ; + $this->mNewSection = false; + $this->mNoGallery = false; + } + + function getText() { return $this->mText; } + function &getLanguageLinks() { return $this->mLanguageLinks; } + function getCategoryLinks() { return array_keys( $this->mCategories ); } + function &getCategories() { return $this->mCategories; } + function getCacheTime() { return $this->mCacheTime; } + function getTitleText() { return $this->mTitleText; } + function &getLinks() { return $this->mLinks; } + function &getTemplates() { return $this->mTemplates; } + function &getImages() { return $this->mImages; } + function &getExternalLinks() { return $this->mExternalLinks; } + function getNoGallery() { return $this->mNoGallery; } + + function containsOldMagic() { return $this->mContainsOldMagic; } + function setText( $text ) { return wfSetVar( $this->mText, $text ); } + function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); } + 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 addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } + function addImage( $name ) { $this->mImages[$name] = 1; } + function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } + function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } + + function setNewSection( $value ) { + $this->mNewSection = (bool)$value; + } + function getNewSection() { + return (bool)$this->mNewSection; + } + + function addLink( $title, $id ) { + $ns = $title->getNamespace(); + $dbk = $title->getDBkey(); + if ( !isset( $this->mLinks[$ns] ) ) { + $this->mLinks[$ns] = array(); + } + $this->mLinks[$ns][$dbk] = $id; + } + + function addTemplate( $title, $id ) { + $ns = $title->getNamespace(); + $dbk = $title->getDBkey(); + if ( !isset( $this->mTemplates[$ns] ) ) { + $this->mTemplates[$ns] = array(); + } + $this->mTemplates[$ns][$dbk] = $id; + } + + /** + * Return true if this cached output object predates the global or + * per-article cache invalidation timestamps, or if it comes from + * an incompatible older version. + * + * @param string $touched the affected article's last touched timestamp + * @return bool + * @public + */ + function expired( $touched ) { + global $wgCacheEpoch; + return $this->getCacheTime() == -1 || // parser says it's uncacheable + $this->getCacheTime() < $touched || + $this->getCacheTime() <= $wgCacheEpoch || + !isset( $this->mVersion ) || + version_compare( $this->mVersion, MW_PARSER_VERSION, "lt" ); + } +} + +/** + * Set options of the Parser + * @todo document + * @package MediaWiki + */ +class ParserOptions +{ + # All variables are private + var $mUseTeX; # Use texvc to expand tags + var $mUseDynamicDates; # Use DateFormatter to format dates + var $mInterwikiMagic; # Interlanguage links are removed and returned in an array + var $mAllowExternalImages; # Allow external images inline + var $mAllowExternalImagesFrom; # If not, any exception? + var $mSkin; # Reference to the preferred skin + var $mDateFormat; # Date format index + var $mEditSection; # Create "edit section" links + var $mNumberHeadings; # Automatically number headings + var $mAllowSpecialInclusion; # Allow inclusion of special pages + var $mTidy; # Ask for tidy cleanup + var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR + + var $mUser; # Stored user object, just used to initialise the skin + + function getUseTeX() { return $this->mUseTeX; } + function getUseDynamicDates() { return $this->mUseDynamicDates; } + function getInterwikiMagic() { return $this->mInterwikiMagic; } + function getAllowExternalImages() { return $this->mAllowExternalImages; } + function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } + function getDateFormat() { return $this->mDateFormat; } + function getEditSection() { return $this->mEditSection; } + function getNumberHeadings() { return $this->mNumberHeadings; } + function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } + function getTidy() { return $this->mTidy; } + function getInterfaceMessage() { return $this->mInterfaceMessage; } + + function &getSkin() { + if ( !isset( $this->mSkin ) ) { + $this->mSkin = $this->mUser->getSkin(); + } + return $this->mSkin; + } + + function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); } + function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); } + function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } + function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } + function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); } + function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } + function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } + function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } + function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } + function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); } + function setSkin( &$x ) { $this->mSkin =& $x; } + function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); } + + function ParserOptions( $user = null ) { + $this->initialiseFromUser( $user ); + } + + /** + * Get parser options + * @static + */ + function newFromUser( &$user ) { + return new ParserOptions( $user ); + } + + /** Get user options */ + function initialiseFromUser( &$userInput ) { + global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; + global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion; + $fname = 'ParserOptions::initialiseFromUser'; + wfProfileIn( $fname ); + if ( !$userInput ) { + global $wgUser; + if ( isset( $wgUser ) ) { + $user = $wgUser; + } else { + $user = new User; + $user->setLoaded( true ); + } + } else { + $user =& $userInput; + } + + $this->mUser = $user; + + $this->mUseTeX = $wgUseTeX; + $this->mUseDynamicDates = $wgUseDynamicDates; + $this->mInterwikiMagic = $wgInterwikiMagic; + $this->mAllowExternalImages = $wgAllowExternalImages; + $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; + $this->mSkin = null; # Deferred + $this->mDateFormat = $user->getOption( 'date' ); + $this->mEditSection = true; + $this->mNumberHeadings = $user->getOption( 'numberheadings' ); + $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion; + $this->mTidy = false; + $this->mInterfaceMessage = false; + wfProfileOut( $fname ); + } +} + +/** + * Callback function used by Parser::replaceLinkHolders() + * to substitute link placeholders. + */ +function &wfOutputReplaceMatches( $matches ) { + global $wgOutputReplace; + return $wgOutputReplace[$matches[1]]; +} + +/** + * Return the total number of articles + */ +function wfNumberOfArticles() { + global $wgNumberOfArticles; + + wfLoadSiteStats(); + return $wgNumberOfArticles; +} + +/** + * Return the number of files + */ +function wfNumberOfFiles() { + $fname = 'wfNumberOfFiles'; + + wfProfileIn( $fname ); + $dbr =& wfGetDB( DB_SLAVE ); + $numImages = $dbr->selectField('site_stats', 'ss_images', array(), $fname ); + wfProfileOut( $fname ); + + return $numImages; +} + +/** + * Return the number of user accounts + * @return integer + */ +function wfNumberOfUsers() { + wfProfileIn( 'wfNumberOfUsers' ); + $dbr =& wfGetDB( DB_SLAVE ); + $count = $dbr->selectField( 'site_stats', 'ss_users', array(), 'wfNumberOfUsers' ); + wfProfileOut( 'wfNumberOfUsers' ); + return (int)$count; +} + +/** + * Return the total number of pages + * @return integer + */ +function wfNumberOfPages() { + wfProfileIn( 'wfNumberOfPages' ); + $dbr =& wfGetDB( DB_SLAVE ); + $count = $dbr->selectField( 'site_stats', 'ss_total_pages', array(), 'wfNumberOfPages' ); + wfProfileOut( 'wfNumberOfPages' ); + return (int)$count; +} + +/** + * Return the total number of admins + * + * @return integer + */ +function wfNumberOfAdmins() { + static $admins = -1; + wfProfileIn( 'wfNumberOfAdmins' ); + if( $admins == -1 ) { + $dbr =& wfGetDB( DB_SLAVE ); + $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), 'wfNumberOfAdmins' ); + } + wfProfileOut( 'wfNumberOfAdmins' ); + return (int)$admins; +} + +/** + * Count the number of pages in a particular namespace + * + * @param $ns Namespace + * @return integer + */ +function wfPagesInNs( $ns ) { + static $pageCount = array(); + wfProfileIn( 'wfPagesInNs' ); + if( !isset( $pageCount[$ns] ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + $pageCount[$ns] = $dbr->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $ns ), 'wfPagesInNs' ); + } + wfProfileOut( 'wfPagesInNs' ); + return (int)$pageCount[$ns]; +} + +/** + * Get various statistics from the database + * @private + */ +function wfLoadSiteStats() { + global $wgNumberOfArticles, $wgTotalViews, $wgTotalEdits; + $fname = 'wfLoadSiteStats'; + + if ( -1 != $wgNumberOfArticles ) return; + $dbr =& wfGetDB( DB_SLAVE ); + $s = $dbr->selectRow( 'site_stats', + array( 'ss_total_views', 'ss_total_edits', 'ss_good_articles' ), + array( 'ss_row_id' => 1 ), $fname + ); + + if ( $s === false ) { + return; + } else { + $wgTotalViews = $s->ss_total_views; + $wgTotalEdits = $s->ss_total_edits; + $wgNumberOfArticles = $s->ss_good_articles; + } +} + +/** + * Escape html tags + * Basically replacing " > and < with HTML entities ( ", >, <) + * + * @param $in String: text that might contain HTML tags. + * @return string Escaped string + */ +function wfEscapeHTMLTagsOnly( $in ) { + return str_replace( + array( '"', '>', '<' ), + array( '"', '>', '<' ), + $in ); +} + +?> diff --git a/includes/ParserCache.php b/includes/ParserCache.php new file mode 100644 index 00000000..3ec7512f --- /dev/null +++ b/includes/ParserCache.php @@ -0,0 +1,127 @@ +mMemc =& $memCached; + } + + function getKey( &$article, &$user ) { + global $wgDBname, $action; + $hash = $user->getPageRenderingHash(); + if( !$article->mTitle->userCanEdit() ) { + // section edit links are suppressed even if the user has them on + $edit = '!edit=0'; + } else { + $edit = ''; + } + $pageid = intval( $article->getID() ); + $renderkey = (int)($action == 'render'); + $key = "$wgDBname:pcache:idhash:$pageid-$renderkey!$hash$edit"; + return $key; + } + + function getETag( &$article, &$user ) { + return 'W/"' . $this->getKey($article, $user) . "--" . $article->mTouched. '"'; + } + + function get( &$article, &$user ) { + global $wgCacheEpoch; + $fname = 'ParserCache::get'; + wfProfileIn( $fname ); + + $hash = $user->getPageRenderingHash(); + $pageid = intval( $article->getID() ); + $key = $this->getKey( $article, $user ); + + wfDebug( "Trying parser cache $key\n" ); + $value = $this->mMemc->get( $key ); + if ( is_object( $value ) ) { + wfDebug( "Found.\n" ); + # Delete if article has changed since the cache was made + $canCache = $article->checkTouched(); + $cacheTime = $value->getCacheTime(); + $touched = $article->mTouched; + if ( !$canCache || $value->expired( $touched ) ) { + if ( !$canCache ) { + wfIncrStats( "pcache_miss_invalid" ); + wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); + } else { + wfIncrStats( "pcache_miss_expired" ); + wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); + } + $this->mMemc->delete( $key ); + $value = false; + } else { + if ( isset( $value->mTimestamp ) ) { + $article->mTimestamp = $value->mTimestamp; + } + wfIncrStats( "pcache_hit" ); + } + } else { + wfDebug( "Parser cache miss.\n" ); + wfIncrStats( "pcache_miss_absent" ); + $value = false; + } + + wfProfileOut( $fname ); + return $value; + } + + function save( $parserOutput, &$article, &$user ){ + global $wgParserCacheExpireTime; + $key = $this->getKey( $article, $user ); + + if( $parserOutput->getCacheTime() != -1 ) { + + $now = wfTimestampNow(); + $parserOutput->setCacheTime( $now ); + + // Save the timestamp so that we don't have to load the revision row on view + $parserOutput->mTimestamp = $article->getTimestamp(); + + $parserOutput->mText .= "\n\n"; + wfDebug( "Saved in parser cache with key $key and timestamp $now\n" ); + + if( $parserOutput->containsOldMagic() ){ + $expire = 3600; # 1 hour + } else { + $expire = $wgParserCacheExpireTime; + } + $this->mMemc->set( $key, $parserOutput, $expire ); + + } else { + wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" ); + } + + } + +} + +?> diff --git a/includes/ParserXML.php b/includes/ParserXML.php new file mode 100644 index 00000000..e7b64f6e --- /dev/null +++ b/includes/ParserXML.php @@ -0,0 +1,643 @@ +(X)HTML parser + * Based on work by Jan Hidders and Magnus Manske + * To use, set + * $wgUseXMLparser = true ; + * $wgEnableParserCache = false ; + * $wgWiki2xml to the path and executable of the command line version (cli) + * in LocalSettings.php + * @package MediaWiki + * @subpackage Experimental + */ + +/** + * the base class for an element + * @package MediaWiki + * @subpackage Experimental + */ +class element { + var $name = ''; + var $attrs = array (); + var $children = array (); + + /** + * This finds the ATTRS element and returns the ATTR sub-children as a single string + * @todo FIXME $parser always empty when calling makeXHTML() + */ + function getSourceAttrs() { + $ret = ''; + foreach ($this->children as $child) { + if (!is_string($child) AND $child->name == 'ATTRS') { + $ret = $child->makeXHTML($parser); + } + } + return $ret; + } + + /** + * This collects the ATTR thingies for getSourceAttrs() + */ + function getTheseAttrs() { + $ret = array (); + foreach ($this->children as $child) { + if (!is_string($child) AND $child->name == 'ATTR') { + $ret[] = $child->attrs["NAME"]."='".$child->children[0]."'"; + } + } + return implode(' ', $ret); + } + + function fixLinkTails(& $parser, $key) { + $k2 = $key +1; + if (!isset ($this->children[$k2])) + return; + if (!is_string($this->children[$k2])) + return; + if (is_string($this->children[$key])) + return; + if ($this->children[$key]->name != "LINK") + return; + + $n = $this->children[$k2]; + $s = ''; + while ($n != '' AND (($n[0] >= 'a' AND $n[0] <= 'z') OR $n[0] == 'ä' OR $n[0] == 'ö' OR $n[0] == 'ü' OR $n[0] == 'ß')) { + $s .= $n[0]; + $n = substr($n, 1); + } + $this->children[$k2] = $n; + + if (count($this->children[$key]->children) > 1) { + $kl = array_keys($this->children[$key]->children); + $kl = array_pop($kl); + $this->children[$key]->children[$kl]->children[] = $s; + } else { + $e = new element; + $e->name = "LINKOPTION"; + $t = $this->children[$key]->sub_makeXHTML($parser); + $e->children[] = trim($t).$s; + $this->children[$key]->children[] = $e; + } + } + + /** + * This function generates the XHTML for the entire subtree + */ + function sub_makeXHTML(& $parser, $tag = '', $attr = '') { + $ret = ''; + + $attr2 = $this->getSourceAttrs(); + if ($attr != '' AND $attr2 != '') + $attr .= ' '; + $attr .= $attr2; + + if ($tag != '') { + $ret .= '<'.$tag; + if ($attr != '') + $ret .= ' '.$attr; + $ret .= '>'; + } + + # THIS SHOULD BE DONE IN THE WIKI2XML-PARSER INSTEAD + # foreach ( array_keys ( $this->children ) AS $x ) + # $this->fixLinkTails ( $parser , $x ) ; + + foreach ($this->children as $child) { + if (is_string($child)) { + $ret .= $child; + } elseif ($child->name != 'ATTRS') { + $ret .= $child->makeXHTML($parser); + } + } + if ($tag != '') + $ret .= '\n"; + return $ret; + } + + /** + * Link functions + */ + function createInternalLink(& $parser, $target, $display_title, $options) { + global $wgUser; + $skin = $wgUser->getSkin(); + $tp = explode(':', $target); # tp = target parts + $title = ''; # The plain title + $language = ''; # The language/meta/etc. part + $namespace = ''; # The namespace, if any + $subtarget = ''; # The '#' thingy + + $nt = Title :: newFromText($target); + $fl = strtoupper($this->attrs['FORCEDLINK']) == 'YES'; + + if ($fl || count($tp) == 1) { + # Plain and simple case + $title = $target; + } else { + # There's stuff missing here... + if ($nt->getNamespace() == NS_IMAGE) { + $options[] = $display_title; + return $parser->makeImage($nt, implode('|', $options)); + } else { + # Default + $title = $target; + } + } + + if ($language != '') { + # External link within the WikiMedia project + return "{language link}"; + } else { + if ($namespace != '') { + # Link to another namespace, check for image/media stuff + return "{namespace link}"; + } else { + return $skin->makeLink($target, $display_title); + } + } + } + + /** @todo document */ + function makeInternalLink(& $parser) { + $target = ''; + $option = array (); + foreach ($this->children as $child) { + if (is_string($child)) { + # This shouldn't be the case! + } else { + if ($child->name == 'LINKTARGET') { + $target = trim($child->makeXHTML($parser)); + } else { + $option[] = trim($child->makeXHTML($parser)); + } + } + } + + if (count($option) == 0) + $option[] = $target; # Create dummy display title + $display_title = array_pop($option); + return $this->createInternalLink($parser, $target, $display_title, $option); + } + + /** @todo document */ + function getTemplateXHTML($title, $parts, & $parser) { + global $wgLang, $wgUser; + $skin = $wgUser->getSkin(); + $ot = $title; # Original title + if (count(explode(':', $title)) == 1) + $title = $wgLang->getNsText(NS_TEMPLATE).":".$title; + $nt = Title :: newFromText($title); + $id = $nt->getArticleID(); + if ($id == 0) { + # No/non-existing page + return $skin->makeBrokenLink($title, $ot); + } + + $a = 0; + $tv = array (); # Template variables + foreach ($parts AS $part) { + $a ++; + $x = explode('=', $part, 2); + if (count($x) == 1) + $key = "{$a}"; + else + $key = $x[0]; + $value = array_pop($x); + $tv[$key] = $value; + } + $art = new Article($nt); + $text = $art->getContent(false); + $parser->plain_parse($text, true, $tv); + + return $text; + } + + /** + * This function actually converts wikiXML into XHTML tags + * @todo use switch() ! + */ + function makeXHTML(& $parser) { + $ret = ''; + $n = $this->name; # Shortcut + + if ($n == 'EXTENSION') { + # Fix allowed HTML + $old_n = $n; + $ext = strtoupper($this->attrs['NAME']); + + switch($ext) { + case 'B': + case 'STRONG': + $n = 'BOLD'; + break; + case 'I': + case 'EM': + $n = 'ITALICS'; + break; + case 'U': + $n = 'UNDERLINED'; # Hey, virtual wiki tag! ;-) + break; + case 'S': + $n = 'STRIKE'; + break; + case 'P': + $n = 'PARAGRAPH'; + break; + case 'TABLE': + $n = 'TABLE'; + break; + case 'TR': + $n = 'TABLEROW'; + break; + case 'TD': + $n = 'TABLECELL'; + break; + case 'TH': + $n = 'TABLEHEAD'; + break; + case 'CAPTION': + $n = 'CAPTION'; + break; + case 'NOWIKI': + $n = 'NOWIKI'; + break; + } + if ($n != $old_n) { + unset ($this->attrs['NAME']); # Cleanup + } elseif ($parser->nowiki > 0) { + # No 'real' wiki tags allowed in nowiki section + $n = ''; + } + } // $n = 'EXTENSION' + + switch($n) { + case 'ARTICLE': + $ret .= $this->sub_makeXHTML($parser); + break; + case 'HEADING': + $ret .= $this->sub_makeXHTML($parser, 'h'.$this->attrs['LEVEL']); + break; + case 'PARAGRAPH': + $ret .= $this->sub_makeXHTML($parser, 'p'); + break; + case 'BOLD': + $ret .= $this->sub_makeXHTML($parser, 'strong'); + break; + case 'ITALICS': + $ret .= $this->sub_makeXHTML($parser, 'em'); + break; + + # These don't exist as wiki markup + case 'UNDERLINED': + $ret .= $this->sub_makeXHTML($parser, 'u'); + break; + case 'STRIKE': + $ret .= $this->sub_makeXHTML($parser, 'strike'); + break; + + # HTML comment + case 'COMMENT': + # Comments are parsed out + $ret .= ''; + break; + + + # Links + case 'LINK': + $ret .= $this->makeInternalLink($parser); + break; + case 'LINKTARGET': + case 'LINKOPTION': + $ret .= $this->sub_makeXHTML($parser); + break; + + case 'TEMPLATE': + $parts = $this->sub_makeXHTML($parser); + $parts = explode('|', $parts); + $title = array_shift($parts); + $ret .= $this->getTemplateXHTML($title, $parts, & $parser); + break; + + case 'TEMPLATEVAR': + $x = $this->sub_makeXHTML($parser); + if (isset ($parser->mCurrentTemplateOptions["{$x}"])) + $ret .= $parser->mCurrentTemplateOptions["{$x}"]; + break; + + # Internal use, not generated by wiki2xml parser + case 'IGNORE': + $ret .= $this->sub_makeXHTML($parser); + + case 'NOWIKI': + $parser->nowiki++; + $ret .= $this->sub_makeXHTML($parser, ''); + $parser->nowiki--; + + + # Unknown HTML extension + case 'EXTENSION': # This is currently a dummy!!! + $ext = $this->attrs['NAME']; + + $ret .= '<'.$ext.'>'; + $ret .= $this->sub_makeXHTML($parser); + $ret .= '</'.$ext.'> '; + break; + + + # Table stuff + + case 'TABLE': + $ret .= $this->sub_makeXHTML($parser, 'table'); + break; + case 'TABLEROW': + $ret .= $this->sub_makeXHTML($parser, 'tr'); + break; + case 'TABLECELL': + $ret .= $this->sub_makeXHTML($parser, 'td'); + break; + case 'TABLEHEAD': + $ret .= $this->sub_makeXHTML($parser, 'th'); + break; + case 'CAPTION': + $ret .= $this->sub_makeXHTML($parser, 'caption'); + break; + case 'ATTRS': # SPECIAL CASE : returning attributes + return $this->getTheseAttrs(); + + + # Lists stuff + case 'LISTITEM': + if ($parser->mListType == 'dl') + $ret .= $this->sub_makeXHTML($parser, 'dd'); + else + $ret .= $this->sub_makeXHTML($parser, 'li'); + break; + case 'LIST': + $type = 'ol'; # Default + if ($this->attrs['TYPE'] == 'bullet') + $type = 'ul'; + else + if ($this->attrs['TYPE'] == 'indent') + $type = 'dl'; + $oldtype = $parser->mListType; + $parser->mListType = $type; + $ret .= $this->sub_makeXHTML($parser, $type); + $parser->mListType = $oldtype; + break; + + # Something else entirely + default: + $ret .= '<'.$n.'>'; + $ret .= $this->sub_makeXHTML($parser); + $ret .= '</'.$n.'> '; + } // switch($n) + + $ret = "\n{$ret}\n"; + $ret = str_replace("\n\n", "\n", $ret); + return $ret; + } + + /** + * A function for additional debugging output + */ + function myPrint() { + $ret = "
      \n"; + $ret .= "
    • Name: $this->name
    • \n"; + // print attributes + $ret .= '
    • Attributes: '; + foreach ($this->attrs as $name => $value) { + $ret .= "$name => $value; "; + } + $ret .= "
    • \n"; + // print children + foreach ($this->children as $child) { + if (is_string($child)) { + $ret .= "
    • $child
    • \n"; + } else { + $ret .= $child->myPrint(); + } + } + $ret .= "
    \n"; + return $ret; + } +} + +$ancStack = array (); // the stack with ancestral elements + +// START Three global functions needed for parsing, sorry guys +/** @todo document */ +function wgXMLstartElement($parser, $name, $attrs) { + global $ancStack; + + $newElem = new element; + $newElem->name = $name; + $newElem->attrs = $attrs; + + array_push($ancStack, $newElem); +} + +/** @todo document */ +function wgXMLendElement($parser, $name) { + global $ancStack, $rootElem; + // pop element off stack + $elem = array_pop($ancStack); + if (count($ancStack) == 0) + $rootElem = $elem; + else + // add it to its parent + array_push($ancStack[count($ancStack) - 1]->children, $elem); +} + +/** @todo document */ +function wgXMLcharacterData($parser, $data) { + global $ancStack; + $data = trim($data); // Don't add blank lines, they're no use... + // add to parent if parent exists + if ($ancStack && $data != "") { + array_push($ancStack[count($ancStack) - 1]->children, $data); + } +} +// END Three global functions needed for parsing, sorry guys + +/** + * Here's the class that generates a nice tree + * @package MediaWiki + * @subpackage Experimental + */ +class xml2php { + + /** @todo document */ + function & scanFile($filename) { + global $ancStack, $rootElem; + $ancStack = array (); + + $xml_parser = xml_parser_create(); + xml_set_element_handler($xml_parser, 'wgXMLstartElement', 'wgXMLendElement'); + xml_set_character_data_handler($xml_parser, 'wgXMLcharacterData'); + if (!($fp = fopen($filename, 'r'))) { + die('could not open XML input'); + } + while ($data = fread($fp, 4096)) { + if (!xml_parse($xml_parser, $data, feof($fp))) { + die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser))); + } + } + xml_parser_free($xml_parser); + + // return the remaining root element we copied in the beginning + return $rootElem; + } + + /** @todo document */ + function scanString($input) { + global $ancStack, $rootElem; + $ancStack = array (); + + $xml_parser = xml_parser_create(); + xml_set_element_handler($xml_parser, 'wgXMLstartElement', 'wgXMLendElement'); + xml_set_character_data_handler($xml_parser, 'wgXMLcharacterData'); + + if (!xml_parse($xml_parser, $input, true)) { + die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser))); + } + xml_parser_free($xml_parser); + + // return the remaining root element we copied in the beginning + return $rootElem; + } + +} + +/** + * @todo document + * @package MediaWiki + * @subpackage Experimental + */ +class ParserXML extends Parser { + /**#@+ + * @private + */ + # Persistent: + var $mTagHooks, $mListType; + + # Cleared with clearState(): + var $mOutput, $mAutonumber, $mDTopen, $mStripState = array (); + var $mVariables, $mIncludeCount, $mArgStack, $mLastSection, $mInPre; + + # Temporary: + var $mOptions, $mTitle, $mOutputType, $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 $nowikicount, $mCurrentTemplateOptions; + + /**#@-*/ + + /** + * Constructor + * + * @public + */ + function ParserXML() { + $this->mTemplates = array (); + $this->mTemplatePath = array (); + $this->mTagHooks = array (); + $this->clearState(); + } + + /** + * Clear Parser state + * + * @private + */ + function clearState() { + $this->mOutput = new ParserOutput; + $this->mAutonumber = 0; + $this->mLastSection = ""; + $this->mDTopen = false; + $this->mVariables = false; + $this->mIncludeCount = array (); + $this->mStripState = array (); + $this->mArgStack = array (); + $this->mInPre = false; + } + + /** + * Turns the wikitext into XML by calling the external parser + * + */ + function html2xml(& $text) { + global $wgWiki2xml; + + # generating html2xml command path + $a = $wgWiki2xml; + $a = explode('/', $a); + array_pop($a); + $a[] = 'html2xml'; + $html2xml = implode('/', $a); + $a = array (); + + $tmpfname = tempnam( wfTempDir(), 'FOO' ); + $handle = fopen($tmpfname, 'w'); + fwrite($handle, utf8_encode($text)); + fclose($handle); + exec($html2xml.' < '.$tmpfname, $a); + $text = utf8_decode(implode("\n", $a)); + unlink($tmpfname); + } + + /** @todo document */ + function runXMLparser(& $text) { + global $wgWiki2xml; + + $this->html2xml($text); + + $tmpfname = tempnam( wfTempDir(), 'FOO'); + $handle = fopen($tmpfname, 'w'); + fwrite($handle, $text); + fclose($handle); + exec($wgWiki2xml.' < '.$tmpfname, $a); + $text = utf8_decode(implode("\n", $a)); + unlink($tmpfname); + } + + /** @todo document */ + function plain_parse(& $text, $inline = false, $templateOptions = array ()) { + $this->runXMLparser($text); + $nowikicount = 0; + $w = new xml2php; + $result = $w->scanString($text); + + $oldTemplateOptions = $this->mCurrentTemplateOptions; + $this->mCurrentTemplateOptions = $templateOptions; + + if ($inline) { # Inline rendering off for templates + if (count($result->children) == 1) + $result->children[0]->name = 'IGNORE'; + } + + if (1) + $text = $result->makeXHTML($this); # No debugging info + else + $text = $result->makeXHTML($this).'
    '.$text.'
    '.$result->myPrint(); + $this->mCurrentTemplateOptions = $oldTemplateOptions; + } + + /** @todo document */ + function parse($text, & $title, $options, $linestart = true, $clearState = true) { + $this->plain_parse($text); + $this->mOutput->setText($text); + return $this->mOutput; + } + +} +?> diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php new file mode 100644 index 00000000..ed058c65 --- /dev/null +++ b/includes/ProfilerSimple.php @@ -0,0 +1,108 @@ +mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart)); + + $elapsedcpu = $this->getCpuTime() - $this->getCpuTime($wgRUstart); + $elapsedreal = microtime(true) - $wgRequestTime; + + $entry =& $this->mCollated["-setup"]; + if (!is_array($entry)) { + $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); + $this->mCollated[$functionname] =& $entry; + + } + $entry['cpu'] += $elapsedcpu; + $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu; + $entry['real'] += $elapsedreal; + $entry['real_sq'] += $elapsedreal*$elapsedreal; + $entry['count']++; + } + } + + function profileIn($functionname) { + global $wgDebugFunctionEntry; + if ($wgDebugFunctionEntry) { + $this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); + } + $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); + } + + function profileOut($functionname) { + $memory = memory_get_usage(); + + global $wgDebugFunctionEntry; + + if ($wgDebugFunctionEntry) { + $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); + } + + list($ofname,$ocount,$ortime,$octime) = array_pop($this->mWorkStack); + + if (!$ofname) { + $this->debug("Profiling error: $functionname\n"); + } else { + if ($functionname == 'close') { + $message = "Profile section ended by close(): {$ofname}"; + $functionname = $ofname; + $this->debug( "$message\n" ); + } + elseif ($ofname != $functionname) { + $message = "Profiling error: in({$ofname}), out($functionname)"; + $this->debug( "$message\n" ); + } + $entry =& $this->mCollated[$functionname]; + $elapsedcpu = $this->getCpuTime() - $octime; + $elapsedreal = microtime(true) - $ortime; + if (!is_array($entry)) { + $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); + $this->mCollated[$functionname] =& $entry; + + } + $entry['cpu'] += $elapsedcpu; + $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu; + $entry['real'] += $elapsedreal; + $entry['real_sq'] += $elapsedreal*$elapsedreal; + $entry['count']++; + + } + } + + function getFunctionReport() { + /* Implement in output subclasses */ + } + + function getCpuTime($ru=null) { + if ($ru==null) + $ru=getrusage(); + return ($ru['ru_utime.tv_sec']+$ru['ru_stime.tv_sec']+($ru['ru_utime.tv_usec']+$ru['ru_stime.tv_usec'])*1e-6); + } + + /* If argument is passed, it assumes that it is dual-format time string, returns proper float time value */ + function getTime($time=null) { + if ($time==null) + return microtime(true); + list($a,$b)=explode(" ",$time); + return (float)($a+$b); + } + + function debug( $s ) { + if (function_exists( 'wfDebug' ) ) { + wfDebug( $s ); + } + } +} +?> diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php new file mode 100644 index 00000000..c395228b --- /dev/null +++ b/includes/ProfilerSimpleUDP.php @@ -0,0 +1,34 @@ +mCollated as $entry=>$pfdata) { + $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", $wgDBname,"-",$pfdata['count'], + $pfdata['cpu'],$pfdata['cpu_sq'],$pfdata['real'],$pfdata['real_sq'],$entry); + $length=strlen($pfline); + /* printf(""); */ + if ($length+$plength>1400) { + socket_sendto($sock,$packet,$plength,0,$wgUDPProfilerHost,$wgUDPProfilerPort); + $packet=""; + $plength=0; + } + $packet.=$pfline; + $plength+=$length; + } + socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort); + } +} +?> diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php new file mode 100644 index 00000000..3bcdaab2 --- /dev/null +++ b/includes/ProfilerStub.php @@ -0,0 +1,26 @@ + diff --git a/includes/Profiling.php b/includes/Profiling.php new file mode 100644 index 00000000..edecc4f3 --- /dev/null +++ b/includes/Profiling.php @@ -0,0 +1,353 @@ +profileIn($functionname); +} + +/** + * @param $functioname name of the function we have profiled + */ +function wfProfileOut($functionname = 'missing') { + global $wgProfiler; + $wgProfiler->profileOut($functionname); +} + +function wfGetProfilingOutput($start, $elapsed) { + global $wgProfiler; + return $wgProfiler->getOutput($start, $elapsed); +} + +function wfProfileClose() { + global $wgProfiler; + $wgProfiler->close(); +} + +if (!function_exists('memory_get_usage')) { + # Old PHP or --enable-memory-limit not compiled in + function memory_get_usage() { + return 0; + } +} + +/** + * @todo document + * @package MediaWiki + */ +class Profiler { + var $mStack = array (), $mWorkStack = array (), $mCollated = array (); + var $mCalls = array (), $mTotals = array (); + + function Profiler() + { + // Push an entry for the pre-profile setup time onto the stack + global $wgRequestTime; + if ( !empty( $wgRequestTime ) ) { + $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, 0 ); + $this->mStack[] = array( '-setup', 1, $wgRequestTime, 0, microtime(true), 0 ); + } else { + $this->profileIn( '-total' ); + } + + } + + function profileIn($functionname) { + global $wgDebugFunctionEntry; + if ($wgDebugFunctionEntry && function_exists('wfDebug')) { + wfDebug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); + } + $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage()); + } + + function profileOut($functionname) { + $memory = memory_get_usage(); + $time = $this->getTime(); + + global $wgDebugFunctionEntry; + + if ($wgDebugFunctionEntry && function_exists('wfDebug')) { + wfDebug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); + } + + $bit = array_pop($this->mWorkStack); + + if (!$bit) { + wfDebug("Profiling error, !\$bit: $functionname\n"); + } else { + //if ($wgDebugProfiling) { + if ($functionname == 'close') { + $message = "Profile section ended by close(): {$bit[0]}"; + wfDebug( "$message\n" ); + $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); + } + elseif ($bit[0] != $functionname) { + $message = "Profiling error: in({$bit[0]}), out($functionname)"; + wfDebug( "$message\n" ); + $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); + } + //} + $bit[] = $time; + $bit[] = $memory; + $this->mStack[] = $bit; + } + } + + function close() { + while (count($this->mWorkStack)) { + $this->profileOut('close'); + } + } + + function getOutput() { + global $wgDebugFunctionEntry; + $wgDebugFunctionEntry = false; + + if (!count($this->mStack) && !count($this->mCollated)) { + return "No profiling output\n"; + } + $this->close(); + + global $wgProfileCallTree; + if ($wgProfileCallTree) { + return $this->getCallTree(); + } else { + return $this->getFunctionReport(); + } + } + + function getCallTree($start = 0) { + return implode('', array_map(array (& $this, 'getCallTreeLine'), $this->remapCallTree($this->mStack))); + } + + function remapCallTree($stack) { + if (count($stack) < 2) { + return $stack; + } + $outputs = array (); + for ($max = count($stack) - 1; $max > 0;) { + /* Find all items under this entry */ + $level = $stack[$max][1]; + $working = array (); + for ($i = $max -1; $i >= 0; $i --) { + if ($stack[$i][1] > $level) { + $working[] = $stack[$i]; + } else { + break; + } + } + $working = $this->remapCallTree(array_reverse($working)); + $output = array (); + foreach ($working as $item) { + array_push($output, $item); + } + array_unshift($output, $stack[$max]); + $max = $i; + + array_unshift($outputs, $output); + } + $final = array (); + foreach ($outputs as $output) { + foreach ($output as $item) { + $final[] = $item; + } + } + return $final; + } + + function getCallTreeLine($entry) { + list ($fname, $level, $start, $x, $end) = $entry; + $delta = $end - $start; + $space = str_repeat(' ', $level); + + # The ugly double sprintf is to work around a PHP bug, + # which has been fixed in recent releases. + return sprintf( "%10s %s %s\n", + trim( sprintf( "%7.3f", $delta * 1000.0 ) ), + $space, $fname ); + } + + function getTime() { + return microtime(true); + #return $this->getUserTime(); + } + + function getUserTime() { + $ru = getrusage(); + return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6; + } + + function getFunctionReport() { + $width = 140; + $nameWidth = $width - 65; + $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n"; + $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n"; + $prof = "\nProfiling data\n"; + $prof .= sprintf($titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem'); + $this->mCollated = array (); + $this->mCalls = array (); + $this->mMemory = array (); + + # Estimate profiling overhead + $profileCount = count($this->mStack); + wfProfileIn('-overhead-total'); + for ($i = 0; $i < $profileCount; $i ++) { + wfProfileIn('-overhead-internal'); + wfProfileOut('-overhead-internal'); + } + wfProfileOut('-overhead-total'); + + # First, subtract the overhead! + foreach ($this->mStack as $entry) { + $fname = $entry[0]; + $thislevel = $entry[1]; + $start = $entry[2]; + $end = $entry[4]; + $elapsed = $end - $start; + $memory = $entry[5] - $entry[3]; + + if ($fname == '-overhead-total') { + $overheadTotal[] = $elapsed; + $overheadMemory[] = $memory; + } + elseif ($fname == '-overhead-internal') { + $overheadInternal[] = $elapsed; + } + } + $overheadTotal = array_sum($overheadTotal) / count($overheadInternal); + $overheadMemory = array_sum($overheadMemory) / count($overheadInternal); + $overheadInternal = array_sum($overheadInternal) / count($overheadInternal); + + # Collate + foreach ($this->mStack as $index => $entry) { + $fname = $entry[0]; + $thislevel = $entry[1]; + $start = $entry[2]; + $end = $entry[4]; + $elapsed = $end - $start; + + $memory = $entry[5] - $entry[3]; + $subcalls = $this->calltreeCount($this->mStack, $index); + + if (!preg_match('/^-overhead/', $fname)) { + # Adjust for profiling overhead (except special values with elapsed=0 + if ( $elapsed ) { + $elapsed -= $overheadInternal; + $elapsed -= ($subcalls * $overheadTotal); + $memory -= ($subcalls * $overheadMemory); + } + } + + if (!array_key_exists($fname, $this->mCollated)) { + $this->mCollated[$fname] = 0; + $this->mCalls[$fname] = 0; + $this->mMemory[$fname] = 0; + $this->mMin[$fname] = 1 << 24; + $this->mMax[$fname] = 0; + $this->mOverhead[$fname] = 0; + } + + $this->mCollated[$fname] += $elapsed; + $this->mCalls[$fname]++; + $this->mMemory[$fname] += $memory; + $this->mMin[$fname] = min($this->mMin[$fname], $elapsed); + $this->mMax[$fname] = max($this->mMax[$fname], $elapsed); + $this->mOverhead[$fname] += $subcalls; + } + + $total = @ $this->mCollated['-total']; + $this->mCalls['-overhead-total'] = $profileCount; + + # Output + asort($this->mCollated, SORT_NUMERIC); + foreach ($this->mCollated as $fname => $elapsed) { + $calls = $this->mCalls[$fname]; + $percent = $total ? 100. * $elapsed / $total : 0; + $memory = $this->mMemory[$fname]; + $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]); + + global $wgProfileToDatabase; + if ($wgProfileToDatabase) { + Profiler :: logToDB($fname, (float) ($elapsed * 1000), $calls); + } + } + $prof .= "\nTotal: $total\n\n"; + + return $prof; + } + + /** + * Counts the number of profiled function calls sitting under + * the given point in the call graph. Not the most efficient algo. + * + * @param $stack Array: + * @param $start Integer: + * @return Integer + * @private + */ + function calltreeCount(& $stack, $start) { + $level = $stack[$start][1]; + $count = 0; + for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) { + $count ++; + } + return $count; + } + + /** + * @static + */ + function logToDB($name, $timeSum, $eventCount) { + # Warning: $wguname is a live patch, it should be moved to Setup.php + global $wguname, $wgProfilePerHost; + + $fname = 'Profiler::logToDB'; + $dbw = & wfGetDB(DB_MASTER); + if (!is_object($dbw)) + return false; + $errorState = $dbw->ignoreErrors( true ); + $profiling = $dbw->tableName('profiling'); + + $name = substr($name, 0, 255); + $encname = $dbw->strencode($name); + + if ($wgProfilePerHost) { + $pfhost = $wguname['nodename']; + } else { + $pfhost = ''; + } + + $sql = "UPDATE $profiling "."SET pf_count=pf_count+{$eventCount}, "."pf_time=pf_time + {$timeSum} ". + "WHERE pf_name='{$encname}' AND pf_server='{$pfhost}'"; + $dbw->query($sql); + + $rc = $dbw->affectedRows(); + if ($rc == 0) { + $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount, + 'pf_time' => $timeSum, 'pf_server' => $pfhost ), $fname, array ('IGNORE')); + } + // When we upgrade to mysql 4.1, the insert+update + // can be merged into just a insert with this construct added: + // "ON DUPLICATE KEY UPDATE ". + // "pf_count=pf_count + VALUES(pf_count), ". + // "pf_time=pf_time + VALUES(pf_time)"; + $dbw->ignoreErrors( $errorState ); + } + + /** + * Get the function name of the current profiling section + */ + function getCurrentSection() { + $elt = end($this->mWorkStack); + return $elt[0]; + } + +} + +?> diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php new file mode 100644 index 00000000..2a40a376 --- /dev/null +++ b/includes/ProtectionForm.php @@ -0,0 +1,244 @@ + + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @package MediaWiki + * @subpackage SpecialPage + */ + +class ProtectionForm { + var $mRestrictions = array(); + var $mReason = ''; + + function ProtectionForm( &$article ) { + global $wgRequest, $wgUser; + global $wgRestrictionTypes, $wgRestrictionLevels; + $this->mArticle =& $article; + $this->mTitle =& $article->mTitle; + + if( $this->mTitle ) { + foreach( $wgRestrictionTypes 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 ) ); + } + } + + // The form will be available in read-only to show levels. + $this->disabled = !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $wgUser->isBlocked(); + $this->disabledAttrib = $this->disabled + ? array( 'disabled' => 'disabled' ) + : array(); + + if( $wgRequest->wasPosted() ) { + $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); + foreach( $wgRestrictionTypes as $action ) { + $val = $wgRequest->getVal( "mwProtect-level-$action" ); + if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { + $this->mRestrictions[$action] = $val; + } + } + } + } + + function show() { + global $wgOut; + + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + if( is_null( $this->mTitle ) || + !$this->mTitle->exists() || + $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgOut->showFatalError( wfMsg( 'badarticleerror' ) ); + return; + } + + if( $this->save() ) { + $wgOut->redirect( $this->mTitle->getFullUrl() ); + return; + } + + $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) ); + $wgOut->setSubtitle( wfMsg( 'protectsub', $this->mTitle->getPrefixedText() ) ); + + $wgOut->addWikiText( + wfMsg( $this->disabled ? "protect-viewtext" : "protect-text", + $this->mTitle->getPrefixedText() ) ); + + $wgOut->addHTML( $this->buildForm() ); + + $this->showLogExtract( $wgOut ); + } + + function save() { + global $wgRequest, $wgUser, $wgOut; + if( !$wgRequest->wasPosted() ) { + return false; + } + + if( $this->disabled ) { + return false; + } + + $token = $wgRequest->getVal( 'wpEditToken' ); + if( !$wgUser->matchEditToken( $token ) ) { + throw new FatalError( wfMsg( 'sessionfailure' ) ); + } + + $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason ); + if( !$ok ) { + throw new FatalError( "Unknown error at restriction save time." ); + } + return $ok; + } + + function buildForm() { + global $wgUser; + + $out = ''; + if( !$this->disabled ) { + $out .= $this->buildScript(); + // The submission needs to reenable the move permission selector + // if it's in locked mode, or some browsers won't submit the data. + $out .= wfOpenElement( 'form', array( + 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), + 'method' => 'post', + 'onsubmit' => 'protectEnable(true)' ) ); + + $out .= wfElement( 'input', array( + 'type' => 'hidden', + 'name' => 'wpEditToken', + 'value' => $wgUser->editToken() ) ); + } + + $out .= ""; + $out .= ""; + $out .= "\n"; + foreach( $this->mRestrictions as $action => $required ) { + /* Not all languages have V_x <-> N_x relation */ + $out .= "\n"; + } + $out .= "\n"; + $out .= "\n"; + foreach( $this->mRestrictions as $action => $selected ) { + $out .= "\n"; + } + $out .= "\n"; + + // JavaScript will add another row with a value-chaining checkbox + + $out .= "\n"; + $out .= "
    " . wfMsgHtml( 'restriction-' . $action ) . "
    \n"; + $out .= $this->buildSelector( $action, $selected ); + $out .= "
    \n"; + + if( !$this->disabled ) { + $out .= "\n"; + $out .= "\n"; + $out .= "\n"; + $out .= "\n"; + $out .= "\n"; + $out .= "
    " . $this->buildReasonInput() . "
    " . $this->buildSubmit() . "
    \n"; + $out .= "\n"; + $out .= $this->buildCleanupScript(); + } + + return $out; + } + + function buildSelector( $action, $selected ) { + global $wgRestrictionLevels; + $id = 'mwProtect-level-' . $action; + $attribs = array( + 'id' => $id, + 'name' => $id, + 'size' => count( $wgRestrictionLevels ), + 'onchange' => 'protectLevelsUpdate(this)', + ) + $this->disabledAttrib; + + $out = wfOpenElement( 'select', $attribs ); + foreach( $wgRestrictionLevels as $key ) { + $out .= $this->buildOption( $key, $selected ); + } + $out .= "\n"; + return $out; + } + + function buildOption( $key, $selected ) { + $text = ( $key == '' ) + ? wfMsg( 'protect-default' ) + : wfMsg( "protect-level-$key" ); + $selectedAttrib = ($selected == $key) + ? array( 'selected' => 'selected' ) + : array(); + return wfElement( 'option', + array( 'value' => $key ) + $selectedAttrib, + $text ); + } + + function buildReasonInput() { + $id = 'mwProtect-reason'; + return wfElement( 'label', array( + 'id' => "$id-label", + 'for' => $id ), + wfMsg( 'protectcomment' ) ) . + '' . + wfElement( 'input', array( + 'size' => 60, + 'name' => $id, + 'id' => $id ) ); + } + + function buildSubmit() { + return wfElement( 'input', array( + 'type' => 'submit', + 'value' => wfMsg( 'confirm' ) ) ); + } + + function buildScript() { + global $wgStylePath; + return ''; + } + + function buildCleanupScript() { + return ''; + } + + /** + * @param OutputPage $out + * @access private + */ + function showLogExtract( &$out ) { + # Show relevant lines from the deletion log: + $out->addHTML( "

    " . htmlspecialchars( LogPage::logName( 'protect' ) ) . "

    \n" ); + require_once( 'SpecialLog.php' ); + $logViewer = new LogViewer( + new LogReader( + new FauxRequest( + array( 'page' => $this->mTitle->getPrefixedText(), + 'type' => 'protect' ) ) ) ); + $logViewer->showList( $out ); + } +} + + +?> diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php new file mode 100644 index 00000000..bed79c10 --- /dev/null +++ b/includes/ProxyTools.php @@ -0,0 +1,233 @@ + $curIP ) { + if ( array_key_exists( $curIP, $trustedProxies ) ) { + if ( isset( $ipchain[$i + 1] ) && wfIsIPPublic( $ipchain[$i + 1] ) ) { + $ip = $ipchain[$i + 1]; + } + } else { + break; + } + } + } + + wfDebug( "IP: $ip\n" ); + $wgIP = $ip; + return $ip; +} + +/** + * Given an IP address in dotted-quad notation, returns an unsigned integer. + * Like ip2long() except that it actually works and has a consistent error return value. + */ +function wfIP2Unsigned( $ip ) { + $n = ip2long( $ip ); + if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version + $n = false; + } elseif ( $n < 0 ) { + $n += pow( 2, 32 ); + } + return $n; +} + +/** + * Return a zero-padded hexadecimal representation of an IP address + */ +function wfIP2Hex( $ip ) { + $n = wfIP2Unsigned( $ip ); + if ( $n !== false ) { + $n = sprintf( '%08X', $n ); + } + return $n; +} + +/** + * Determine if an IP address really is an IP address, and if it is public, + * i.e. not RFC 1918 or similar + */ +function wfIsIPPublic( $ip ) { + $n = wfIP2Unsigned( $ip ); + if ( !$n ) { + return false; + } + + // ip2long accepts incomplete addresses, as well as some addresses + // followed by garbage characters. Check that it's really valid. + if( $ip != long2ip( $n ) ) { + return false; + } + + static $privateRanges = false; + if ( !$privateRanges ) { + $privateRanges = array( + array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) + array( '172.16.0.0', '172.31.255.255' ), # " + array( '192.168.0.0', '192.168.255.255' ), # " + array( '0.0.0.0', '0.255.255.255' ), # this network + array( '127.0.0.0', '127.255.255.255' ), # loopback + ); + } + + foreach ( $privateRanges as $r ) { + $start = wfIP2Unsigned( $r[0] ); + $end = wfIP2Unsigned( $r[1] ); + if ( $n >= $start && $n <= $end ) { + return false; + } + } + return true; +} + +/** + * Forks processes to scan the originating IP for an open proxy server + * MemCached can be used to skip IPs that have already been scanned + */ +function wfProxyCheck() { + global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath; + global $wgUseMemCached, $wgMemc, $wgDBname, $wgProxyMemcExpiry; + global $wgProxyKey; + + if ( !$wgBlockOpenProxies ) { + return; + } + + $ip = wfGetIP(); + + # Get MemCached key + $skip = false; + if ( $wgUseMemCached ) { + $mcKey = "$wgDBname:proxy:ip:$ip"; + $mcValue = $wgMemc->get( $mcKey ); + if ( $mcValue ) { + $skip = true; + } + } + + # Fork the processes + if ( !$skip ) { + $title = Title::makeTitle( NS_SPECIAL, 'Blockme' ); + $iphash = md5( $ip . $wgProxyKey ); + $url = $title->getFullURL( 'ip='.$iphash ); + + foreach ( $wgProxyPorts as $port ) { + $params = implode( ' ', array( + escapeshellarg( $wgProxyScriptPath ), + escapeshellarg( $ip ), + escapeshellarg( $port ), + escapeshellarg( $url ) + )); + exec( "php $params &>/dev/null &" ); + } + # Set MemCached key + if ( $wgUseMemCached ) { + $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry ); + } + } +} + +/** + * Convert a network specification in CIDR notation to an integer network and a number of bits + */ +function wfParseCIDR( $range ) { + $parts = explode( '/', $range, 2 ); + if ( count( $parts ) != 2 ) { + return array( false, false ); + } + $network = wfIP2Unsigned( $parts[0] ); + if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) { + $bits = $parts[1]; + } else { + $network = false; + $bits = false; + } + return array( $network, $bits ); +} + +/** + * Check if an IP address is in the local proxy list + */ +function wfIsLocallyBlockedProxy( $ip ) { + global $wgProxyList; + $fname = 'wfIsLocallyBlockedProxy'; + + if ( !$wgProxyList ) { + return false; + } + wfProfileIn( $fname ); + + if ( !is_array( $wgProxyList ) ) { + # Load from the specified file + $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); + } + + if ( !is_array( $wgProxyList ) ) { + $ret = false; + } elseif ( array_search( $ip, $wgProxyList ) !== false ) { + $ret = true; + } elseif ( array_key_exists( $ip, $wgProxyList ) ) { + # Old-style flipped proxy list + $ret = true; + } else { + $ret = false; + } + wfProfileOut( $fname ); + return $ret; +} + + + + +?> diff --git a/includes/QueryPage.php b/includes/QueryPage.php new file mode 100644 index 00000000..53e17616 --- /dev/null +++ b/includes/QueryPage.php @@ -0,0 +1,483 @@ +listoutput; + * + * @param bool $bool + */ + function setListoutput( $bool ) { + $this->listoutput = $bool; + } + + /** + * Subclasses return their name here. Make sure the name is also + * specified in SpecialPage.php and in Language.php as a language message + * param. + */ + function getName() { + return ''; + } + + /** + * Return title object representing this page + * + * @return Title + */ + function getTitle() { + return Title::makeTitle( NS_SPECIAL, $this->getName() ); + } + + /** + * Subclasses return an SQL query here. + * + * Note that the query itself should return the following four columns: + * 'type' (your special page's name), 'namespace', 'title', and 'value' + * *in that order*. 'value' is used for sorting. + * + * These may be stored in the querycache table for expensive queries, + * and that cached data will be returned sometimes, so the presence of + * extra fields can't be relied upon. The cached 'value' column will be + * an integer; non-numeric values are useful only for sorting the initial + * query. + * + * Don't include an ORDER or LIMIT clause, this will be added. + */ + function getSQL() { + return "SELECT 'sample' as type, 0 as namespace, 'Sample result' as title, 42 as value"; + } + + /** + * Override to sort by increasing values + */ + function sortDescending() { + return true; + } + + function getOrder() { + return ' ORDER BY value ' . + ($this->sortDescending() ? 'DESC' : ''); + } + + /** + * Is this query expensive (for some definition of expensive)? Then we + * don't let it run in miser mode. $wgDisableQueryPages causes all query + * pages to be declared expensive. Some query pages are always expensive. + */ + function isExpensive( ) { + global $wgDisableQueryPages; + return $wgDisableQueryPages; + } + + /** + * Whether or not the output of the page in question is retrived from + * the database cache. + * + * @return bool + */ + function isCached() { + global $wgMiserMode; + + return $this->isExpensive() && $wgMiserMode; + } + + /** + * Sometime we dont want to build rss / atom feeds. + */ + function isSyndicated() { + return true; + } + + /** + * Formats the results of the query for display. The skin is the current + * skin; you can use it for making links. The result is a single row of + * result data. You should be able to grab SQL results off of it. + * If the function return "false", the line output will be skipped. + */ + function formatResult( $skin, $result ) { + return ''; + } + + /** + * The content returned by this function will be output before any result + */ + function getPageHeader( ) { + return ''; + } + + /** + * If using extra form wheely-dealies, return a set of parameters here + * as an associative array. They will be encoded and added to the paging + * links (prev/next/lengths). + * @return array + */ + function linkParameters() { + return array(); + } + + /** + * Some special pages (for example SpecialListusers) might not return the + * current object formatted, but return the previous one instead. + * Setting this to return true, will call one more time wfFormatResult to + * be sure that the very last result is formatted and shown. + */ + function tryLastResult( ) { + return false; + } + + /** + * Clear the cache and save new results + */ + function recache( $limit, $ignoreErrors = true ) { + $fname = get_class($this) . '::recache'; + $dbw =& wfGetDB( DB_MASTER ); + $dbr =& wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); + if ( !$dbw || !$dbr ) { + return false; + } + + $querycache = $dbr->tableName( 'querycache' ); + + if ( $ignoreErrors ) { + $ignoreW = $dbw->ignoreErrors( true ); + $ignoreR = $dbr->ignoreErrors( true ); + } + + # Clear out any old cached data + $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname ); + # Do query + $sql = $this->getSQL() . $this->getOrder(); + if ($limit !== false) + $sql = $dbr->limitResult($sql, $limit, 0); + $res = $dbr->query($sql, $fname); + $num = false; + if ( $res ) { + $num = $dbr->numRows( $res ); + # Fetch results + $insertSql = "INSERT INTO $querycache (qc_type,qc_namespace,qc_title,qc_value) VALUES "; + $first = true; + while ( $res && $row = $dbr->fetchObject( $res ) ) { + if ( $first ) { + $first = false; + } else { + $insertSql .= ','; + } + if ( isset( $row->value ) ) { + $value = $row->value; + } else { + $value = ''; + } + + $insertSql .= '(' . + $dbw->addQuotes( $row->type ) . ',' . + $dbw->addQuotes( $row->namespace ) . ',' . + $dbw->addQuotes( $row->title ) . ',' . + $dbw->addQuotes( $value ) . ')'; + } + + # Save results into the querycache table on the master + if ( !$first ) { + if ( !$dbw->query( $insertSql, $fname ) ) { + // Set result to false to indicate error + $dbr->freeResult( $res ); + $res = false; + } + } + if ( $res ) { + $dbr->freeResult( $res ); + } + if ( $ignoreErrors ) { + $dbw->ignoreErrors( $ignoreW ); + $dbr->ignoreErrors( $ignoreR ); + } + + # Update the querycache_info record for the page + $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname ); + $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname ); + + } + return $num; + } + + /** + * This is the actual workhorse. It does everything needed to make a + * real, honest-to-gosh query page. + * + * @param $offset database query offset + * @param $limit database query limit + * @param $shownavigation show navigation like "next 200"? + */ + function doQuery( $offset, $limit, $shownavigation=true ) { + global $wgUser, $wgOut, $wgLang, $wgContLang; + + $this->offset = $offset; + $this->limit = $limit; + + $sname = $this->getName(); + $fname = get_class($this) . '::doQuery'; + $sql = $this->getSQL(); + $dbr =& wfGetDB( DB_SLAVE ); + $querycache = $dbr->tableName( 'querycache' ); + + $wgOut->setSyndicated( $this->isSyndicated() ); + + if ( $this->isCached() ) { + $type = $dbr->strencode( $sname ); + $sql = + "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value + FROM $querycache WHERE qc_type='$type'"; + + if( !$this->listoutput ) { + + # Fetch the timestamp of this update + $tRes = $dbr->select( 'querycache_info', array( 'qci_timestamp' ), array( 'qci_type' => $type ), $fname ); + $tRow = $dbr->fetchObject( $tRes ); + + if( $tRow ) { + $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true ); + $cacheNotice = wfMsg( 'perfcachedts', $updated ); + $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp ); + $wgOut->addScript( '' ); + } else { + $cacheNotice = wfMsg( 'perfcached' ); + } + + $wgOut->addWikiText( $cacheNotice ); + } + + } + + $sql .= $this->getOrder(); + $sql = $dbr->limitResult($sql, $limit, $offset); + $res = $dbr->query( $sql ); + $num = $dbr->numRows($res); + + $this->preprocessResults( $dbr, $res ); + + $sk = $wgUser->getSkin( ); + + if($shownavigation) { + $wgOut->addHTML( $this->getPageHeader() ); + $top = wfShowingResults( $offset, $num); + $wgOut->addHTML( "

    {$top}\n" ); + + # often disable 'next' link when we reach the end + $atend = $num < $limit; + + $sl = wfViewPrevNext( $offset, $limit , + $wgContLang->specialPage( $sname ), + wfArrayToCGI( $this->linkParameters() ), $atend ); + $wgOut->addHTML( "
    {$sl}

    \n" ); + } + if ( $num > 0 ) { + $s = array(); + if ( ! $this->listoutput ) + $s[] = "
      "; + + # Only read at most $num rows, because $res may contain the whole 1000 + for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) { + $format = $this->formatResult( $sk, $obj ); + if ( $format ) { + $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && + $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; + $s[] = $this->listoutput ? $format : "{$format}\n"; + } + } + + if($this->tryLastResult()) { + // flush the very last result + $obj = null; + $format = $this->formatResult( $sk, $obj ); + if( $format ) { + $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && + $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; + $s[] = "{$format}\n"; + } + } + + $dbr->freeResult( $res ); + if ( ! $this->listoutput ) + $s[] = '
    '; + $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s ); + $wgOut->addHTML( $str ); + } + if($shownavigation) { + $wgOut->addHTML( "

    {$sl}

    \n" ); + } + return $num; + } + + /** + * Do any necessary preprocessing of the result object. + * You should pass this by reference: &$db , &$res + */ + function preprocessResults( $db, $res ) {} + + /** + * Similar to above, but packaging in a syndicated feed instead of a web page + */ + function doFeed( $class = '', $limit = 50 ) { + global $wgFeedClasses; + + if( isset($wgFeedClasses[$class]) ) { + $feed = new $wgFeedClasses[$class]( + $this->feedTitle(), + $this->feedDesc(), + $this->feedUrl() ); + $feed->outHeader(); + + $dbr =& wfGetDB( DB_SLAVE ); + $sql = $this->getSQL() . $this->getOrder(); + $sql = $dbr->limitResult( $sql, $limit, 0 ); + $res = $dbr->query( $sql, 'QueryPage::doFeed' ); + while( $obj = $dbr->fetchObject( $res ) ) { + $item = $this->feedResult( $obj ); + if( $item ) $feed->outItem( $item ); + } + $dbr->freeResult( $res ); + + $feed->outFooter(); + return true; + } else { + return false; + } + } + + /** + * Override for custom handling. If the titles/links are ok, just do + * feedItemDesc() + */ + function feedResult( $row ) { + if( !isset( $row->title ) ) { + return NULL; + } + $title = Title::MakeTitle( intval( $row->namespace ), $row->title ); + if( $title ) { + $date = isset( $row->timestamp ) ? $row->timestamp : ''; + $comments = ''; + if( $title ) { + $talkpage = $title->getTalkPage(); + $comments = $talkpage->getFullURL(); + } + + return new FeedItem( + $title->getPrefixedText(), + $this->feedItemDesc( $row ), + $title->getFullURL(), + $date, + $this->feedItemAuthor( $row ), + $comments); + } else { + return NULL; + } + } + + function feedItemDesc( $row ) { + return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : ''; + } + + function feedItemAuthor( $row ) { + return isset( $row->user_text ) ? $row->user_text : ''; + } + + function feedTitle() { + global $wgLanguageCode, $wgSitename; + $page = SpecialPage::getPage( $this->getName() ); + $desc = $page->getDescription(); + return "$wgSitename - $desc [$wgLanguageCode]"; + } + + function feedDesc() { + return wfMsg( 'tagline' ); + } + + function feedUrl() { + $title = Title::MakeTitle( NS_SPECIAL, $this->getName() ); + return $title->getFullURL(); + } +} + +/** + * This is a subclass for very simple queries that are just looking for page + * titles that match some criteria. It formats each result item as a link to + * that page. + * + * @package MediaWiki + */ +class PageQueryPage extends QueryPage { + + function formatResult( $skin, $result ) { + global $wgContLang; + $nt = Title::makeTitle( $result->namespace, $result->title ); + return $skin->makeKnownLinkObj( $nt, htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ) ); + } +} + +?> diff --git a/includes/RawPage.php b/includes/RawPage.php new file mode 100644 index 00000000..3cdabfd9 --- /dev/null +++ b/includes/RawPage.php @@ -0,0 +1,203 @@ + + * http://wikidev.net/ + * Based on PageHistory and SpecialExport + * + * License: GPL (http://www.gnu.org/copyleft/gpl.html) + * + * @author Gabriel Wicke + * @package MediaWiki + */ + +/** + * @todo document + * @package MediaWiki + */ +class RawPage { + var $mArticle, $mTitle, $mRequest; + var $mOldId, $mGen, $mCharset; + var $mSmaxage, $mMaxage; + var $mContentType, $mExpandTemplates; + + function RawPage( &$article, $request = false ) { + global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType; + + $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); + $this->mArticle =& $article; + $this->mTitle =& $article->mTitle; + + if ( $request === false ) { + $this->mRequest =& $wgRequest; + } else { + $this->mRequest = $request; + } + + $ctype = $this->mRequest->getVal( 'ctype' ); + $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); + $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); + $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; + + $oldid = $this->mRequest->getInt( 'oldid' ); + switch ( $wgRequest->getText( 'direction' ) ) { + case 'next': + # output next revision, or nothing if there isn't one + if ( $oldid ) { + $oldid = $this->mTitle->getNextRevisionId( $oldid ); + } + $oldid = $oldid ? $oldid : -1; + break; + case 'prev': + # output previous revision, or nothing if there isn't one + if ( ! $oldid ) { + # get the current revision so we can get the penultimate one + $this->mArticle->getTouched(); + $oldid = $this->mArticle->mLatest; + } + $prev = $this->mTitle->getPreviousRevisionId( $oldid ); + $oldid = $prev ? $prev : -1 ; + break; + case 'cur': + $oldid = 0; + break; + } + $this->mOldId = $oldid; + + # special case for 'generated' raw things: user css/js + $gen = $this->mRequest->getVal( 'gen' ); + + if($gen == 'css') { + $this->mGen = $gen; + if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; + if($ctype == '') $ctype = 'text/css'; + } elseif ($gen == 'js') { + $this->mGen = $gen; + if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; + if($ctype == '') $ctype = $wgJsMimeType; + } else { + $this->mGen = false; + } + $this->mCharset = $wgInputEncoding; + $this->mSmaxage = intval( $smaxage ); + $this->mMaxage = $maxage; + if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { + $this->mContentType = 'text/x-wiki'; + } else { + $this->mContentType = $ctype; + } + } + + function view() { + global $wgOut, $wgScript; + + if( isset( $_SERVER['SCRIPT_URL'] ) ) { + # Normally we use PHP_SELF to get the URL to the script + # as it was called, minus the query string. + # + # Some sites use Apache rewrite rules to handle subdomains, + # and have PHP set up in a weird way that causes PHP_SELF + # to contain the rewritten URL instead of the one that the + # outside world sees. + # + # If in this mode, use SCRIPT_URL instead, which mod_rewrite + # provides containing the "before" URL. + $url = $_SERVER['SCRIPT_URL']; + } else { + $url = $_SERVER['PHP_SELF']; + } + + $ua = @$_SERVER['HTTP_USER_AGENT']; + if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) { + # 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 + # have eg '.php' and should remain safe. + # + # We used to redirect to a canonical-form URL as a general + # backwards-compatibility / good-citizen nice thing. However + # a lot of servers are set up in buggy ways, resulting in + # redirect loops which hang the browser until the CSS load + # times out. + # + # Just return a 403 Forbidden and get it over with. + wfHttpError( 403, 'Forbidden', + 'Raw pages must be accessed through the primary script entry point.' ); + return; + } + + header( "Content-type: ".$this->mContentType.'; charset='.$this->mCharset ); + # allow the client to cache this for 24 hours + header( 'Cache-Control: s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + echo $this->getRawText(); + $wgOut->disable(); + } + + function getRawText() { + global $wgUser, $wgOut; + if($this->mGen) { + $sk = $wgUser->getSkin(); + $sk->initPage($wgOut); + if($this->mGen == 'css') { + return $sk->getUserStylesheet(); + } else if($this->mGen == 'js') { + return $sk->getUserJs(); + } + } else { + return $this->getArticleText(); + } + } + + function getArticleText() { + $found = false; + $text = ''; + if( $this->mTitle ) { + // If it's a MediaWiki message we can just hit the message cache + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $key = $this->mTitle->getDBkey(); + $text = wfMsgForContentNoTrans( $key ); + # If the message doesn't exist, return a blank + if( $text == '<' . $key . '>' ) + $text = ''; + $found = true; + } else { + // Get it from the DB + $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); + if ( $rev ) { + $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); + header( "Last-modified: $lastmod" ); + $text = $rev->getText(); + $found = true; + } + } + } + + # Bad title or page does not exist + if( !$found && $this->mContentType == 'text/x-wiki' ) { + # Don't return a 404 response for CSS or JavaScript; + # 404s aren't generally cached and it would create + # extra hits when user CSS/JS are on and the user doesn't + # have the pages. + header( "HTTP/1.0 404 Not Found" ); + } + + return $this->parseArticleText( $text ); + } + + function parseArticleText( $text ) { + if ( $text === '' ) + return ''; + else + if ( $this->mExpandTemplates ) { + global $wgTitle; + + $parser = new Parser(); + $parser->Options( new ParserOptions() ); // We don't want this to be user-specific + $parser->Title( $wgTitle ); + $parser->OutputType( OT_HTML ); + + return $parser->replaceVariables( $text ); + } else + return $text; + } +} +?> diff --git a/includes/RecentChange.php b/includes/RecentChange.php new file mode 100644 index 00000000..f320a47a --- /dev/null +++ b/includes/RecentChange.php @@ -0,0 +1,509 @@ +loadFromRow( $row ); + return $rc; + } + + /* static */ function newFromCurRow( $row, $rc_this_oldid = 0 ) + { + $rc = new RecentChange; + $rc->loadFromCurRow( $row, $rc_this_oldid ); + $rc->notificationtimestamp = false; + $rc->numberofWatchingusers = false; + return $rc; + } + + # Accessors + + function setAttribs( $attribs ) + { + $this->mAttribs = $attribs; + } + + function setExtra( $extra ) + { + $this->mExtra = $extra; + } + + function &getTitle() + { + if ( $this->mTitle === false ) { + $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); + } + return $this->mTitle; + } + + function getMovedToTitle() + { + if ( $this->mMovedToTitle === false ) { + $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'], + $this->mAttribs['rc_moved_to_title'] ); + } + return $this->mMovedToTitle; + } + + # Writes the data in this object to the database + function save() + { + global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix, $wgUseRCPatrol; + $fname = 'RecentChange::save'; + + $dbw =& wfGetDB( DB_MASTER ); + if ( !is_array($this->mExtra) ) { + $this->mExtra = array(); + } + $this->mExtra['lang'] = $wgLocalInterwiki; + + if ( !$wgPutIPinRC ) { + $this->mAttribs['rc_ip'] = ''; + } + + # Fixup database timestamps + $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']); + $this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']); + $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' ); + + # Insert new row + $dbw->insert( 'recentchanges', $this->mAttribs, $fname ); + + # Set the ID + $this->mAttribs['rc_id'] = $dbw->insertId(); + + # Update old rows, if necessary + if ( $this->mAttribs['rc_type'] == RC_EDIT ) { + $lastTime = $this->mExtra['lastTimestamp']; + #$now = $this->mAttribs['rc_timestamp']; + #$curId = $this->mAttribs['rc_cur_id']; + + # Don't bother looking for entries that have probably + # been purged, it just locks up the indexes needlessly. + global $wgRCMaxAge; + $age = time() - wfTimestamp( TS_UNIX, $lastTime ); + if( $age < $wgRCMaxAge ) { + # live hack, will commit once tested - kate + # Update rc_this_oldid for the entries which were current + # + #$oldid = $this->mAttribs['rc_last_oldid']; + #$ns = $this->mAttribs['rc_namespace']; + #$title = $this->mAttribs['rc_title']; + # + #$dbw->update( 'recentchanges', + # array( /* SET */ + # 'rc_this_oldid' => $oldid + # ), array( /* WHERE */ + # 'rc_namespace' => $ns, + # 'rc_title' => $title, + # 'rc_timestamp' => $dbw->timestamp( $lastTime ) + # ), $fname + #); + } + + # Update rc_cur_time + #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ), + # array( 'rc_cur_id' => $curId ), $fname ); + } + + # Notify external application via UDP + if ( $wgRC2UDPAddress ) { + $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + if ( $conn ) { + $line = $wgRC2UDPPrefix . $this->getIRCLine(); + socket_sendto( $conn, $line, strlen($line), 0, $wgRC2UDPAddress, $wgRC2UDPPort ); + socket_close( $conn ); + } + } + + // E-mail notifications + global $wgUseEnotif; + if( $wgUseEnotif ) { + # this would be better as an extension hook + include_once( "UserMailer.php" ); + $enotif = new EmailNotification(); + $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); + $enotif->notifyOnPageChange( $title, + $this->mAttribs['rc_timestamp'], + $this->mAttribs['rc_comment'], + $this->mAttribs['rc_minor'], + $this->mAttribs['rc_last_oldid'] ); + } + + } + + # Marks a certain row as patrolled + function markPatrolled( $rcid ) + { + $fname = 'RecentChange::markPatrolled'; + + $dbw =& wfGetDB( DB_MASTER ); + + $dbw->update( 'recentchanges', + array( /* SET */ + 'rc_patrolled' => 1 + ), array( /* WHERE */ + 'rc_id' => $rcid + ), $fname + ); + } + + # Makes an entry in the database corresponding to an edit + /*static*/ function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, + $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, + $newId = 0) + { + if ( $bot == 'default' ) { + $bot = $user->isBot(); + } + + if ( !$ip ) { + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } + } + + $rc = new RecentChange; + $rc->mAttribs = array( + 'rc_timestamp' => $timestamp, + 'rc_cur_time' => $timestamp, + 'rc_namespace' => $title->getNamespace(), + 'rc_title' => $title->getDBkey(), + 'rc_type' => RC_EDIT, + 'rc_minor' => $minor ? 1 : 0, + 'rc_cur_id' => $title->getArticleID(), + 'rc_user' => $user->getID(), + 'rc_user_text' => $user->getName(), + 'rc_comment' => $comment, + 'rc_this_oldid' => $newId, + 'rc_last_oldid' => $oldId, + 'rc_bot' => $bot ? 1 : 0, + 'rc_moved_to_ns' => 0, + 'rc_moved_to_title' => '', + 'rc_ip' => $ip, + 'rc_patrolled' => 0, + 'rc_new' => 0 # obsolete + ); + + $rc->mExtra = array( + 'prefixedDBkey' => $title->getPrefixedDBkey(), + 'lastTimestamp' => $lastTimestamp, + 'oldSize' => $oldSize, + 'newSize' => $newSize, + ); + $rc->save(); + return( $rc->mAttribs['rc_id'] ); + } + + # Makes an entry in the database corresponding to page creation + # Note: the title object must be loaded with the new id using resetArticleID() + /*static*/ function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default", + $ip='', $size = 0, $newId = 0 ) + { + if ( !$ip ) { + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } + } + if ( $bot == 'default' ) { + $bot = $user->isBot(); + } + + $rc = new RecentChange; + $rc->mAttribs = array( + 'rc_timestamp' => $timestamp, + 'rc_cur_time' => $timestamp, + 'rc_namespace' => $title->getNamespace(), + 'rc_title' => $title->getDBkey(), + 'rc_type' => RC_NEW, + 'rc_minor' => $minor ? 1 : 0, + 'rc_cur_id' => $title->getArticleID(), + 'rc_user' => $user->getID(), + 'rc_user_text' => $user->getName(), + 'rc_comment' => $comment, + 'rc_this_oldid' => $newId, + 'rc_last_oldid' => 0, + 'rc_bot' => $bot ? 1 : 0, + 'rc_moved_to_ns' => 0, + 'rc_moved_to_title' => '', + 'rc_ip' => $ip, + 'rc_patrolled' => 0, + 'rc_new' => 1 # obsolete + ); + + $rc->mExtra = array( + 'prefixedDBkey' => $title->getPrefixedDBkey(), + 'lastTimestamp' => 0, + 'oldSize' => 0, + 'newSize' => $size + ); + $rc->save(); + return( $rc->mAttribs['rc_id'] ); + } + + # Makes an entry in the database corresponding to a rename + /*static*/ function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) + { + if ( !$ip ) { + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } + } + + $rc = new RecentChange; + $rc->mAttribs = array( + 'rc_timestamp' => $timestamp, + 'rc_cur_time' => $timestamp, + 'rc_namespace' => $oldTitle->getNamespace(), + 'rc_title' => $oldTitle->getDBkey(), + 'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE, + 'rc_minor' => 0, + 'rc_cur_id' => $oldTitle->getArticleID(), + 'rc_user' => $user->getID(), + 'rc_user_text' => $user->getName(), + 'rc_comment' => $comment, + 'rc_this_oldid' => 0, + 'rc_last_oldid' => 0, + 'rc_bot' => $user->isBot() ? 1 : 0, + 'rc_moved_to_ns' => $newTitle->getNamespace(), + 'rc_moved_to_title' => $newTitle->getDBkey(), + 'rc_ip' => $ip, + 'rc_new' => 0, # obsolete + 'rc_patrolled' => 1 + ); + + $rc->mExtra = array( + 'prefixedDBkey' => $oldTitle->getPrefixedDBkey(), + 'lastTimestamp' => 0, + 'prefixedMoveTo' => $newTitle->getPrefixedDBkey() + ); + $rc->save(); + } + + /* static */ function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false ); + } + + /* static */ function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); + } + + # A log entry is different to an edit in that previous revisions are + # not kept + /*static*/ function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', + $type, $action, $target, $logComment, $params ) + { + if ( !$ip ) { + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } + } + + $rc = new RecentChange; + $rc->mAttribs = array( + 'rc_timestamp' => $timestamp, + 'rc_cur_time' => $timestamp, + 'rc_namespace' => $title->getNamespace(), + 'rc_title' => $title->getDBkey(), + 'rc_type' => RC_LOG, + 'rc_minor' => 0, + 'rc_cur_id' => $title->getArticleID(), + 'rc_user' => $user->getID(), + 'rc_user_text' => $user->getName(), + 'rc_comment' => $comment, + 'rc_this_oldid' => 0, + 'rc_last_oldid' => 0, + 'rc_bot' => $user->isBot() ? 1 : 0, + 'rc_moved_to_ns' => 0, + 'rc_moved_to_title' => '', + 'rc_ip' => $ip, + 'rc_patrolled' => 1, + 'rc_new' => 0 # obsolete + ); + $rc->mExtra = array( + 'prefixedDBkey' => $title->getPrefixedDBkey(), + 'lastTimestamp' => 0, + 'logType' => $type, + 'logAction' => $action, + 'logComment' => $logComment, + 'logTarget' => $target, + 'logParams' => $params + ); + $rc->save(); + } + + # Initialises the members of this object from a mysql row object + function loadFromRow( $row ) + { + $this->mAttribs = get_object_vars( $row ); + $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]); + $this->mExtra = array(); + } + + # Makes a pseudo-RC entry from a cur row, for watchlists and things + function loadFromCurRow( $row ) + { + $this->mAttribs = array( + 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp), + 'rc_cur_time' => $row->rev_timestamp, + 'rc_user' => $row->rev_user, + 'rc_user_text' => $row->rev_user_text, + 'rc_namespace' => $row->page_namespace, + 'rc_title' => $row->page_title, + 'rc_comment' => $row->rev_comment, + 'rc_minor' => $row->rev_minor_edit ? 1 : 0, + 'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT, + 'rc_cur_id' => $row->page_id, + 'rc_this_oldid' => $row->rev_id, + 'rc_last_oldid' => isset($row->rc_last_oldid) ? $row->rc_last_oldid : 0, + 'rc_bot' => 0, + 'rc_moved_to_ns' => 0, + 'rc_moved_to_title' => '', + 'rc_ip' => '', + 'rc_id' => $row->rc_id, + 'rc_patrolled' => $row->rc_patrolled, + 'rc_new' => $row->page_is_new # obsolete + ); + + $this->mExtra = array(); + } + + + /** + * Gets the end part of the diff URL associated with this object + * Blank if no diff link should be displayed + */ + function diffLinkTrail( $forceCur ) + { + if ( $this->mAttribs['rc_type'] == RC_EDIT ) { + $trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) . + "&oldid=" . (int)($this->mAttribs['rc_last_oldid']); + if ( $forceCur ) { + $trail .= '&diff=0' ; + } else { + $trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']); + } + } else { + $trail = ''; + } + return $trail; + } + + function cleanupForIRC( $text ) { + return str_replace(array("\n", "\r"), array("", ""), $text); + } + + function getIRCLine() { + global $wgUseRCPatrol; + + extract($this->mAttribs); + extract($this->mExtra); + + $titleObj =& $this->getTitle(); + if ( $rc_type == RC_LOG ) { + $title = Namespace::getCanonicalName( $titleObj->getNamespace() ) . $titleObj->getText(); + } else { + $title = $titleObj->getPrefixedText(); + } + $title = $this->cleanupForIRC( $title ); + + $bad = array("\n", "\r"); + $empty = array("", ""); + $title = $titleObj->getPrefixedText(); + $title = str_replace($bad, $empty, $title); + + // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26 + if ( $rc_type == RC_LOG ) { + $url = ''; + } elseif ( $rc_new && $wgUseRCPatrol ) { + $url = $titleObj->getInternalURL("rcid=$rc_id"); + } else if ( $rc_new ) { + $url = $titleObj->getInternalURL(); + } else if ( $wgUseRCPatrol ) { + $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid&rcid=$rc_id"); + } else { + $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid"); + } + + if ( isset( $oldSize ) && isset( $newSize ) ) { + $szdiff = $newSize - $oldSize; + if ($szdiff < -500) { + $szdiff = "\002$szdiff\002"; + } elseif ($szdiff >= 0) { + $szdiff = '+' . $szdiff ; + } + $szdiff = '(' . $szdiff . ')' ; + } else { + $szdiff = ''; + } + + $user = $this->cleanupForIRC( $rc_user_text ); + + if ( $rc_type == RC_LOG ) { + $logTargetText = $logTarget->getPrefixedText(); + $comment = $this->cleanupForIRC( str_replace( $logTargetText, "\00302$logTargetText\00310", $rc_comment ) ); + $flag = $logAction; + } else { + $comment = $this->cleanupForIRC( $rc_comment ); + $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : ""); + } + # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003, + # no colour (\003) switches back to the term default + $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " . + "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n"; + return $fullString; + } + +} +?> diff --git a/includes/Revision.php b/includes/Revision.php new file mode 100644 index 00000000..653bacb8 --- /dev/null +++ b/includes/Revision.php @@ -0,0 +1,799 @@ + intval( $id ) ) ); + } + + /** + * Load either the current, or a specified, revision + * that's attached to a given title. If not attached + * to that title, will return null. + * + * @param Title $title + * @param int $id + * @return Revision + * @access public + * @static + */ + function newFromTitle( &$title, $id = 0 ) { + if( $id ) { + $matchId = intval( $id ); + } else { + $matchId = 'page_latest'; + } + return Revision::newFromConds( + array( "rev_id=$matchId", + 'page_id=rev_page', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbkey() ) ); + } + + /** + * Load either the current, or a specified, revision + * that's attached to a given page. If not attached + * to that page, will return null. + * + * @param Database $db + * @param int $pageid + * @param int $id + * @return Revision + * @access public + */ + function loadFromPageId( &$db, $pageid, $id = 0 ) { + $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid )); + if( $id ) { + $conds['rev_id']=intval($id); + } else { + $conds[]='rev_id=page_latest'; + } + return Revision::loadFromConds( $db, $conds ); + } + + /** + * Load either the current, or a specified, revision + * that's attached to a given page. If not attached + * to that page, will return null. + * + * @param Database $db + * @param Title $title + * @param int $id + * @return Revision + * @access public + */ + function loadFromTitle( &$db, $title, $id = 0 ) { + if( $id ) { + $matchId = intval( $id ); + } else { + $matchId = 'page_latest'; + } + return Revision::loadFromConds( + $db, + array( "rev_id=$matchId", + 'page_id=rev_page', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbkey() ) ); + } + + /** + * Load the revision for the given title with the given timestamp. + * WARNING: Timestamps may in some circumstances not be unique, + * so this isn't the best key to use. + * + * @param Database $db + * @param Title $title + * @param string $timestamp + * @return Revision + * @access public + * @static + */ + function loadFromTimestamp( &$db, &$title, $timestamp ) { + return Revision::loadFromConds( + $db, + array( 'rev_timestamp' => $db->timestamp( $timestamp ), + 'page_id=rev_page', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbkey() ) ); + } + + /** + * Given a set of conditions, fetch a revision. + * + * @param array $conditions + * @return Revision + * @static + * @access private + */ + function newFromConds( $conditions ) { + $db =& wfGetDB( DB_SLAVE ); + $row = Revision::loadFromConds( $db, $conditions ); + if( is_null( $row ) ) { + $dbw =& wfGetDB( DB_MASTER ); + $row = Revision::loadFromConds( $dbw, $conditions ); + } + return $row; + } + + /** + * Given a set of conditions, fetch a revision from + * the given database connection. + * + * @param Database $db + * @param array $conditions + * @return Revision + * @static + * @access private + */ + function loadFromConds( &$db, $conditions ) { + $res = Revision::fetchFromConds( $db, $conditions ); + if( $res ) { + $row = $res->fetchObject(); + $res->free(); + if( $row ) { + $ret = new Revision( $row ); + return $ret; + } + } + $ret = null; + return $ret; + } + + /** + * Return a wrapper for a series of database rows to + * fetch all of a given page's revisions in turn. + * Each row can be fed to the constructor to get objects. + * + * @param Title $title + * @return ResultWrapper + * @static + * @access public + */ + function fetchAllRevisions( &$title ) { + return Revision::fetchFromConds( + wfGetDB( DB_SLAVE ), + array( 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbkey(), + 'page_id=rev_page' ) ); + } + + /** + * Return a wrapper for a series of database rows to + * fetch all of a given page's revisions in turn. + * Each row can be fed to the constructor to get objects. + * + * @param Title $title + * @return ResultWrapper + * @static + * @access public + */ + function fetchRevision( &$title ) { + return Revision::fetchFromConds( + wfGetDB( DB_SLAVE ), + array( 'rev_id=page_latest', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDbkey(), + 'page_id=rev_page' ) ); + } + + /** + * Given a set of conditions, return a ResultWrapper + * which will return matching database rows with the + * fields necessary to build Revision objects. + * + * @param Database $db + * @param array $conditions + * @return ResultWrapper + * @static + * @access private + */ + function fetchFromConds( &$db, $conditions ) { + $res = $db->select( + array( 'page', 'revision' ), + array( 'page_namespace', + 'page_title', + 'page_latest', + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_comment', + 'rev_user_text', + 'rev_user', + 'rev_minor_edit', + 'rev_timestamp', + 'rev_deleted' ), + $conditions, + 'Revision::fetchRow', + array( 'LIMIT' => 1 ) ); + $ret = $db->resultObject( $res ); + return $ret; + } + + /** + * @param object $row + * @access private + */ + function Revision( $row ) { + if( is_object( $row ) ) { + $this->mId = intval( $row->rev_id ); + $this->mPage = intval( $row->rev_page ); + $this->mTextId = intval( $row->rev_text_id ); + $this->mComment = $row->rev_comment; + $this->mUserText = $row->rev_user_text; + $this->mUser = intval( $row->rev_user ); + $this->mMinorEdit = intval( $row->rev_minor_edit ); + $this->mTimestamp = $row->rev_timestamp; + $this->mDeleted = intval( $row->rev_deleted ); + + if( isset( $row->page_latest ) ) { + $this->mCurrent = ( $row->rev_id == $row->page_latest ); + $this->mTitle = Title::makeTitle( $row->page_namespace, + $row->page_title ); + } else { + $this->mCurrent = false; + $this->mTitle = null; + } + + if( isset( $row->old_text ) ) { + $this->mText = $this->getRevisionText( $row ); + } else { + $this->mText = null; + } + } elseif( is_array( $row ) ) { + // Build a new revision to be saved... + global $wgUser; + + $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; + $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; + $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; + $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName(); + $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); + $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; + $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW ); + $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; + + // Enforce spacing trimming on supplied text + $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; + $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; + + $this->mTitle = null; # Load on demand if needed + $this->mCurrent = false; + } else { + throw new MWException( 'Revision constructor passed invalid row format.' ); + } + } + + /**#@+ + * @access public + */ + + /** + * @return int + */ + function getId() { + return $this->mId; + } + + /** + * @return int + */ + function getTextId() { + return $this->mTextId; + } + + /** + * Returns the title of the page associated with this entry. + * @return Title + */ + function getTitle() { + if( isset( $this->mTitle ) ) { + return $this->mTitle; + } + $dbr =& wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( + array( 'page', 'revision' ), + array( 'page_namespace', 'page_title' ), + array( 'page_id=rev_page', + 'rev_id' => $this->mId ), + 'Revision::getTitle' ); + if( $row ) { + $this->mTitle = Title::makeTitle( $row->page_namespace, + $row->page_title ); + } + return $this->mTitle; + } + + /** + * Set the title of the revision + * @param Title $title + */ + function setTitle( $title ) { + $this->mTitle = $title; + } + + /** + * @return int + */ + function getPage() { + return $this->mPage; + } + + /** + * Fetch revision's user id if it's available to all users + * @return int + */ + function getUser() { + if( $this->isDeleted( self::DELETED_USER ) ) { + return 0; + } else { + return $this->mUser; + } + } + + /** + * Fetch revision's user id without regard for the current user's permissions + * @return string + */ + function getRawUser() { + return $this->mUser; + } + + /** + * Fetch revision's username if it's available to all users + * @return string + */ + function getUserText() { + if( $this->isDeleted( self::DELETED_USER ) ) { + return ""; + } else { + return $this->mUserText; + } + } + + /** + * Fetch revision's username without regard for view restrictions + * @return string + */ + function getRawUserText() { + return $this->mUserText; + } + + /** + * Fetch revision comment if it's available to all users + * @return string + */ + function getComment() { + if( $this->isDeleted( self::DELETED_COMMENT ) ) { + return ""; + } else { + return $this->mComment; + } + } + + /** + * Fetch revision comment without regard for the current user's permissions + * @return string + */ + function getRawComment() { + return $this->mComment; + } + + /** + * @return bool + */ + function isMinor() { + return (bool)$this->mMinorEdit; + } + + /** + * int $field one of DELETED_* bitfield constants + * @return bool + */ + function isDeleted( $field ) { + return ($this->mDeleted & $field) == $field; + } + + /** + * Fetch revision text if it's available to all users + * @return string + */ + function getText() { + if( $this->isDeleted( self::DELETED_TEXT ) ) { + return ""; + } else { + return $this->getRawText(); + } + } + + /** + * Fetch revision text without regard for view restrictions + * @return string + */ + function getRawText() { + if( is_null( $this->mText ) ) { + // Revision text is immutable. Load on demand: + $this->mText = $this->loadText(); + } + return $this->mText; + } + + /** + * @return string + */ + function getTimestamp() { + return wfTimestamp(TS_MW, $this->mTimestamp); + } + + /** + * @return bool + */ + function isCurrent() { + return $this->mCurrent; + } + + /** + * @return Revision + */ + function getPrevious() { + $prev = $this->mTitle->getPreviousRevisionID( $this->mId ); + if ( $prev ) { + return Revision::newFromTitle( $this->mTitle, $prev ); + } else { + return null; + } + } + + /** + * @return Revision + */ + function getNext() { + $next = $this->mTitle->getNextRevisionID( $this->mId ); + if ( $next ) { + return Revision::newFromTitle( $this->mTitle, $next ); + } else { + return null; + } + } + /**#@-*/ + + /** + * Get revision text associated with an old or archive row + * $row is usually an object from wfFetchRow(), both the flags and the text + * field must be included + * @static + * @param integer $row Id of a row + * @param string $prefix table prefix (default 'old_') + * @return string $text|false the text requested + */ + function getRevisionText( $row, $prefix = 'old_' ) { + $fname = 'Revision::getRevisionText'; + wfProfileIn( $fname ); + + # Get data + $textField = $prefix . 'text'; + $flagsField = $prefix . 'flags'; + + if( isset( $row->$flagsField ) ) { + $flags = explode( ',', $row->$flagsField ); + } else { + $flags = array(); + } + + if( isset( $row->$textField ) ) { + $text = $row->$textField; + } else { + wfProfileOut( $fname ); + return false; + } + + # Use external methods for external objects, text in table is URL-only then + if ( in_array( 'external', $flags ) ) { + $url=$text; + @list($proto,$path)=explode('://',$url,2); + if ($path=="") { + wfProfileOut( $fname ); + return false; + } + require_once('ExternalStore.php'); + $text=ExternalStore::fetchFromURL($url); + } + + // If the text was fetched without an error, convert it + if ( $text !== false ) { + if( in_array( 'gzip', $flags ) ) { + # Deal with optional compression of archived pages. + # This can be done periodically via maintenance/compressOld.php, and + # as pages are saved if $wgCompressRevisions is set. + $text = gzinflate( $text ); + } + + if( in_array( 'object', $flags ) ) { + # Generic compressed storage + $obj = unserialize( $text ); + if ( !is_object( $obj ) ) { + // Invalid object + wfProfileOut( $fname ); + return false; + } + $text = $obj->getText(); + } + + global $wgLegacyEncoding; + if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) { + # Old revisions kept around in a legacy encoding? + # Upconvert on demand. + global $wgInputEncoding, $wgContLang; + $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text ); + } + } + wfProfileOut( $fname ); + return $text; + } + + /** + * If $wgCompressRevisions is enabled, we will compress data. + * The input string is modified in place. + * Return value is the flags field: contains 'gzip' if the + * data is compressed, and 'utf-8' if we're saving in UTF-8 + * mode. + * + * @static + * @param mixed $text reference to a text + * @return string + */ + function compressRevisionText( &$text ) { + global $wgCompressRevisions; + $flags = array(); + + # Revisions not marked this way will be converted + # on load if $wgLegacyCharset is set in the future. + $flags[] = 'utf-8'; + + if( $wgCompressRevisions ) { + if( function_exists( 'gzdeflate' ) ) { + $text = gzdeflate( $text ); + $flags[] = 'gzip'; + } else { + wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" ); + } + } + return implode( ',', $flags ); + } + + /** + * Insert a new revision into the database, returning the new revision ID + * number on success and dies horribly on failure. + * + * @param Database $dbw + * @return int + */ + function insertOn( &$dbw ) { + global $wgDefaultExternalStore; + + $fname = 'Revision::insertOn'; + wfProfileIn( $fname ); + + $data = $this->mText; + $flags = Revision::compressRevisionText( $data ); + + # Write to external storage if required + if ( $wgDefaultExternalStore ) { + if ( is_array( $wgDefaultExternalStore ) ) { + // Distribute storage across multiple clusters + $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)]; + } else { + $store = $wgDefaultExternalStore; + } + require_once('ExternalStore.php'); + // Store and get the URL + $data = ExternalStore::insert( $store, $data ); + if ( !$data ) { + # This should only happen in the case of a configuration error, where the external store is not valid + throw new MWException( "Unable to store text to external storage $store" ); + } + if ( $flags ) { + $flags .= ','; + } + $flags .= 'external'; + } + + # Record the text (or external storage URL) to the text table + if( !isset( $this->mTextId ) ) { + $old_id = $dbw->nextSequenceValue( 'text_old_id_val' ); + $dbw->insert( 'text', + array( + 'old_id' => $old_id, + 'old_text' => $data, + 'old_flags' => $flags, + ), $fname + ); + $this->mTextId = $dbw->insertId(); + } + + # Record the edit in revisions + $rev_id = isset( $this->mId ) + ? $this->mId + : $dbw->nextSequenceValue( 'rev_rev_id_val' ); + $dbw->insert( 'revision', + array( + 'rev_id' => $rev_id, + 'rev_page' => $this->mPage, + 'rev_text_id' => $this->mTextId, + 'rev_comment' => $this->mComment, + 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, + 'rev_user' => $this->mUser, + 'rev_user_text' => $this->mUserText, + 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'rev_deleted' => $this->mDeleted, + ), $fname + ); + + $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId(); + wfProfileOut( $fname ); + return $this->mId; + } + + /** + * Lazy-load the revision's text. + * Currently hardcoded to the 'text' table storage engine. + * + * @return string + * @access private + */ + function loadText() { + $fname = 'Revision::loadText'; + wfProfileIn( $fname ); + + $dbr =& wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'text', + array( 'old_text', 'old_flags' ), + array( 'old_id' => $this->getTextId() ), + $fname); + + if( !$row ) { + $dbw =& wfGetDB( DB_MASTER ); + $row = $dbw->selectRow( 'text', + array( 'old_text', 'old_flags' ), + array( 'old_id' => $this->getTextId() ), + $fname); + } + + $text = Revision::getRevisionText( $row ); + wfProfileOut( $fname ); + + return $text; + } + + /** + * Create a new null-revision for insertion into a page's + * history. This will not re-save the text, but simply refer + * to the text from the previous version. + * + * Such revisions can for instance identify page rename + * operations and other such meta-modifications. + * + * @param Database $dbw + * @param int $pageId ID number of the page to read from + * @param string $summary + * @param bool $minor + * @return Revision + */ + function newNullRevision( &$dbw, $pageId, $summary, $minor ) { + $fname = 'Revision::newNullRevision'; + wfProfileIn( $fname ); + + $current = $dbw->selectRow( + array( 'page', 'revision' ), + array( 'page_latest', 'rev_text_id' ), + array( + 'page_id' => $pageId, + 'page_latest=rev_id', + ), + $fname ); + + if( $current ) { + $revision = new Revision( array( + 'page' => $pageId, + 'comment' => $summary, + 'minor_edit' => $minor, + 'text_id' => $current->rev_text_id, + ) ); + } else { + $revision = null; + } + + wfProfileOut( $fname ); + return $revision; + } + + /** + * Determine if the current user is allowed to view a particular + * field of this revision, if it's marked as deleted. + * @param int $field one of self::DELETED_TEXT, + * self::DELETED_COMMENT, + * self::DELETED_USER + * @return bool + */ + function userCan( $field ) { + if( ( $this->mDeleted & $field ) == $field ) { + global $wgUser; + $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED + ? 'hiderevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } + + + /** + * Get rev_timestamp from rev_id, without loading the rest of the row + * @param integer $id + */ + static function getTimestampFromID( $id ) { + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $id ), __METHOD__ ); + if ( $timestamp === false ) { + # Not in slave, try master + $dbw =& wfGetDB( DB_MASTER ); + $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $id ), __METHOD__ ); + } + return $timestamp; + } + + static function countByPageId( $db, $id ) { + $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount', + array( 'rev_page' => $id ), __METHOD__ ); + if( $row ) { + return $row->revCount; + } + return 0; + } + + static function countByTitle( $db, $title ) { + $id = $title->getArticleId(); + if( $id ) { + return Revision::countByPageId( $db, $id ); + } + return 0; + } +} + +/** + * Aliases for backwards compatibility with 1.6 + */ +define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT ); +define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT ); +define( 'MW_REV_DELETED_USER', Revision::DELETED_USER ); +define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED ); + + +?> diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php new file mode 100644 index 00000000..f5a24dfa --- /dev/null +++ b/includes/Sanitizer.php @@ -0,0 +1,1184 @@ + et al + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @package MediaWiki + * @subpackage Parser + */ + +/** + * Regular expression to match various types of character references in + * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences + */ +define( 'MW_CHAR_REFS_REGEX', + '/&([A-Za-z0-9]+); + |&\#([0-9]+); + |&\#x([0-9A-Za-z]+); + |&\#X([0-9A-Za-z]+); + |(&)/x' ); + +/** + * Regular expression to match HTML/XML attribute pairs within a tag. + * Allows some... latitude. + * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes + */ +$attrib = '[A-Za-z0-9]'; +$space = '[\x09\x0a\x0d\x20]'; +define( 'MW_ATTRIBS_REGEX', + "/(?:^|$space)($attrib+) + ($space*=$space* + (?: + # The attribute value: quoted or alone + \"([^<\"]*)\" + | '([^<']*)' + | ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+) + | (\#[0-9a-fA-F]+) # Technically wrong, but lots of + # colors are specified like this. + # We'll be normalizing it. + ) + )?(?=$space|\$)/sx" ); + +/** + * List of all named character entities defined in HTML 4.01 + * http://www.w3.org/TR/html4/sgml/entities.html + * @private + */ +global $wgHtmlEntities; +$wgHtmlEntities = array( + 'Aacute' => 193, + 'aacute' => 225, + 'Acirc' => 194, + 'acirc' => 226, + 'acute' => 180, + 'AElig' => 198, + 'aelig' => 230, + 'Agrave' => 192, + 'agrave' => 224, + 'alefsym' => 8501, + 'Alpha' => 913, + 'alpha' => 945, + 'amp' => 38, + 'and' => 8743, + 'ang' => 8736, + 'Aring' => 197, + 'aring' => 229, + 'asymp' => 8776, + 'Atilde' => 195, + 'atilde' => 227, + 'Auml' => 196, + 'auml' => 228, + 'bdquo' => 8222, + 'Beta' => 914, + 'beta' => 946, + 'brvbar' => 166, + 'bull' => 8226, + 'cap' => 8745, + 'Ccedil' => 199, + 'ccedil' => 231, + 'cedil' => 184, + 'cent' => 162, + 'Chi' => 935, + 'chi' => 967, + 'circ' => 710, + 'clubs' => 9827, + 'cong' => 8773, + 'copy' => 169, + 'crarr' => 8629, + 'cup' => 8746, + 'curren' => 164, + 'dagger' => 8224, + 'Dagger' => 8225, + 'darr' => 8595, + 'dArr' => 8659, + 'deg' => 176, + 'Delta' => 916, + 'delta' => 948, + 'diams' => 9830, + 'divide' => 247, + 'Eacute' => 201, + 'eacute' => 233, + 'Ecirc' => 202, + 'ecirc' => 234, + 'Egrave' => 200, + 'egrave' => 232, + 'empty' => 8709, + 'emsp' => 8195, + 'ensp' => 8194, + 'Epsilon' => 917, + 'epsilon' => 949, + 'equiv' => 8801, + 'Eta' => 919, + 'eta' => 951, + 'ETH' => 208, + 'eth' => 240, + 'Euml' => 203, + 'euml' => 235, + 'euro' => 8364, + 'exist' => 8707, + 'fnof' => 402, + 'forall' => 8704, + 'frac12' => 189, + 'frac14' => 188, + 'frac34' => 190, + 'frasl' => 8260, + 'Gamma' => 915, + 'gamma' => 947, + 'ge' => 8805, + 'gt' => 62, + 'harr' => 8596, + 'hArr' => 8660, + 'hearts' => 9829, + 'hellip' => 8230, + 'Iacute' => 205, + 'iacute' => 237, + 'Icirc' => 206, + 'icirc' => 238, + 'iexcl' => 161, + 'Igrave' => 204, + 'igrave' => 236, + 'image' => 8465, + 'infin' => 8734, + 'int' => 8747, + 'Iota' => 921, + 'iota' => 953, + 'iquest' => 191, + 'isin' => 8712, + 'Iuml' => 207, + 'iuml' => 239, + 'Kappa' => 922, + 'kappa' => 954, + 'Lambda' => 923, + 'lambda' => 955, + 'lang' => 9001, + 'laquo' => 171, + 'larr' => 8592, + 'lArr' => 8656, + 'lceil' => 8968, + 'ldquo' => 8220, + 'le' => 8804, + 'lfloor' => 8970, + 'lowast' => 8727, + 'loz' => 9674, + 'lrm' => 8206, + 'lsaquo' => 8249, + 'lsquo' => 8216, + 'lt' => 60, + 'macr' => 175, + 'mdash' => 8212, + 'micro' => 181, + 'middot' => 183, + 'minus' => 8722, + 'Mu' => 924, + 'mu' => 956, + 'nabla' => 8711, + 'nbsp' => 160, + 'ndash' => 8211, + 'ne' => 8800, + 'ni' => 8715, + 'not' => 172, + 'notin' => 8713, + 'nsub' => 8836, + 'Ntilde' => 209, + 'ntilde' => 241, + 'Nu' => 925, + 'nu' => 957, + 'Oacute' => 211, + 'oacute' => 243, + 'Ocirc' => 212, + 'ocirc' => 244, + 'OElig' => 338, + 'oelig' => 339, + 'Ograve' => 210, + 'ograve' => 242, + 'oline' => 8254, + 'Omega' => 937, + 'omega' => 969, + 'Omicron' => 927, + 'omicron' => 959, + 'oplus' => 8853, + 'or' => 8744, + 'ordf' => 170, + 'ordm' => 186, + 'Oslash' => 216, + 'oslash' => 248, + 'Otilde' => 213, + 'otilde' => 245, + 'otimes' => 8855, + 'Ouml' => 214, + 'ouml' => 246, + 'para' => 182, + 'part' => 8706, + 'permil' => 8240, + 'perp' => 8869, + 'Phi' => 934, + 'phi' => 966, + 'Pi' => 928, + 'pi' => 960, + 'piv' => 982, + 'plusmn' => 177, + 'pound' => 163, + 'prime' => 8242, + 'Prime' => 8243, + 'prod' => 8719, + 'prop' => 8733, + 'Psi' => 936, + 'psi' => 968, + 'quot' => 34, + 'radic' => 8730, + 'rang' => 9002, + 'raquo' => 187, + 'rarr' => 8594, + 'rArr' => 8658, + 'rceil' => 8969, + 'rdquo' => 8221, + 'real' => 8476, + 'reg' => 174, + 'rfloor' => 8971, + 'Rho' => 929, + 'rho' => 961, + 'rlm' => 8207, + 'rsaquo' => 8250, + 'rsquo' => 8217, + 'sbquo' => 8218, + 'Scaron' => 352, + 'scaron' => 353, + 'sdot' => 8901, + 'sect' => 167, + 'shy' => 173, + 'Sigma' => 931, + 'sigma' => 963, + 'sigmaf' => 962, + 'sim' => 8764, + 'spades' => 9824, + 'sub' => 8834, + 'sube' => 8838, + 'sum' => 8721, + 'sup' => 8835, + 'sup1' => 185, + 'sup2' => 178, + 'sup3' => 179, + 'supe' => 8839, + 'szlig' => 223, + 'Tau' => 932, + 'tau' => 964, + 'there4' => 8756, + 'Theta' => 920, + 'theta' => 952, + 'thetasym' => 977, + 'thinsp' => 8201, + 'THORN' => 222, + 'thorn' => 254, + 'tilde' => 732, + 'times' => 215, + 'trade' => 8482, + 'Uacute' => 218, + 'uacute' => 250, + 'uarr' => 8593, + 'uArr' => 8657, + 'Ucirc' => 219, + 'ucirc' => 251, + 'Ugrave' => 217, + 'ugrave' => 249, + 'uml' => 168, + 'upsih' => 978, + 'Upsilon' => 933, + 'upsilon' => 965, + 'Uuml' => 220, + 'uuml' => 252, + 'weierp' => 8472, + 'Xi' => 926, + 'xi' => 958, + 'Yacute' => 221, + 'yacute' => 253, + 'yen' => 165, + 'Yuml' => 376, + 'yuml' => 255, + 'Zeta' => 918, + 'zeta' => 950, + 'zwj' => 8205, + 'zwnj' => 8204 ); + +/** @package MediaWiki */ +class Sanitizer { + /** + * Cleans up HTML, removes dangerous tags and attributes, and + * removes HTML comments + * @private + * @param string $text + * @param callback $processCallback to do any variable or parameter replacements in HTML attribute values + * @param array $args for the processing callback + * @return string + */ + function removeHTMLtags( $text, $processCallback = null, $args = array() ) { + global $wgUseTidy, $wgUserHtml; + $fname = 'Parser::removeHTMLtags'; + wfProfileIn( $fname ); + + if( $wgUserHtml ) { + $htmlpairs = array( # Tags that must be closed + 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', + 'strike', 'strong', 'tt', 'var', 'div', 'center', + 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', + 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u' + ); + $htmlsingle = array( + 'br', 'hr', 'li', 'dt', 'dd' + ); + $htmlsingleonly = array( # Elements that cannot have close tags + 'br', 'hr' + ); + $htmlnest = array( # Tags that can be nested--?? + 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul', + 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span' + ); + $tabletags = array( # Can only appear inside table + 'td', 'th', 'tr', + ); + $htmllist = array( # Tags used by list + 'ul','ol', + ); + $listtags = array( # Tags that can appear in a list + 'li', + ); + + } else { + $htmlpairs = array(); + $htmlsingle = array(); + $htmlnest = array(); + $tabletags = array(); + } + + $htmlsingleallowed = array_merge( $htmlsingle, $tabletags ); + $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest ); + + # Remove HTML comments + $text = Sanitizer::removeHTMLcomments( $text ); + $bits = explode( '<', $text ); + $text = array_shift( $bits ); + if(!$wgUseTidy) { + $tagstack = array(); $tablestack = array(); + foreach ( $bits as $x ) { + $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) ); + preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', + $x, $regs ); + list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; + error_reporting( $prev ); + + $badtag = 0 ; + if ( in_array( $t = strtolower( $t ), $htmlelements ) ) { + # Check our stack + if ( $slash ) { + # Closing a tag... + if( in_array( $t, $htmlsingleonly ) ) { + $badtag = 1; + } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) { + if ( in_array($ot, $htmlsingleallowed) ) { + # Pop all elements with an optional close tag + # and see if we find a match below them + $optstack = array(); + array_push ($optstack, $ot); + while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) && + in_array($ot, $htmlsingleallowed) ) { + array_push ($optstack, $ot); + } + if ( $t != $ot ) { + # No match. Push the optinal elements back again + $badtag = 1; + while ( $ot = @array_pop( $optstack ) ) { + array_push( $tagstack, $ot ); + } + } + } else { + @array_push( $tagstack, $ot ); + #
  • can be nested in