summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2006-10-11 18:12:39 +0000
committerPierre Schmitz <pierre@archlinux.de>2006-10-11 18:12:39 +0000
commit183851b06bd6c52f3cae5375f433da720d410447 (patch)
treea477257decbf3360127f6739c2f9d0ec57a03d39 /includes
MediaWiki 1.7.1 wiederhergestellt
Diffstat (limited to 'includes')
-rw-r--r--includes/.htaccess1
-rw-r--r--includes/AjaxDispatcher.php83
-rw-r--r--includes/AjaxFunctions.php157
-rw-r--r--includes/Article.php2575
-rw-r--r--includes/AuthPlugin.php232
-rw-r--r--includes/AutoLoader.php272
-rw-r--r--includes/BagOStuff.php538
-rw-r--r--includes/Block.php440
-rw-r--r--includes/CacheManager.php159
-rw-r--r--includes/CategoryPage.php315
-rw-r--r--includes/Categoryfinder.php191
-rw-r--r--includes/ChangesList.php653
-rw-r--r--includes/CoreParserFunctions.php150
-rw-r--r--includes/Credits.php187
-rw-r--r--includes/Database.php2020
-rw-r--r--includes/DatabaseFunctions.php414
-rw-r--r--includes/DatabaseMysql.php6
-rw-r--r--includes/DatabaseOracle.php692
-rw-r--r--includes/DatabasePostgres.php609
-rw-r--r--includes/DateFormatter.php288
-rw-r--r--includes/DefaultSettings.php2189
-rw-r--r--includes/Defines.php183
-rw-r--r--includes/DifferenceEngine.php1751
-rw-r--r--includes/DjVuImage.php214
-rw-r--r--includes/EditPage.php1864
-rw-r--r--includes/Exception.php193
-rw-r--r--includes/Exif.php1124
-rw-r--r--includes/Export.php736
-rw-r--r--includes/ExternalEdit.php77
-rw-r--r--includes/ExternalStore.php70
-rw-r--r--includes/ExternalStoreDB.php150
-rw-r--r--includes/ExternalStoreHttp.php23
-rw-r--r--includes/FakeTitle.php88
-rw-r--r--includes/Feed.php310
-rw-r--r--includes/FileStore.php377
-rw-r--r--includes/GlobalFunctions.php2005
-rw-r--r--includes/HTMLCacheUpdate.php230
-rw-r--r--includes/HTMLForm.php177
-rw-r--r--includes/HistoryBlob.php308
-rw-r--r--includes/Hooks.php131
-rw-r--r--includes/HttpFunctions.php91
-rw-r--r--includes/Image.php2265
-rw-r--r--includes/ImageFunctions.php223
-rw-r--r--includes/ImageGallery.php211
-rw-r--r--includes/ImagePage.php726
-rw-r--r--includes/JobQueue.php267
-rw-r--r--includes/Licenses.php171
-rw-r--r--includes/LinkBatch.php184
-rw-r--r--includes/LinkCache.php178
-rw-r--r--includes/LinkFilter.php92
-rw-r--r--includes/Linker.php1101
-rw-r--r--includes/LinksUpdate.php601
-rw-r--r--includes/LoadBalancer.php666
-rw-r--r--includes/LogPage.php246
-rw-r--r--includes/MacBinary.php272
-rw-r--r--includes/MagicWord.php448
-rw-r--r--includes/Math.php269
-rw-r--r--includes/MemcachedSessions.php74
-rw-r--r--includes/MessageCache.php581
-rw-r--r--includes/Metadata.php362
-rw-r--r--includes/MimeMagic.php695
-rw-r--r--includes/Namespace.php129
-rw-r--r--includes/ObjectCache.php125
-rw-r--r--includes/OutputPage.php1078
-rw-r--r--includes/PageHistory.php685
-rw-r--r--includes/Parser.php4727
-rw-r--r--includes/ParserCache.php127
-rw-r--r--includes/ParserXML.php643
-rw-r--r--includes/ProfilerSimple.php108
-rw-r--r--includes/ProfilerSimpleUDP.php34
-rw-r--r--includes/ProfilerStub.php26
-rw-r--r--includes/Profiling.php353
-rw-r--r--includes/ProtectionForm.php244
-rw-r--r--includes/ProxyTools.php233
-rw-r--r--includes/QueryPage.php483
-rw-r--r--includes/RawPage.php203
-rw-r--r--includes/RecentChange.php509
-rw-r--r--includes/Revision.php799
-rw-r--r--includes/Sanitizer.php1184
-rw-r--r--includes/SearchEngine.php345
-rw-r--r--includes/SearchMySQL.php206
-rw-r--r--includes/SearchMySQL4.php73
-rw-r--r--includes/SearchPostgres.php156
-rw-r--r--includes/SearchTsearch2.php123
-rw-r--r--includes/SearchUpdate.php115
-rw-r--r--includes/Setup.php330
-rw-r--r--includes/SiteConfiguration.php121
-rw-r--r--includes/SiteStatsUpdate.php82
-rw-r--r--includes/Skin.php1499
-rw-r--r--includes/SkinTemplate.php1109
-rw-r--r--includes/SpecialAllmessages.php212
-rw-r--r--includes/SpecialAllpages.php322
-rw-r--r--includes/SpecialAncientpages.php65
-rw-r--r--includes/SpecialBlockip.php239
-rw-r--r--includes/SpecialBlockme.php40
-rw-r--r--includes/SpecialBooksources.php109
-rw-r--r--includes/SpecialBrokenRedirects.php88
-rw-r--r--includes/SpecialCategories.php68
-rw-r--r--includes/SpecialConfirmemail.php97
-rw-r--r--includes/SpecialContributions.php444
-rw-r--r--includes/SpecialDeadendpages.php63
-rw-r--r--includes/SpecialDisambiguations.php81
-rw-r--r--includes/SpecialDoubleRedirects.php107
-rw-r--r--includes/SpecialEmailuser.php160
-rw-r--r--includes/SpecialExport.php106
-rw-r--r--includes/SpecialImagelist.php121
-rw-r--r--includes/SpecialImport.php848
-rw-r--r--includes/SpecialIpblocklist.php255
-rw-r--r--includes/SpecialListredirects.php69
-rw-r--r--includes/SpecialListusers.php235
-rw-r--r--includes/SpecialLockdb.php118
-rw-r--r--includes/SpecialLog.php427
-rw-r--r--includes/SpecialLonelypages.php58
-rw-r--r--includes/SpecialLongpages.php41
-rw-r--r--includes/SpecialMIMEsearch.php155
-rw-r--r--includes/SpecialMostcategories.php68
-rw-r--r--includes/SpecialMostimages.php64
-rw-r--r--includes/SpecialMostlinked.php98
-rw-r--r--includes/SpecialMostlinkedcategories.php81
-rw-r--r--includes/SpecialMostrevisions.php68
-rw-r--r--includes/SpecialMovepage.php283
-rw-r--r--includes/SpecialNewimages.php204
-rw-r--r--includes/SpecialNewpages.php198
-rw-r--r--includes/SpecialPage.php575
-rw-r--r--includes/SpecialPopularpages.php59
-rw-r--r--includes/SpecialPreferences.php937
-rw-r--r--includes/SpecialPrefixindex.php149
-rw-r--r--includes/SpecialRandompage.php58
-rw-r--r--includes/SpecialRandomredirect.php54
-rw-r--r--includes/SpecialRecentchanges.php709
-rw-r--r--includes/SpecialRecentchangeslinked.php173
-rw-r--r--includes/SpecialRevisiondelete.php258
-rw-r--r--includes/SpecialSearch.php413
-rw-r--r--includes/SpecialShortpages.php91
-rw-r--r--includes/SpecialSpecialpages.php73
-rw-r--r--includes/SpecialStatistics.php86
-rw-r--r--includes/SpecialUncategorizedcategories.php39
-rw-r--r--includes/SpecialUncategorizedimages.php55
-rw-r--r--includes/SpecialUncategorizedpages.php59
-rw-r--r--includes/SpecialUndelete.php737
-rw-r--r--includes/SpecialUnlockdb.php105
-rw-r--r--includes/SpecialUnusedcategories.php48
-rw-r--r--includes/SpecialUnusedimages.php86
-rw-r--r--includes/SpecialUnusedtemplates.php59
-rw-r--r--includes/SpecialUnwatchedpages.php71
-rw-r--r--includes/SpecialUpload.php1109
-rw-r--r--includes/SpecialUploadMogile.php135
-rw-r--r--includes/SpecialUserlogin.php671
-rw-r--r--includes/SpecialUserlogout.php27
-rw-r--r--includes/SpecialUserrights.php183
-rw-r--r--includes/SpecialVersion.php270
-rw-r--r--includes/SpecialWantedcategories.php85
-rw-r--r--includes/SpecialWantedpages.php133
-rw-r--r--includes/SpecialWatchlist.php513
-rw-r--r--includes/SpecialWhatlinkshere.php277
-rw-r--r--includes/SquidUpdate.php279
-rw-r--r--includes/StreamFile.php72
-rw-r--r--includes/Title.php2307
-rw-r--r--includes/User.php1986
-rw-r--r--includes/UserMailer.php414
-rw-r--r--includes/Utf8Case.php1506
-rw-r--r--includes/WatchedItem.php190
-rw-r--r--includes/WebRequest.php491
-rw-r--r--includes/Wiki.php410
-rw-r--r--includes/WikiError.php125
-rw-r--r--includes/Xml.php279
-rw-r--r--includes/XmlFunctions.php65
-rw-r--r--includes/ZhClient.php149
-rw-r--r--includes/ZhConversion.php8457
-rw-r--r--includes/cbt/CBTCompiler.php369
-rw-r--r--includes/cbt/CBTProcessor.php540
-rw-r--r--includes/cbt/README108
-rw-r--r--includes/memcached-client.php1060
-rw-r--r--includes/mime.info76
-rw-r--r--includes/mime.types117
-rw-r--r--includes/normal/CleanUpTest.php423
-rw-r--r--includes/normal/Makefile72
-rw-r--r--includes/normal/README55
-rw-r--r--includes/normal/RandomTest.php107
-rw-r--r--includes/normal/Utf8Test.php151
-rw-r--r--includes/normal/UtfNormal.php792
-rw-r--r--includes/normal/UtfNormalBench.php107
-rw-r--r--includes/normal/UtfNormalData.inc13
-rw-r--r--includes/normal/UtfNormalDataK.inc10
-rw-r--r--includes/normal/UtfNormalGenerate.php235
-rw-r--r--includes/normal/UtfNormalTest.php249
-rw-r--r--includes/normal/UtfNormalUtil.php142
-rw-r--r--includes/proxy_check.php55
-rw-r--r--includes/templates/Userlogin.php215
-rw-r--r--includes/zhtable/Makefile268
-rw-r--r--includes/zhtable/README16
-rw-r--r--includes/zhtable/printutf8.c99
-rw-r--r--includes/zhtable/simp2trad.manual178
-rw-r--r--includes/zhtable/toCN.manual331
-rw-r--r--includes/zhtable/toHK.manual211
-rw-r--r--includes/zhtable/toSG.manual15
-rw-r--r--includes/zhtable/toTW.manual309
-rw-r--r--includes/zhtable/trad2simp.manual15
-rw-r--r--includes/zhtable/tradphrases.manual149
199 files changed, 84860 insertions, 0 deletions
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 @@
+<?php
+
+//$wgRequestTime = microtime();
+
+// unset( $IP );
+// @ini_set( 'allow_url_fopen', 0 ); # For security...
+
+# Valid web server entry point, enable includes.
+# Please don't move this line to includes/Defines.php. This line essentially defines
+# a valid entry point. If you put it in includes/Defines.php, then any script that includes
+# it becomes an entry point, thereby defeating its purpose.
+// define( 'MEDIAWIKI', true );
+// require_once( './includes/Defines.php' );
+// require_once( './LocalSettings.php' );
+// require_once( 'includes/Setup.php' );
+require_once( 'AjaxFunctions.php' );
+
+if ( ! $wgUseAjax ) {
+ die( 1 );
+}
+
+class AjaxDispatcher {
+ var $mode;
+ var $func_name;
+ var $args;
+
+ function AjaxDispatcher() {
+ global $wgAjaxCachePolicy;
+
+ wfProfileIn( 'AjaxDispatcher::AjaxDispatcher' );
+
+ $wgAjaxCachePolicy = new AjaxCachePolicy();
+
+ $this->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 @@
+<?php
+
+if( !defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+require_once('WebRequest.php');
+
+/**
+ * Function converts an Javascript escaped string back into a string with
+ * specified charset (default is UTF-8).
+ * Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps
+ *
+ * @param $source String escaped with Javascript's escape() function
+ * @param $iconv_to String destination character set will be used as second paramether in the iconv function. Default is UTF-8.
+ * @return string
+ */
+function js_unescape($source, $iconv_to = 'UTF-8') {
+ $decodedStr = '';
+ $pos = 0;
+ $len = strlen ($source);
+ while ($pos < $len) {
+ $charAt = substr ($source, $pos, 1);
+ if ($charAt == '%') {
+ $pos++;
+ $charAt = substr ($source, $pos, 1);
+ if ($charAt == 'u') {
+ // we got a unicode character
+ $pos++;
+ $unicodeHexVal = substr ($source, $pos, 4);
+ $unicode = hexdec ($unicodeHexVal);
+ $decodedStr .= code2utf($unicode);
+ $pos += 4;
+ }
+ else {
+ // we have an escaped ascii character
+ $hexVal = substr ($source, $pos, 2);
+ $decodedStr .= chr (hexdec ($hexVal));
+ $pos += 2;
+ }
+ }
+ else {
+ $decodedStr .= $charAt;
+ $pos++;
+ }
+ }
+
+ if ($iconv_to != "UTF-8") {
+ $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr);
+ }
+
+ return $decodedStr;
+}
+
+/**
+ * Function coverts number of utf char into that character.
+ * Function taken from: http://sk2.php.net/manual/en/function.utf8-encode.php#49336
+ *
+ * @param $num Integer
+ * @return utf8char
+ */
+function code2utf($num){
+ if ( $num<128 )
+ return chr($num);
+ if ( $num<2048 )
+ return chr(($num>>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 .= '<li>' . $l->makeKnownLinkObj( $nt ) . "</li>\n";
+ }
+ if ( $i > $limit ) {
+ $more = '<i>' . $l->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
+ wfMsg('moredotdotdot'),
+ "namespace=0&from=" . wfUrlEncode ( $term ) ) .
+ '</i>';
+ } else {
+ $more = '';
+ }
+
+ $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
+ $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) );
+
+ $term = htmlspecialchars( $term );
+ return '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">'
+ . wfMsg( 'hideresults' ) . '</a></div>'
+ . '<h1 class="firstHeading">'.wfMsg('search')
+ . '</h1><div id="contentSub">'. $subtitle . '</div><ul><li>'
+ . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+ wfMsg( 'searchcontaining', $term ),
+ "search=$term&fulltext=Search" )
+ . '</li><li>' . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+ wfMsg( 'searchnamed', $term ) ,
+ "search=$term&go=Go" )
+ . "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>"
+ . '<ul>' .$r .'</ul>'.$more;
+}
+
+?>
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 @@
+<?php
+/**
+ * File for articles
+ * @package MediaWiki
+ */
+
+/**
+ * Need the CacheManager to be loaded
+ */
+require_once( 'CacheManager.php' );
+
+/**
+ * Class representing a MediaWiki article and history.
+ *
+ * See design.txt for an overview.
+ * Note: edit user interface and cache support functions have been
+ * moved to separate EditPage and CacheManager classes.
+ *
+ * @package MediaWiki
+ */
+class Article {
+ /**@{{
+ * @private
+ */
+ var $mComment; //!<
+ var $mContent; //!<
+ var $mContentLoaded; //!<
+ var $mCounter; //!<
+ var $mForUpdate; //!<
+ var $mGoodAdjustment; //!<
+ var $mLatest; //!<
+ var $mMinorEdit; //!<
+ var $mOldId; //!<
+ var $mRedirectedFrom; //!<
+ var $mRedirectUrl; //!<
+ var $mRevIdFetched; //!<
+ var $mRevision; //!<
+ var $mTimestamp; //!<
+ var $mTitle; //!<
+ var $mTotalAdjustment; //!<
+ var $mTouched; //!<
+ var $mUser; //!<
+ var $mUserText; //!<
+ /**@}}*/
+
+ /**
+ * Constructor and clear the article
+ * @param $title Reference to a Title object.
+ * @param $oldId Integer revision ID, null to fetch from request, zero for current
+ */
+ function Article( &$title, $oldId = null ) {
+ $this->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 "<div class='noarticletext'>$ret</div>";
+ } 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 \<h1\>Heading\</h1\>, or
+ * the first section before any such heading (section 0).
+ *
+ * If a section contains subsections, these are also returned.
+ *
+ * @param $text String: text to look in
+ * @param $section Integer: section number
+ * @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( '<pre>'.htmlspecialchars($this->mContent)."\n</pre>" );
+ } 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( '<img src="'.$imageUrl.'" alt="#REDIRECT" />' .
+ '<span class="redirectText">'.$link.'</span>' );
+
+ $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(
+ "<div class='patrollink'>" .
+ wfMsg ( 'markaspatrolledlink',
+ $sk->makeKnownLinkObj( $this->mTitle, wfMsg('markaspatrolledtext'), "action=markpatrolled&rcid=$rcid" )
+ ) .
+ '</div>'
+ );
+ }
+
+ # 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',
+ "<form method=\"post\" action=\"$action\">\n" .
+ "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" .
+ "</form>\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( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' );
+ }
+
+ # If a single user is responsible for all revisions, find out who they are
+ if ( count( $authors ) == $maxRevisions ) {
+ // Query bailed out, too many revisions to find out if they're all the same
+ $authorOfAll = false;
+ } else {
+ $authorOfAll = reset( $authors );
+ foreach ( $authors as $author ) {
+ if ( $authorOfAll != $author ) {
+ $authorOfAll = false;
+ break;
+ }
+ }
+ }
+ # Fetch article text
+ $rev = Revision::newFromTitle( $this->mTitle );
+
+ if( !is_null( $rev ) ) {
+ # if this is a mini-text, we can paste part of it into the deletion reason
+ $text = $rev->getText();
+
+ #if this is empty, an earlier revision may contain "useful" text
+ $blanked = false;
+ if( $text == '' ) {
+ $prev = $rev->getPrevious();
+ if( $prev ) {
+ $text = $prev->getText();
+ $blanked = true;
+ }
+ }
+
+ $length = strlen( $text );
+
+ # this should not happen, since it is not possible to store an empty, new
+ # page. Let's insert a standard text in case it does, though
+ if( $length == 0 && $reason === '' ) {
+ $reason = wfMsgForContent( 'exblank' );
+ }
+
+ if( $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( "
+<form id='deleteconfirm' method='post' action=\"{$formaction}\">
+ <table border='0'>
+ <tr>
+ <td align='right'>
+ <label for='wpReason'>{$delcom}:</label>
+ </td>
+ <td align='left'>
+ <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" />
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type='submit' name='wpConfirmB' value=\"{$confirm}\" />
+ </td>
+ </tr>
+ </table>
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+</form>\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( '<h2>' . htmlspecialchars( $newComment ) . "</h2>\n<hr />\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 \<date\>; 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( "<ul><li>" . wfMsg("numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' );
+ $wgOut->addHTML( "<li>" . wfMsg('numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>');
+ if( $talkInfo ) {
+ $wgOut->addHTML( '<li>' . wfMsg("numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>');
+ }
+ $wgOut->addHTML( '<li>' . wfMsg("numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' );
+ if( $talkInfo ) {
+ $wgOut->addHTML( '<li>' . wfMsg('numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' );
+ }
+ $wgOut->addHTML( '</ul>' );
+
+ }
+ }
+
+ /**
+ * 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 @@
+<?php
+/**
+ * @package MediaWiki
+ */
+# Copyright (C) 2004 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
+
+/**
+ * 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 @@
+<?php
+
+/* This defines autoloading handler for whole MediaWiki framework */
+
+ini_set('unserialize_callback_func', '__autoload' );
+
+function __autoload($className) {
+ global $wgAutoloadClasses;
+
+ static $localClasses = array(
+ 'AjaxDispatcher' => '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 @@
+<?php
+#
+# Copyright (C) 2003-2004 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
+ */
+
+/**
+ * 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<count($reps);$i++) {
+ $sql = str_replace(
+ '$' . $i,
+ $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 @@
+<?php
+/**
+ * Blocks and bans object
+ * @package MediaWiki
+ */
+
+/**
+ * The block class
+ * All the functions in this class assume the object is either explicitly
+ * loaded or filled. It is not load-on-demand. There are no accessors.
+ *
+ * To use delete(), you only need to fill $mAddress
+ * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags
+ *
+ * @todo This could be used everywhere, but it isn't.
+ * @package MediaWiki
+ */
+class Block
+{
+ /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry,
+ $mRangeStart, $mRangeEnd;
+ /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName;
+
+ const EB_KEEP_EXPIRED = 1;
+ const EB_FOR_UPDATE = 2;
+ const EB_RANGE_ONLY = 4;
+
+ function Block( $address = '', $user = '', $by = 0, $reason = '',
+ $timestamp = '' , $auto = 0, $expiry = '' )
+ {
+ $this->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 @@
+<?php
+/**
+ * Contain the CacheManager class
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+
+/**
+ * Handles talking to the file cache, putting stuff in and taking it back out.
+ * Mostly called from Article.php, also from DatabaseFunctions.php for the
+ * emergency abort/fallback to cache.
+ *
+ * Global options that affect this module:
+ * $wgCachePages
+ * $wgCacheEpoch
+ * $wgUseFileCache
+ * $wgFileCacheDirectory
+ * $wgUseGzip
+ * @package MediaWiki
+ */
+class CacheManager {
+ var $mTitle, $mFileCache;
+
+ function CacheManager( &$title ) {
+ $this->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( '</html>',
+ '<!-- Cached/compressed '.$now." -->\n</html>",
+ $text );
+ $text = gzencode( $rawtext );
+ } else {
+ $text = str_replace( '</html>',
+ '<!-- Cached '.$now." -->\n</html>",
+ $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 @@
+<?php
+/**
+ * Special handling for category description pages
+ * Modelled after ImagePage.php
+ *
+ * @package MediaWiki
+ */
+
+if( !defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+/**
+ * @package MediaWiki
+ */
+class CategoryPage extends Article {
+
+ function view() {
+ if(!wfRunHooks('CategoryPageView', array(&$this))) return;
+
+ if ( NS_CATEGORY == $this->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 = "<br style=\"clear:both;\"/>\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 .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\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 .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\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 = '<table width="100%"><tr valign="top">';
+
+ $prev_start_char = 'none';
+
+ // loop through the chunks
+ for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0;
+ $chunkIndex < 3;
+ $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1)
+ {
+ $r .= "<td>\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 .= "</ul>\n";
+ }
+ $cont_msg = "";
+ if ( $articles_start_char[$index] == $prev_start_char )
+ $cont_msg = wfMsgHtml('listingcontinuesabbrev');
+ $r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>";
+ $prev_start_char = $articles_start_char[$index];
+ }
+
+ $r .= "<li>{$articles[$index]}</li>";
+ }
+ if( !$atColumnTop ) {
+ $r .= "</ul>\n";
+ }
+ $r .= "</td>\n";
+
+
+ }
+ $r .= '</tr></table>';
+ 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 = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
+ $r .= '<ul><li>'.$articles[0].'</li>';
+ for ($index = 1; $index < count($articles); $index++ )
+ {
+ if ($articles_start_char[$index] != $articles_start_char[$index - 1])
+ {
+ $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
+ }
+
+ $r .= "<li>{$articles[$index]}</li>";
+ }
+ $r .= '</ul>';
+ 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 @@
+<?php
+/*
+The "Categoryfinder" class takes a list of articles, creates an internal representation of all their parent
+categories (as well as parents of parents etc.). From this representation, it determines which of these articles
+are in one or all of a given subset of categories.
+
+Example use :
+
+ # Determines wether the article with the page_id 12345 is in both
+ # "Category 1" and "Category 2" or their subcategories, respectively
+
+ $cf = new Categoryfinder ;
+ $cf->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 @@
+<?php
+/**
+ * @package MediaWiki
+ * Contain class to show various lists of change:
+ * - what's link here
+ * - related changes
+ * - recent changes
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class RCCacheEntry extends RecentChange
+{
+ var $secureName, $link;
+ var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ;
+ var $userlink, $timestamp, $watched;
+
+ function newFromParent( $rc )
+ {
+ $rc2 = new RCCacheEntry;
+ $rc2->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 = '&nbsp;', $bot = false ) {
+ $f = $new ? '<span class="newpage">' . $this->message['newpageletter'] . '</span>'
+ : $nothing;
+ $f .= $minor ? '<span class="minor">' . $this->message['minoreditletter'] . '</span>'
+ : $nothing;
+ $f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing;
+ $f .= $patrolled ? '<span class="unpatrolled">!</span>' : $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 "</ul>\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 .= "</ul>\n";
+ }
+ $s .= '<h4>'.$date."</h4>\n<ul class=\"special\">";
+ $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 = '<strong>'.$articlelink.'</strong>';
+ 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 .= '<li>';
+
+ // 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 .= "</li>\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 .= "<h4>{$date}</h4>\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.'&times;)';
+ }
+ array_push( $users, $text );
+ }
+
+ $users = ' <span class="changedby">['.implode('; ',$users).']</span>';
+
+ # Arrow
+ $rci = 'RCI'.$this->rcCacheIndex;
+ $rcl = 'RCL'.$this->rcCacheIndex;
+ $rcm = 'RCM'.$this->rcCacheIndex;
+ $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')";
+ $tl = '<span id="'.$rcm.'"><a href="'.$toggleLink.'">' . $this->sideArrow() . '</a></span>';
+ $tl .= '<span id="'.$rcl.'" style="display:none"><a href="'.$toggleLink.'">' . $this->downArrow() . '</a></span>';
+ $r .= $tl;
+
+ # Main line
+ $r .= '<tt>';
+ $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
+
+ # Timestamp
+ $r .= ' '.$block[0]->timestamp.' ';
+ $r .= '</tt>';
+
+ # 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 .= "<br />\n";
+
+ # Sub-entries
+ $r .= '<div id="'.$rci.'" style="display:none">';
+ foreach( $block as $rcObj ) {
+ # Get rc_xxxx variables
+ extract( $rcObj->mAttribs );
+
+ $r .= $this->spacerArrow();
+ $r .= '<tt>&nbsp; &nbsp; &nbsp; &nbsp;';
+ $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
+ $r .= '&nbsp;</tt>';
+
+ $o = '';
+ if( $rc_this_oldid != 0 ) {
+ $o = 'oldid='.$rc_this_oldid;
+ }
+ if( $rc_type == RC_LOG ) {
+ $link = $rcObj->timestamp;
+ } else {
+ $link = $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o );
+ }
+ $link = '<tt>'.$link.'</tt>';
+
+ $r .= $link;
+ $r .= ' (';
+ $r .= $rcObj->curlink;
+ $r .= '; ';
+ $r .= $rcObj->lastlink;
+ $r .= ') . . '.$rcObj->userlink;
+ $r .= $rcObj->usertalklink;
+ $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
+ $r .= "<br />\n";
+ }
+ $r .= "</div>\n";
+
+ $this->rcCacheIndex++;
+ return $r;
+ }
+
+ function maybeWatchedLink( $link, $watched=false ) {
+ if( $watched ) {
+ // FIXME: css style might be more appropriate
+ return '<strong>' . $link . '</strong>';
+ } 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 <img> tag
+ * @access private
+ */
+ function arrow( $dir, $alt='' ) {
+ global $wgStylePath;
+ $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
+ $encAlt = htmlspecialchars( $alt );
+ return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" />";
+ }
+
+ /**
+ * Generate HTML for a right- or left-facing arrow,
+ * depending on language direction.
+ * @return string HTML <img> 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 <img> tag
+ * @access private
+ */
+ function downArrow() {
+ return $this->arrow( 'd', '-' );
+ }
+
+ /**
+ * Generate HTML for a spacer image
+ * @return string HTML <img> 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 .= '<tt>';
+
+ if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+ $r .= '&nbsp;&nbsp;&nbsp;';
+ } else {
+ $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
+ }
+ $r .= ' '.$rcObj->timestamp.' </tt>';
+
+ # 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 .= "<br />\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 '<div>'.$blockOut.'</div>';
+ }
+
+ /**
+ * 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 @@
+<?php
+
+/**
+ * Various core parser functions, registered in Parser::firstCallInit()
+ */
+
+class CoreParserFunctions {
+ static function ns( $parser, $part1 = '' ) {
+ global $wgContLang;
+ $found = false;
+ if ( intval( $part1 ) || $part1 == "0" ) {
+ $text = $wgContLang->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 @@
+<?php
+/**
+ * Credits.php -- formats credits for articles
+ * Copyright 2004, Evan Prodromou <evan@wikitravel.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
+ *
+ * @author <evan@wikitravel.org>
+ * @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 @@
+<?php
+/**
+ * This file deals with MySQL interface functions
+ * and query specifics/optimisations
+ * @package MediaWiki
+ */
+
+/** See Database::makeList() */
+define( 'LIST_COMMA', 0 );
+define( 'LIST_AND', 1 );
+define( 'LIST_SET', 2 );
+define( 'LIST_NAMES', 3);
+define( 'LIST_OR', 4);
+
+/** Number of times to re-try an operation in case of deadlock */
+define( 'DEADLOCK_TRIES', 4 );
+/** Minimum time to wait before retry, in microseconds */
+define( 'DEADLOCK_DELAY_MIN', 500000 );
+/** Maximum time to wait before retry */
+define( 'DEADLOCK_DELAY_MAX', 1500000 );
+
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
+
+class DBObject {
+ public $mData;
+
+ function DBObject($data) {
+ $this->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 = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
+ $mainpage = 'Main Page';
+ $searchdisabled = <<<EOT
+<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
+EOT;
+
+ $googlesearch = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+ $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 = '<p style="color: red"><b>'.$msg."<br />\n" .
+ $cachederror . "</b></p>\n";
+
+ $tag = '<div id="article">';
+ $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 @@
+<?php
+/**
+ * Backwards compatibility wrapper for Database.php
+ *
+ * Note: $wgDatabase has ceased to exist. Destroy all references.
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ * @param $sql String: SQL query
+ * @param $db Mixed: database handler
+ * @param $fname String: name of the php function calling
+ */
+function wfQuery( $sql, $db, $fname = '' ) {
+ global $wgOut;
+ if ( !is_numeric( $db ) ) {
+ # Someone has tried to call this the old way
+ throw new FatalError( wfMsgNoDB( 'wrong_wfQuery_params', $db, $sql ) );
+ }
+ $c =& wfGetDB( $db );
+ if ( $c !== false ) {
+ return $c->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 @@
+<?php
+/*
+ * Stub database class for MySQL.
+ */
+require_once('Database.php');
+?>
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 @@
+<?php
+
+/**
+ * Oracle.
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Depends on database
+ */
+require_once( 'Database.php' );
+
+class OracleBlob extends DBObject {
+ function isLOB() {
+ return true;
+ }
+ function data() {
+ return $this->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", '<br />', $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 @@
+<?php
+
+/**
+ * This is PostgreSQL database abstraction layer.
+ *
+ * As it includes more generic version for DB functions,
+ * than MySQL ones, some of them should be moved to parent
+ * Database class.
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Depends on database
+ */
+require_once( 'Database.php' );
+
+class DatabasePostgres extends Database {
+ var $mInsertId = NULL;
+ var $mLastResult = NULL;
+
+ function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0 )
+ {
+
+ 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;
+
+ $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 "<li>Checking for tsearch2 ...";
+ if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
+ print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
+ print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
+ print " for instructions.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+
+ ## Do we have plpgsql installed?
+ print "<li>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 "<b>FAILED</b>. Make sure the language plpgsql is installed for the database <tt>$wgDBname</tt>t</li>";
+ ## XXX Better help
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+
+ ## Does the schema already exist? Who owns it?
+ $result = $this->schemaExists($wgDBmwschema);
+ if (!$result) {
+ print "<li>Creating schema <b>$wgDBmwschema</b> ...";
+ $result = $this->doQuery("CREATE SCHEMA $wgDBmwschema");
+ if (!$result) {
+ print "FAILED.</li>\n";
+ return false;
+ }
+ print "ok</li>\n";
+ }
+ else if ($result != $user) {
+ print "<li>Schema <b>$wgDBmwschema</b> exists but is not owned by <b>$user</b>. Not ideal.</li>\n";
+ }
+ else {
+ print "<li>Schema <b>$wgDBmwschema</b> exists and is owned by <b>$user ($result)</b>. Excellent.</li>\n";
+ }
+
+ ## Fix up the search paths if needed
+ print "<li>Setting the search path for user <b>$user</b> ...";
+ $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.</li>\n";
+ return false;
+ }
+ print "ok</li>\n";
+ ## Set for the rest of this session
+ $SQL = "SET search_path = $path";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<li>Failed to set search_path</li>\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( "<li>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 @@
+<?php
+/**
+ * Contain things
+ * @todo document
+ * @package MediaWiki
+ * @subpackage Parser
+ */
+
+/** */
+define('DF_ALL', -1);
+define('DF_NONE', 0);
+define('DF_MDY', 1);
+define('DF_DMY', 2);
+define('DF_YMD', 3);
+define('DF_ISO1', 4);
+define('DF_LASTPREF', 4);
+define('DF_ISO2', 5);
+define('DF_YDM', 6);
+define('DF_DM', 7);
+define('DF_MD', 8);
+define('DF_LAST', 8);
+
+/**
+ * @todo preferences, OutputPage
+ * @package MediaWiki
+ * @subpackage Parser
+ */
+class DateFormatter
+{
+ var $mSource, $mTarget;
+ var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
+
+ var $regexes, $pDays, $pMonths, $pYears;
+ var $rules, $xMonths;
+
+ /**
+ * @todo document
+ */
+ function DateFormatter() {
+ global $wgContLang;
+
+ $this->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 @@
+<?php
+/**
+ *
+ * NEVER EDIT THIS FILE
+ *
+ *
+ * To customize your installation, edit "LocalSettings.php". If you make
+ * changes here, they will be lost on next upgrade of MediaWiki!
+ *
+ * Note that since all these string interpolations are expanded
+ * before LocalSettings is included, if you localize something
+ * like $wgScriptPath, you must also localize everything that
+ * depends on it.
+ *
+ * Documentation is in the source and on:
+ * http://www.mediawiki.org/wiki/Help:Configuration_settings
+ *
+ * @package MediaWiki
+ */
+
+# This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined
+if( !defined( 'MEDIAWIKI' ) ) {
+ echo "This file is part of MediaWiki and is not a valid entry point\n";
+ die( 1 );
+}
+
+/**
+ * Create a site configuration object
+ * Not used for much in a default install
+ */
+require_once( 'includes/SiteConfiguration.php' );
+$wgConf = new SiteConfiguration;
+
+/** MediaWiki version number */
+$wgVersion = '1.7.1';
+
+/** Name of the site. It must be changed in LocalSettings.php */
+$wgSitename = 'MediaWiki';
+
+/** Will be same as you set @see $wgSitename */
+$wgMetaNamespace = FALSE;
+
+
+/** URL of the server. It will be automatically built including https mode */
+$wgServer = '';
+
+if( isset( $_SERVER['SERVER_NAME'] ) ) {
+ $wgServerName = $_SERVER['SERVER_NAME'];
+} elseif( isset( $_SERVER['HOSTNAME'] ) ) {
+ $wgServerName = $_SERVER['HOSTNAME'];
+} elseif( isset( $_SERVER['HTTP_HOST'] ) ) {
+ $wgServerName = $_SERVER['HTTP_HOST'];
+} elseif( isset( $_SERVER['SERVER_ADDR'] ) ) {
+ $wgServerName = $_SERVER['SERVER_ADDR'];
+} else {
+ $wgServerName = 'localhost';
+}
+
+# check if server use https:
+$wgProto = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
+
+$wgServer = $wgProto.'://' . $wgServerName;
+# If the port is a non-standard one, add it to the URL
+if( isset( $_SERVER['SERVER_PORT'] )
+ && !strpos( $wgServerName, ':' )
+ && ( ( $wgProto == 'http' && $_SERVER['SERVER_PORT'] != 80 )
+ || ( $wgProto == 'https' && $_SERVER['SERVER_PORT'] != 443 ) ) ) {
+
+ $wgServer .= ":" . $_SERVER['SERVER_PORT'];
+}
+
+
+/**
+ * The path we should point to.
+ * It might be a virtual path in case with use apache mod_rewrite for example
+ */
+$wgScriptPath = '/wiki';
+
+/**
+ * Whether to support URLs like index.php/Page_title
+ * @global bool $wgUsePathInfo
+ */
+$wgUsePathInfo = ( strpos( php_sapi_name(), 'cgi' ) === false );
+
+
+/**#@+
+ * Script users will request to get articles
+ * ATTN: Old installations used wiki.phtml and redirect.phtml -
+ * make sure that LocalSettings.php is correctly set!
+ * @deprecated
+ */
+/**
+ * @global string $wgScript
+ */
+$wgScript = "{$wgScriptPath}/index.php";
+/**
+ * @global string $wgRedirectScript
+ */
+$wgRedirectScript = "{$wgScriptPath}/redirect.php";
+/**#@-*/
+
+
+/**#@+
+ * @global string
+ */
+/**
+ * style path as seen by users
+ * @global string $wgStylePath
+ */
+$wgStylePath = "{$wgScriptPath}/skins";
+/**
+ * filesystem stylesheets directory
+ * @global string $wgStyleDirectory
+ */
+$wgStyleDirectory = "{$IP}/skins";
+$wgStyleSheetPath = &$wgStylePath;
+$wgArticlePath = "{$wgScript}?title=$1";
+$wgUploadPath = "{$wgScriptPath}/upload";
+$wgUploadDirectory = "{$IP}/upload";
+$wgHashedUploadDirectory = true;
+$wgLogo = "{$wgUploadPath}/wiki.png";
+$wgFavicon = '/favicon.ico';
+$wgMathPath = "{$wgUploadPath}/math";
+$wgMathDirectory = "{$wgUploadDirectory}/math";
+$wgTmpDirectory = "{$wgUploadDirectory}/tmp";
+$wgUploadBaseUrl = "";
+/**#@-*/
+
+
+/**
+ * By default deleted files are simply discarded; to save them and
+ * make it possible to undelete images, create a directory which
+ * is writable to the web server but is not exposed to the internet.
+ *
+ * Set $wgSaveDeletedFiles to true and set up the save path in
+ * $wgFileStore['deleted']['directory'].
+ */
+$wgSaveDeletedFiles = false;
+
+/**
+ * New file storage paths; currently used only for deleted files.
+ * Set it like this:
+ *
+ * $wgFileStore['deleted']['directory'] = '/var/wiki/private/deleted';
+ *
+ */
+$wgFileStore = array();
+$wgFileStore['deleted']['directory'] = null; // Don't forget to set this.
+$wgFileStore['deleted']['url'] = null; // Private
+$wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
+
+/**
+ * Allowed title characters -- regex character class
+ * Don't change this unless you know what you're doing
+ *
+ * Problematic punctuation:
+ * []{}|# Are needed for link syntax, never enable these
+ * % Enabled by default, minor problems with path to query rewrite rules, see below
+ * + Doesn't work with path to query rewrite rules, corrupted by apache
+ * ? Enabled by default, but doesn't work with path to PATH_INFO rewrites
+ *
+ * All three of these punctuation problems can be avoided by using an alias, instead of a
+ * rewrite rule of either variety.
+ *
+ * The problem with % is that when using a path to query rewrite rule, URLs are
+ * double-unescaped: once by Apache's path conversion code, and again by PHP. So
+ * %253F, for example, becomes "?". Our code does not double-escape to compensate
+ * for this, indeed double escaping would break if the double-escaped title was
+ * passed in the query string rather than the path. This is a minor security issue
+ * because articles can be created such that they are hard to view or edit.
+ *
+ * Theoretically 0x80-0x9F of ISO 8859-1 should be disallowed, but
+ * this breaks interlanguage links
+ */
+$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF";
+
+
+/**
+ * The external URL protocols
+ */
+$wgUrlProtocols = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'irc://',
+ 'gopher://',
+ 'telnet://', // Well if we're going to support the above.. -ævar
+ 'nntp://', // @bug 3808 RFC 1738
+ 'worldwind://',
+ 'mailto:',
+ 'news:'
+);
+
+/** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array.
+ * Set this to NULL to disable virus scanning. If not null, every file uploaded will be scanned for viruses.
+ * @global string $wgAntivirus
+ */
+$wgAntivirus= NULL;
+
+/** Configuration for different virus scanners. This an associative array of associative arrays:
+ * it contains on setup array per known scanner type. The entry is selected by $wgAntivirus, i.e.
+ * valid values for $wgAntivirus are the keys defined in this array.
+ *
+ * The configuration array for each scanner contains the following keys: "command", "codemap", "messagepattern";
+ *
+ * "command" is the full command to call the virus scanner - %f will be replaced with the name of the
+ * file to scan. If not present, the filename will be appended to the command. Note that this must be
+ * overwritten if the scanner is not in the system path; in that case, plase set
+ * $wgAntivirusSetup[$wgAntivirus]['command'] to the desired command with full path.
+ *
+ * "codemap" is a mapping of exit code to return codes of the detectVirus function in SpecialUpload.
+ * An exit code mapped to AV_SCAN_FAILED causes the function to consider the scan to be failed. This will pass
+ * the file if $wgAntivirusRequired is not set.
+ * An exit code mapped to AV_SCAN_ABORTED causes the function to consider the file to have an usupported format,
+ * which is probably imune to virusses. This causes the file to pass.
+ * An exit code mapped to AV_NO_VIRUS will cause the file to pass, meaning no virus was found.
+ * All other codes (like AV_VIRUS_FOUND) will cause the function to report a virus.
+ * You may use "*" as a key in the array to catch all exit codes not mapped otherwise.
+ *
+ * "messagepattern" is a perl regular expression to extract the meaningful part of the scanners
+ * output. The relevant part should be matched as group one (\1).
+ * If not defined or the pattern does not match, the full message is shown to the user.
+ *
+ * @global array $wgAntivirusSetup
+ */
+$wgAntivirusSetup= array(
+
+ #setup for clamav
+ 'clamav' => 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 <apache@' . $wgServerName . '>';
+
+/**
+ * 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 :
+ * <code>
+ * "host" => 'SMTP domain',
+ * "IDHost" => 'domain for MessageID',
+ * "port" => "25",
+ * "auth" => true/false,
+ * "username" => user,
+ * "password" => password
+ * </code>
+ *
+ * @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 <img> 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 <html>...</html> 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.
+ *
+ * <code>
+ * $wgExtensionCredits[$type][] = array(
+ * 'name' => 'Example extension',
+ * 'version' => 1.9,
+ * 'author' => 'Foo Barstein',
+ * 'url' => 'http://wwww.example.com/Example%20Extension/',
+ * );
+ * </code>
+ *
+ * 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 <thorn>, ð to <eth>, Þ to <THORN> and Ð to <ETH>
+ *
+ * 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, <title> 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 = '&nbsp;&nbsp;&nbsp;<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 = '&nbsp;';
+ } 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', '&#160;'); // 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">&nbsp;</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" => "&#x0" ) );
+
+ $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( "&#x0" => "&#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>
+ </head>
+ <body>
+ <h1><img src='$wgLogo' style='float:left;margin-right:1em' alt=''>$title</h1>
+ ";
+ }
+
+ function htmlFooter() {
+ echo "</body></html>";
+ }
+}
+
+/**
+ * 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 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage Metadata
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @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 @@
+<?php
+# Copyright (C) 2003, 2005, 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
+ * @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 = "<contributors>";
+ $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 .= "<contributor>" .
+ "<username>" .
+ htmlentities( $row->rev_user_text ) .
+ "</username>" .
+ "<id>" .
+ $row->rev_user .
+ "</id>" .
+ "</contributor>";
+ }
+ wfProfileOut( $fname );
+ $this->author_list .= "</contributors>";
+ }
+
+ 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 <mediawiki> 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 " <siteinfo>\n " .
+ implode( "\n ", $info ) .
+ "\n </siteinfo>\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 = " <namespaces>\n";
+ foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) {
+ $spaces .= ' ' . wfElement( 'namespace', array( 'key' => $ns ), $title ) . "\n";
+ }
+ $spaces .= " </namespaces>";
+ return $spaces;
+ }
+
+ /**
+ * Closes the output stream with the closing root element.
+ * Call when finished dumping things.
+ */
+ function closeStream() {
+ return "</mediawiki>\n";
+ }
+
+
+ /**
+ * Opens a <page> section on the output stream, with data
+ * from the given database row.
+ *
+ * @param object $row
+ * @return string
+ * @access private
+ */
+ function openPage( $row ) {
+ $out = " <page>\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 <page> section on the output stream.
+ *
+ * @access private
+ */
+ function closePage() {
+ return " </page>\n";
+ }
+
+ /**
+ * Dumps a <revision> 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 = " <revision>\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 .= " <contributor>\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 .= " </contributor>\n";
+ }
+
+ if( $row->rev_minor_edit ) {
+ $out .= " <minor/>\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 .= " </revision>\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 ) {