summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
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 ) {
+ $this->sink->writeClosePage( $string );
+ $this->sendingThisPage = false;
+ }
+ }
+
+ function writeRevision( $rev, $string ) {
+ if( $this->sendingThisPage ) {
+ $this->sink->writeRevision( $rev, $string );
+ }
+ }
+
+ /**
+ * Override for page-based filter types.
+ * @return bool
+ */
+ function pass( $page, $string ) {
+ return true;
+ }
+}
+
+/**
+ * Simple dump output filter to exclude all talk pages.
+ */
+class DumpNotalkFilter extends DumpFilter {
+ function pass( $page ) {
+ return !Namespace::isTalk( $page->page_namespace );
+ }
+}
+
+/**
+ * Dump output filter to include or exclude pages in a given set of namespaces.
+ */
+class DumpNamespaceFilter extends DumpFilter {
+ var $invert = false;
+ var $namespaces = array();
+
+ function DumpNamespaceFilter( &$sink, $param ) {
+ parent::DumpFilter( $sink );
+
+ $constants = array(
+ "NS_MAIN" => NS_MAIN,
+ "NS_TALK" => NS_TALK,
+ "NS_USER" => NS_USER,
+ "NS_USER_TALK" => NS_USER_TALK,
+ "NS_PROJECT" => NS_PROJECT,
+ "NS_PROJECT_TALK" => NS_PROJECT_TALK,
+ "NS_IMAGE" => NS_IMAGE,
+ "NS_IMAGE_TALK" => NS_IMAGE_TALK,
+ "NS_MEDIAWIKI" => NS_MEDIAWIKI,
+ "NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK,
+ "NS_TEMPLATE" => NS_TEMPLATE,
+ "NS_TEMPLATE_TALK" => NS_TEMPLATE_TALK,
+ "NS_HELP" => NS_HELP,
+ "NS_HELP_TALK" => NS_HELP_TALK,
+ "NS_CATEGORY" => NS_CATEGORY,
+ "NS_CATEGORY_TALK" => NS_CATEGORY_TALK );
+
+ if( $param{0} == '!' ) {
+ $this->invert = true;
+ $param = substr( $param, 1 );
+ }
+
+ foreach( explode( ',', $param ) as $key ) {
+ $key = trim( $key );
+ if( isset( $constants[$key] ) ) {
+ $ns = $constants[$key];
+ $this->namespaces[$ns] = true;
+ } elseif( is_numeric( $key ) ) {
+ $ns = intval( $key );
+ $this->namespaces[$ns] = true;
+ } else {
+ throw new MWException( "Unrecognized namespace key '$key'\n" );
+ }
+ }
+ }
+
+ function pass( $page ) {
+ $match = isset( $this->namespaces[$page->page_namespace] );
+ return $this->invert xor $match;
+ }
+}
+
+
+/**
+ * Dump output filter to include only the last revision in each page sequence.
+ */
+class DumpLatestFilter extends DumpFilter {
+ var $page, $pageString, $rev, $revString;
+
+ function writeOpenPage( $page, $string ) {
+ $this->page = $page;
+ $this->pageString = $string;
+ }
+
+ function writeClosePage( $string ) {
+ if( $this->rev ) {
+ $this->sink->writeOpenPage( $this->page, $this->pageString );
+ $this->sink->writeRevision( $this->rev, $this->revString );
+ $this->sink->writeClosePage( $string );
+ }
+ $this->rev = null;
+ $this->revString = null;
+ $this->page = null;
+ $this->pageString = null;
+ }
+
+ function writeRevision( $rev, $string ) {
+ if( $rev->rev_id == $this->page->page_latest ) {
+ $this->rev = $rev;
+ $this->revString = $string;
+ }
+ }
+}
+
+/**
+ * Base class for output stream; prints to stdout or buffer or whereever.
+ */
+class DumpMultiWriter {
+ function DumpMultiWriter( $sinks ) {
+ $this->sinks = $sinks;
+ $this->count = count( $sinks );
+ }
+
+ function writeOpenStream( $string ) {
+ for( $i = 0; $i < $this->count; $i++ ) {
+ $this->sinks[$i]->writeOpenStream( $string );
+ }
+ }
+
+ function writeCloseStream( $string ) {
+ for( $i = 0; $i < $this->count; $i++ ) {
+ $this->sinks[$i]->writeCloseStream( $string );
+ }
+ }
+
+ function writeOpenPage( $page, $string ) {
+ for( $i = 0; $i < $this->count; $i++ ) {
+ $this->sinks[$i]->writeOpenPage( $page, $string );
+ }
+ }
+
+ function writeClosePage( $string ) {
+ for( $i = 0; $i < $this->count; $i++ ) {
+ $this->sinks[$i]->writeClosePage( $string );
+ }
+ }
+
+ function writeRevision( $rev, $string ) {
+ for( $i = 0; $i < $this->count; $i++ ) {
+ $this->sinks[$i]->writeRevision( $rev, $string );
+ }
+ }
+}
+
+function xmlsafe( $string ) {
+ $fname = 'xmlsafe';
+ wfProfileIn( $fname );
+
+ /**
+ * The page may contain old data which has not been properly normalized.
+ * Invalid UTF-8 sequences or forbidden control characters will make our
+ * XML output invalid, so be sure to strip them out.
+ */
+ $string = UtfNormal::cleanUp( $string );
+
+ $string = htmlspecialchars( $string );
+ wfProfileOut( $fname );
+ return $string;
+}
+
+?>
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
new file mode 100644
index 00000000..21f632ec
--- /dev/null
+++ b/includes/ExternalEdit.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * License: Public domain
+ *
+ * @author Erik Moeller <moeller@scireview.de>
+ * @package MediaWiki
+ */
+
+/**
+ *
+ * @package MediaWiki
+ *
+ * Support for external editors to modify both text and files
+ * in external applications. It works as follows: MediaWiki
+ * sends a meta-file with the MIME type 'application/x-external-editor'
+ * to the client. The user has to associate that MIME type with
+ * a helper application (a reference implementation in Perl
+ * can be found in extensions/ee), which will launch the editor,
+ * and save the modified data back to the server.
+ *
+ */
+
+class ExternalEdit {
+
+ function ExternalEdit ( $article, $mode ) {
+ global $wgInputEncoding;
+ $this->mArticle =& $article;
+ $this->mTitle =& $article->mTitle;
+ $this->mCharset = $wgInputEncoding;
+ $this->mMode = $mode;
+ }
+
+ function edit() {
+ global $wgOut, $wgScript, $wgScriptPath, $wgServer, $wgLang;
+ $wgOut->disable();
+ $name=$this->mTitle->getText();
+ $pos=strrpos($name,".")+1;
+ header ( "Content-type: application/x-external-editor; charset=".$this->mCharset );
+
+ # $type can be "Edit text", "Edit file" or "Diff text" at the moment
+ # See the protocol specifications at [[m:Help:External editors/Tech]] for
+ # details.
+ if(!isset($this->mMode)) {
+ $type="Edit text";
+ $url=$this->mTitle->getFullURL("action=edit&internaledit=true");
+ # *.wiki file extension is used by some editors for syntax
+ # highlighting, so we follow that convention
+ $extension="wiki";
+ } elseif($this->mMode=="file") {
+ $type="Edit file";
+ $image = Image::newFromTitle( $this->mTitle );
+ $img_url = $image->getURL();
+ if(strpos($img_url,"://")) {
+ $url = $img_url;
+ } else {
+ $url = $wgServer . $img_url;
+ }
+ $extension=substr($name, $pos);
+ }
+ $special=$wgLang->getNsText(NS_SPECIAL);
+ $control = <<<CONTROL
+[Process]
+Type=$type
+Engine=MediaWiki
+Script={$wgServer}{$wgScript}
+Server={$wgServer}
+Path={$wgScriptPath}
+Special namespace=$special
+
+[File]
+Extension=$extension
+URL=$url
+CONTROL;
+ echo $control;
+ }
+}
+?>
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
new file mode 100644
index 00000000..79f1a528
--- /dev/null
+++ b/includes/ExternalStore.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ *
+ * Constructor class for data kept in external repositories
+ *
+ * External repositories might be populated by maintenance/async
+ * scripts, thus partial moving of data may be possible, as well
+ * as possibility to have any storage format (i.e. for archives)
+ *
+ */
+
+class ExternalStore {
+ /* Fetch data from given URL */
+ function fetchFromURL($url) {
+ global $wgExternalStores;
+
+ if (!$wgExternalStores)
+ return false;
+
+ @list($proto,$path)=explode('://',$url,2);
+ /* Bad URL */
+ if ($path=="")
+ return false;
+
+ $store =& ExternalStore::getStoreObject( $proto );
+ if ( $store === false )
+ return false;
+ return $store->fetchFromURL($url);
+ }
+
+ /**
+ * Get an external store object of the given type
+ */
+ function &getStoreObject( $proto ) {
+ global $wgExternalStores;
+ if (!$wgExternalStores)
+ return false;
+ /* Protocol not enabled */
+ if (!in_array( $proto, $wgExternalStores ))
+ return false;
+
+ $class='ExternalStore'.ucfirst($proto);
+ /* Preloaded modules might exist, especially ones serving multiple protocols */
+ if (!class_exists($class)) {
+ if (!include_once($class.'.php'))
+ return false;
+ }
+ $store=new $class();
+ return $store;
+ }
+
+ /**
+ * Store a data item to an external store, identified by a partial URL
+ * The protocol part is used to identify the class, the rest is passed to the
+ * class itself as a parameter.
+ * Returns the URL of the stored data item, or false on error
+ */
+ function insert( $url, $data ) {
+ list( $proto, $params ) = explode( '://', $url, 2 );
+ $store =& ExternalStore::getStoreObject( $proto );
+ if ( $store === false ) {
+ return false;
+ } else {
+ return $store->store( $params, $data );
+ }
+ }
+}
+?>
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
new file mode 100644
index 00000000..f610df80
--- /dev/null
+++ b/includes/ExternalStoreDB.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ *
+ * DB accessable external objects
+ *
+ */
+require_once( 'LoadBalancer.php' );
+
+
+/** @package MediaWiki */
+
+/**
+ * External database storage will use one (or more) separate connection pools
+ * from what the main wiki uses. If we load many revisions, such as when doing
+ * bulk backups or maintenance, we want to keep them around over the lifetime
+ * of the script.
+ *
+ * Associative array of LoadBalancer objects, indexed by cluster name.
+ */
+global $wgExternalLoadBalancers;
+$wgExternalLoadBalancers = array();
+
+/**
+ * One-step cache variable to hold base blobs; operations that
+ * pull multiple revisions may often pull multiple times from
+ * the same blob. By keeping the last-used one open, we avoid
+ * redundant unserialization and decompression overhead.
+ */
+global $wgExternalBlobCache;
+$wgExternalBlobCache = array();
+
+class ExternalStoreDB {
+
+ /** @todo Document.*/
+ function &getLoadBalancer( $cluster ) {
+ global $wgExternalServers, $wgExternalLoadBalancers;
+ if ( !array_key_exists( $cluster, $wgExternalLoadBalancers ) ) {
+ $wgExternalLoadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] );
+ }
+ $wgExternalLoadBalancers[$cluster]->allowLagged(true);
+ return $wgExternalLoadBalancers[$cluster];
+ }
+
+ /** @todo Document.*/
+ function &getSlave( $cluster ) {
+ $lb =& $this->getLoadBalancer( $cluster );
+ return $lb->getConnection( DB_SLAVE );
+ }
+
+ /** @todo Document.*/
+ function &getMaster( $cluster ) {
+ $lb =& $this->getLoadBalancer( $cluster );
+ return $lb->getConnection( DB_MASTER );
+ }
+
+ /** @todo Document.*/
+ function getTable( &$db ) {
+ $table = $db->getLBInfo( 'blobs table' );
+ if ( is_null( $table ) ) {
+ $table = 'blobs';
+ }
+ return $table;
+ }
+
+ /**
+ * Fetch data from given URL
+ * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
+ */
+ function fetchFromURL($url) {
+ $path = explode( '/', $url );
+ $cluster = $path[2];
+ $id = $path[3];
+ if ( isset( $path[4] ) ) {
+ $itemID = $path[4];
+ } else {
+ $itemID = false;
+ }
+
+ $ret =& $this->fetchBlob( $cluster, $id, $itemID );
+
+ if ( $itemID !== false && $ret !== false ) {
+ return $ret->getItem( $itemID );
+ }
+ return $ret;
+ }
+
+ /**
+ * Fetch a blob item out of the database; a cache of the last-loaded
+ * blob will be kept so that multiple loads out of a multi-item blob
+ * can avoid redundant database access and decompression.
+ * @param $cluster
+ * @param $id
+ * @param $itemID
+ * @return mixed
+ * @private
+ */
+ function &fetchBlob( $cluster, $id, $itemID ) {
+ global $wgExternalBlobCache;
+ $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/";
+ if( isset( $wgExternalBlobCache[$cacheID] ) ) {
+ wfDebug( "ExternalStoreDB::fetchBlob cache hit on $cacheID\n" );
+ return $wgExternalBlobCache[$cacheID];
+ }
+
+ wfDebug( "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" );
+
+ $dbr =& $this->getSlave( $cluster );
+ $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ) );
+ if ( $ret === false ) {
+ wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" );
+ // Try the master
+ $dbw =& $this->getMaster( $cluster );
+ $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ) );
+ if( $ret === false) {
+ wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" );
+ }
+ }
+ if( $itemID !== false && $ret !== false ) {
+ // Unserialise object; caller extracts item
+ $ret = unserialize( $ret );
+ }
+
+ $wgExternalBlobCache = array( $cacheID => &$ret );
+ return $ret;
+ }
+
+ /**
+ * Insert a data item into a given cluster
+ *
+ * @param $cluster String: the cluster name
+ * @param $data String: the data item
+ * @return string URL
+ */
+ function store( $cluster, $data ) {
+ $fname = 'ExternalStoreDB::store';
+
+ $dbw =& $this->getMaster( $cluster );
+
+ $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' );
+ $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname );
+ $id = $dbw->insertId();
+ if ( $dbw->getFlag( DBO_TRX ) ) {
+ $dbw->immediateCommit();
+ }
+ return "DB://$cluster/$id";
+ }
+}
+?>
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
new file mode 100644
index 00000000..daf62cc4
--- /dev/null
+++ b/includes/ExternalStoreHttp.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ *
+ * Example class for HTTP accessable external objects
+ *
+ */
+
+class ExternalStoreHttp {
+ /* Fetch data from given URL */
+ function fetchFromURL($url) {
+ ini_set( "allow_url_fopen", true );
+ $ret = file_get_contents( $url );
+ ini_set( "allow_url_fopen", false );
+ return $ret;
+ }
+
+ /* XXX: may require other methods, for store, delete,
+ * whatever, for initial ext storage
+ */
+}
+?>
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
new file mode 100644
index 00000000..ae05385a
--- /dev/null
+++ b/includes/FakeTitle.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * Fake title class that triggers an error if any members are called
+ */
+class FakeTitle {
+ function error() { throw new MWException( "Attempt to call member function of FakeTitle\n" ); }
+
+ // PHP 5.1 method overload
+ function __call( $name, $args ) { $this->error(); }
+
+ // PHP <5.1 compatibility
+ function getInterwikiLink() { $this->error(); }
+ function getInterwikiCached() { $this->error(); }
+ function isLocal() { $this->error(); }
+ function isTrans() { $this->error(); }
+ function touchArray( $titles, $timestamp = '' ) { $this->error(); }
+ function getText() { $this->error(); }
+ function getPartialURL() { $this->error(); }
+ function getDBkey() { $this->error(); }
+ function getNamespace() { $this->error(); }
+ function getNsText() { $this->error(); }
+ function getSubjectNsText() { $this->error(); }
+ function getInterwiki() { $this->error(); }
+ function getFragment() { $this->error(); }
+ function getDefaultNamespace() { $this->error(); }
+ function getIndexTitle() { $this->error(); }
+ function getPrefixedDBkey() { $this->error(); }
+ function getPrefixedText() { $this->error(); }
+ function getFullText() { $this->error(); }
+ function getPrefixedURL() { $this->error(); }
+ function getFullURL() {$this->error(); }
+ function getLocalURL() { $this->error(); }
+ function escapeLocalURL() { $this->error(); }
+ function escapeFullURL() { $this->error(); }
+ function getInternalURL() { $this->error(); }
+ function getEditURL() { $this->error(); }
+ function getEscapedText() { $this->error(); }
+ function isExternal() { $this->error(); }
+ function isSemiProtected() { $this->error(); }
+ function isProtected() { $this->error(); }
+ function userIsWatching() { $this->error(); }
+ function userCan() { $this->error(); }
+ function userCanEdit() { $this->error(); }
+ function userCanMove() { $this->error(); }
+ function isMovable() { $this->error(); }
+ function userCanRead() { $this->error(); }
+ function isTalkPage() { $this->error(); }
+ function isCssJsSubpage() { $this->error(); }
+ function isValidCssJsSubpage() { $this->error(); }
+ function getSkinFromCssJsSubpage() { $this->error(); }
+ function isCssSubpage() { $this->error(); }
+ function isJsSubpage() { $this->error(); }
+ function userCanEditCssJsSubpage() { $this->error(); }
+ function loadRestrictions( $res ) { $this->error(); }
+ function getRestrictions($action) { $this->error(); }
+ function isDeleted() { $this->error(); }
+ function getArticleID( $flags = 0 ) { $this->error(); }
+ function getLatestRevID() { $this->error(); }
+ function resetArticleID( $newid ) { $this->error(); }
+ function invalidateCache() { $this->error(); }
+ function getTalkPage() { $this->error(); }
+ function getSubjectPage() { $this->error(); }
+ function getLinksTo() { $this->error(); }
+ function getTemplateLinksTo() { $this->error(); }
+ function getBrokenLinksFrom() { $this->error(); }
+ function getSquidURLs() { $this->error(); }
+ function moveNoAuth() { $this->error(); }
+ function isValidMoveOperation() { $this->error(); }
+ function moveTo() { $this->error(); }
+ function moveOverExistingRedirect() { $this->error(); }
+ function moveToNewTitle() { $this->error(); }
+ function isValidMoveTarget() { $this->error(); }
+ function createRedirect() { $this->error(); }
+ function getParentCategories() { $this->error(); }
+ function getParentCategoryTree() { $this->error(); }
+ function pageCond() { $this->error(); }
+ function getPreviousRevisionID() { $this->error(); }
+ function getNextRevisionID() { $this->error(); }
+ function equals() { $this->error(); }
+ function exists() { $this->error(); }
+ function isAlwaysKnown() { $this->error(); }
+ function touchLinks() { $this->error(); }
+ function trackbackURL() { $this->error(); }
+ function trackbackRDF() { $this->error(); }
+}
+
+?>
diff --git a/includes/Feed.php b/includes/Feed.php
new file mode 100644
index 00000000..7663e820
--- /dev/null
+++ b/includes/Feed.php
@@ -0,0 +1,310 @@
+<?php
+# Basic support for outputting syndication feeds in RSS, other formats
+#
+# 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
+
+/**
+ * Contain a feed class as well as classes to build rss / atom ... feeds
+ * Available feeds are defined in Defines.php
+ * @package MediaWiki
+ */
+
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class FeedItem {
+ /**#@+
+ * @var string
+ * @private
+ */
+ var $Title = 'Wiki';
+ var $Description = '';
+ var $Url = '';
+ var $Date = '';
+ var $Author = '';
+ /**#@-*/
+
+ /**#@+
+ * @todo document
+ */
+ function FeedItem( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) {
+ $this->Title = $Title;
+ $this->Description = $Description;
+ $this->Url = $Url;
+ $this->Date = $Date;
+ $this->Author = $Author;
+ $this->Comments = $Comments;
+ }
+
+ /**
+ * @static
+ */
+ function xmlEncode( $string ) {
+ $string = str_replace( "\r\n", "\n", $string );
+ $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string );
+ return htmlspecialchars( $string );
+ }
+
+ function getTitle() { return $this->xmlEncode( $this->Title ); }
+ function getUrl() { return $this->xmlEncode( $this->Url ); }
+ function getDescription() { return $this->xmlEncode( $this->Description ); }
+ function getLanguage() {
+ global $wgContLanguageCode;
+ return $wgContLanguageCode;
+ }
+ function getDate() { return $this->Date; }
+ function getAuthor() { return $this->xmlEncode( $this->Author ); }
+ function getComments() { return $this->xmlEncode( $this->Comments ); }
+ /**#@-*/
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class ChannelFeed extends FeedItem {
+ /**#@+
+ * Abstract function, override!
+ * @abstract
+ */
+
+ /**
+ * Generate Header of the feed
+ */
+ function outHeader() {
+ # print "<feed>";
+ }
+
+ /**
+ * Generate an item
+ * @param $item
+ */
+ function outItem( $item ) {
+ # print "<item>...</item>";
+ }
+
+ /**
+ * Generate Footer of the feed
+ */
+ function outFooter() {
+ # print "</feed>";
+ }
+ /**#@-*/
+
+ /**
+ * Setup and send HTTP headers. Don't send any content;
+ * content might end up being cached and re-sent with
+ * these same headers later.
+ *
+ * This should be called from the outHeader() method,
+ * but can also be called separately.
+ *
+ * @public
+ */
+ function httpHeaders() {
+ global $wgOut;
+
+ # We take over from $wgOut, excepting its cache header info
+ $wgOut->disable();
+ $mimetype = $this->contentType();
+ header( "Content-type: $mimetype; charset=UTF-8" );
+ $wgOut->sendCacheControl();
+
+ }
+
+ /**
+ * Return an internet media type to be sent in the headers.
+ *
+ * @return string
+ * @private
+ */
+ function contentType() {
+ global $wgRequest;
+ $ctype = $wgRequest->getVal('ctype','application/xml');
+ $allowedctypes = array('application/xml','text/xml','application/rss+xml','application/atom+xml');
+ return (in_array($ctype, $allowedctypes) ? $ctype : 'application/xml');
+ }
+
+ /**
+ * Output the initial XML headers with a stylesheet for legibility
+ * if someone finds it in a browser.
+ * @private
+ */
+ function outXmlHeader() {
+ global $wgServer, $wgStylePath;
+
+ $this->httpHeaders();
+ echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
+ echo '<?xml-stylesheet type="text/css" href="' .
+ htmlspecialchars( "$wgServer$wgStylePath/common/feed.css" ) . '"?' . ">\n";
+ }
+}
+
+/**
+ * Generate a RSS feed
+ * @todo document
+ * @package MediaWiki
+ */
+class RSSFeed extends ChannelFeed {
+
+ /**
+ * Format a date given a timestamp
+ * @param integer $ts Timestamp
+ * @return string Date string
+ */
+ function formatTime( $ts ) {
+ return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+ }
+
+ /**
+ * Ouput an RSS 2.0 header
+ */
+ function outHeader() {
+ global $wgVersion;
+
+ $this->outXmlHeader();
+ ?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <channel>
+ <title><?php print $this->getTitle() ?></title>
+ <link><?php print $this->getUrl() ?></link>
+ <description><?php print $this->getDescription() ?></description>
+ <language><?php print $this->getLanguage() ?></language>
+ <generator>MediaWiki <?php print $wgVersion ?></generator>
+ <lastBuildDate><?php print $this->formatTime( wfTimestampNow() ) ?></lastBuildDate>
+<?php
+ }
+
+ /**
+ * Output an RSS 2.0 item
+ * @param FeedItem item to be output
+ */
+ function outItem( $item ) {
+ ?>
+ <item>
+ <title><?php print $item->getTitle() ?></title>
+ <link><?php print $item->getUrl() ?></link>
+ <description><?php print $item->getDescription() ?></description>
+ <?php if( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ) ?></pubDate><?php } ?>
+ <?php if( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor() ?></dc:creator><?php }?>
+ <?php if( $item->getComments() ) { ?><comments><?php print $item->getComments() ?></comments><?php }?>
+ </item>
+<?php
+ }
+
+ /**
+ * Ouput an RSS 2.0 footer
+ */
+ function outFooter() {
+ ?>
+ </channel>
+</rss><?php
+ }
+}
+
+/**
+ * Generate an Atom feed
+ * @todo document
+ * @package MediaWiki
+ */
+class AtomFeed extends ChannelFeed {
+ /**
+ * @todo document
+ */
+ function formatTime( $ts ) {
+ // need to use RFC 822 time format at least for rss2.0
+ return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $ts ) );
+ }
+
+ /**
+ * Outputs a basic header for Atom 1.0 feeds.
+ */
+ function outHeader() {
+ global $wgVersion;
+
+ $this->outXmlHeader();
+ ?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="<?php print $this->getLanguage() ?>">
+ <id><?php print $this->getFeedId() ?></id>
+ <title><?php print $this->getTitle() ?></title>
+ <link rel="self" type="application/atom+xml" href="<?php print $this->getSelfUrl() ?>"/>
+ <link rel="alternate" type="text/html" href="<?php print $this->getUrl() ?>"/>
+ <updated><?php print $this->formatTime( wfTimestampNow() ) ?>Z</updated>
+ <subtitle><?php print $this->getDescription() ?></subtitle>
+ <generator>MediaWiki <?php print $wgVersion ?></generator>
+
+<?php
+ }
+
+ /**
+ * Atom 1.0 requires a unique, opaque IRI as a unique indentifier
+ * for every feed we create. For now just use the URL, but who
+ * can tell if that's right? If we put options on the feed, do we
+ * have to change the id? Maybe? Maybe not.
+ *
+ * @return string
+ * @private
+ */
+ function getFeedId() {
+ return $this->getSelfUrl();
+ }
+
+ /**
+ * Atom 1.0 requests a self-reference to the feed.
+ * @return string
+ * @private
+ */
+ function getSelfUrl() {
+ global $wgRequest;
+ return htmlspecialchars( $wgRequest->getFullRequestURL() );
+ }
+
+ /**
+ * Output a given item.
+ * @param $item
+ */
+ function outItem( $item ) {
+ global $wgMimeType;
+ ?>
+ <entry>
+ <id><?php print $item->getUrl() ?></id>
+ <title><?php print $item->getTitle() ?></title>
+ <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print $item->getUrl() ?>"/>
+ <?php if( $item->getDate() ) { ?>
+ <updated><?php print $this->formatTime( $item->getDate() ) ?>Z</updated>
+ <?php } ?>
+
+ <summary type="html"><?php print $item->getDescription() ?></summary>
+ <?php if( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor() ?></name></author><?php }?>
+ </entry>
+
+<?php /* FIXME need to add comments
+ <?php if( $item->getComments() ) { ?><dc:comment><?php print $item->getComments() ?></dc:comment><?php }?>
+ */
+ }
+
+ /**
+ * Outputs the footer for Atom 1.0 feed (basicly '\</feed\>').
+ */
+ function outFooter() {?>
+ </feed><?php
+ }
+}
+
+?>
diff --git a/includes/FileStore.php b/includes/FileStore.php
new file mode 100644
index 00000000..85aaedfe
--- /dev/null
+++ b/includes/FileStore.php
@@ -0,0 +1,377 @@
+<?php
+
+class FileStore {
+ const DELETE_ORIGINAL = 1;
+
+ /**
+ * Fetch the FileStore object for a given storage group
+ */
+ static function get( $group ) {
+ global $wgFileStore;
+
+ if( isset( $wgFileStore[$group] ) ) {
+ $info = $wgFileStore[$group];
+ return new FileStore( $group,
+ $info['directory'],
+ $info['url'],
+ intval( $info['hash'] ) );
+ } else {
+ return null;
+ }
+ }
+
+ private function __construct( $group, $directory, $path, $hash ) {
+ $this->mGroup = $group;
+ $this->mDirectory = $directory;
+ $this->mPath = $path;
+ $this->mHashLevel = $hash;
+ }
+
+ /**
+ * Acquire a lock; use when performing write operations on a store.
+ * This is attached to your master database connection, so if you
+ * suffer an uncaught error the lock will be released when the
+ * connection is closed.
+ *
+ * @fixme Probably only works on MySQL. Abstract to the Database class?
+ */
+ static function lock() {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $lockname = $dbw->addQuotes( FileStore::lockName() );
+ $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", $fname );
+ $row = $dbw->fetchObject( $result );
+ $dbw->freeResult( $result );
+
+ if( $row->lockstatus == 1 ) {
+ return true;
+ } else {
+ wfDebug( "$fname failed to acquire lock\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Release the global file store lock.
+ */
+ static function unlock() {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $lockname = $dbw->addQuotes( FileStore::lockName() );
+ $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", $fname );
+ $row = $dbw->fetchObject( $result );
+ $dbw->freeResult( $result );
+ }
+
+ private static function lockName() {
+ global $wgDBname, $wgDBprefix;
+ return "MediaWiki.{$wgDBname}.{$wgDBprefix}FileStore";
+ }
+
+ /**
+ * Copy a file into the file store from elsewhere in the filesystem.
+ * Should be protected by FileStore::lock() to avoid race conditions.
+ *
+ * @param $key storage key string
+ * @param $flags
+ * DELETE_ORIGINAL - remove the source file on transaction commit.
+ *
+ * @throws FSException if copy can't be completed
+ * @return FSTransaction
+ */
+ function insert( $key, $sourcePath, $flags=0 ) {
+ $destPath = $this->filePath( $key );
+ return $this->copyFile( $sourcePath, $destPath, $flags );
+ }
+
+ /**
+ * Copy a file from the file store to elsewhere in the filesystem.
+ * Should be protected by FileStore::lock() to avoid race conditions.
+ *
+ * @param $key storage key string
+ * @param $flags
+ * DELETE_ORIGINAL - remove the source file on transaction commit.
+ *
+ * @throws FSException if copy can't be completed
+ * @return FSTransaction on success
+ */
+ function export( $key, $destPath, $flags=0 ) {
+ $sourcePath = $this->filePath( $key );
+ return $this->copyFile( $sourcePath, $destPath, $flags );
+ }
+
+ private function copyFile( $sourcePath, $destPath, $flags=0 ) {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+
+ if( !file_exists( $sourcePath ) ) {
+ // Abort! Abort!
+ throw new FSException( "missing source file '$sourcePath'\n" );
+ }
+
+ $transaction = new FSTransaction();
+
+ if( $flags & self::DELETE_ORIGINAL ) {
+ $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath );
+ }
+
+ if( file_exists( $destPath ) ) {
+ // An identical file is already present; no need to copy.
+ } else {
+ if( !file_exists( dirname( $destPath ) ) ) {
+ wfSuppressWarnings();
+ $ok = mkdir( dirname( $destPath ), 0777, true );
+ wfRestoreWarnings();
+
+ if( !$ok ) {
+ throw new FSException(
+ "failed to create directory for '$destPath'\n" );
+ }
+ }
+
+ wfSuppressWarnings();
+ $ok = copy( $sourcePath, $destPath );
+ wfRestoreWarnings();
+
+ if( $ok ) {
+ wfDebug( "$fname copied '$sourcePath' to '$destPath'\n" );
+ $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
+ } else {
+ throw new FSException(
+ "$fname failed to copy '$sourcePath' to '$destPath'\n" );
+ }
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Delete a file from the file store.
+ * Caller's responsibility to make sure it's not being used by another row.
+ *
+ * File is not actually removed until transaction commit.
+ * Should be protected by FileStore::lock() to avoid race conditions.
+ *
+ * @param $key storage key string
+ * @throws FSException if file can't be deleted
+ * @return FSTransaction
+ */
+ function delete( $key ) {
+ $destPath = $this->filePath( $key );
+ if( false === $destPath ) {
+ throw new FSExcepton( "file store does not contain file '$key'" );
+ } else {
+ return FileStore::deleteFile( $destPath );
+ }
+ }
+
+ /**
+ * Delete a non-managed file on a transactional basis.
+ *
+ * File is not actually removed until transaction commit.
+ * Should be protected by FileStore::lock() to avoid race conditions.
+ *
+ * @param $path file to remove
+ * @throws FSException if file can't be deleted
+ * @return FSTransaction
+ *
+ * @fixme Might be worth preliminary permissions check
+ */
+ static function deleteFile( $path ) {
+ if( file_exists( $path ) ) {
+ $transaction = new FSTransaction();
+ $transaction->addCommit( FSTransaction::DELETE_FILE, $path );
+ return $transaction;
+ } else {
+ throw new FSException( "cannot delete missing file '$path'" );
+ }
+ }
+
+ /**
+ * Stream a contained file directly to HTTP output.
+ * Will throw a 404 if file is missing; 400 if invalid key.
+ * @return true on success, false on failure
+ */
+ function stream( $key ) {
+ $path = $this->filePath( $key );
+ if( $path === false ) {
+ wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." );
+ return false;
+ }
+
+ if( file_exists( $path ) ) {
+ // Set the filename for more convenient save behavior from browsers
+ // FIXME: Is this safe?
+ header( 'Content-Disposition: inline; filename="' . $key . '"' );
+
+ require_once 'StreamFile.php';
+ wfStreamFile( $path );
+ } else {
+ return wfHttpError( 404, "Not found",
+ "The requested resource does not exist." );
+ }
+ }
+
+ /**
+ * Confirm that the given file key is valid.
+ * Note that a valid key may refer to a file that does not exist.
+ *
+ * Key should consist of a 32-digit base-36 SHA-1 hash and
+ * an optional alphanumeric extension, all lowercase.
+ * The whole must not exceed 64 characters.
+ *
+ * @param $key
+ * @return boolean
+ */
+ static function validKey( $key ) {
+ return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key );
+ }
+
+
+ /**
+ * Calculate file storage key from a file on disk.
+ * You must pass an extension to it, as some files may be calculated
+ * out of a temporary file etc.
+ *
+ * @param $path to file
+ * @param $extension
+ * @return string or false if could not open file or bad extension
+ */
+ static function calculateKey( $path, $extension ) {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+
+ wfSuppressWarnings();
+ $hash = sha1_file( $path );
+ wfRestoreWarnings();
+ if( $hash === false ) {
+ wfDebug( "$fname: couldn't hash file '$path'\n" );
+ return false;
+ }
+
+ $base36 = wfBaseConvert( $hash, 16, 36, 32 );
+ if( $extension == '' ) {
+ $key = $base36;
+ } else {
+ $key = $base36 . '.' . $extension;
+ }
+
+ // Sanity check
+ if( self::validKey( $key ) ) {
+ return $key;
+ } else {
+ wfDebug( "$fname: generated bad key '$key'\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Return filesystem path to the given file.
+ * Note that the file may or may not exist.
+ * @return string or false if an invalid key
+ */
+ function filePath( $key ) {
+ if( self::validKey( $key ) ) {
+ return $this->mDirectory . DIRECTORY_SEPARATOR .
+ $this->hashPath( $key, DIRECTORY_SEPARATOR );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Return URL path to the given file, if the store is public.
+ * @return string or false if not public
+ */
+ function urlPath( $key ) {
+ if( $this->mUrl && self::validKey( $key ) ) {
+ return $this->mUrl . '/' . $this->hashPath( $key, '/' );
+ } else {
+ return false;
+ }
+ }
+
+ private function hashPath( $key, $separator ) {
+ $parts = array();
+ for( $i = 0; $i < $this->mHashLevel; $i++ ) {
+ $parts[] = $key{$i};
+ }
+ $parts[] = $key;
+ return implode( $separator, $parts );
+ }
+}
+
+/**
+ * Wrapper for file store transaction stuff.
+ *
+ * FileStore methods may return one of these for undoable operations;
+ * you can then call its rollback() or commit() methods to perform
+ * final cleanup if dependent database work fails or succeeds.
+ */
+class FSTransaction {
+ const DELETE_FILE = 1;
+
+ /**
+ * Combine more items into a fancier transaction
+ */
+ function add( FSTransaction $transaction ) {
+ $this->mOnCommit = array_merge(
+ $this->mOnCommit, $transaction->mOnCommit );
+ $this->mOnRollback = array_merge(
+ $this->mOnRollback, $transaction->mOnRollback );
+ }
+
+ /**
+ * Perform final actions for success.
+ * @return true if actions applied ok, false if errors
+ */
+ function commit() {
+ return $this->apply( $this->mOnCommit );
+ }
+
+ /**
+ * Perform final actions for failure.
+ * @return true if actions applied ok, false if errors
+ */
+ function rollback() {
+ return $this->apply( $this->mOnRollback );
+ }
+
+ // --- Private and friend functions below...
+
+ function __construct() {
+ $this->mOnCommit = array();
+ $this->mOnRollback = array();
+ }
+
+ function addCommit( $action, $path ) {
+ $this->mOnCommit[] = array( $action, $path );
+ }
+
+ function addRollback( $action, $path ) {
+ $this->mOnRollback[] = array( $action, $path );
+ }
+
+ private function apply( $actions ) {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+ $result = true;
+ foreach( $actions as $item ) {
+ list( $action, $path ) = $item;
+ if( $action == self::DELETE_FILE ) {
+ wfSuppressWarnings();
+ $ok = unlink( $path );
+ wfRestoreWarnings();
+ if( $ok )
+ wfDebug( "$fname: deleting file '$path'\n" );
+ else
+ wfDebug( "$fname: failed to delete file '$path'\n" );
+ $result = $result && $ok;
+ }
+ }
+ return $result;
+ }
+}
+
+class FSException extends MWException { }
+
+?> \ No newline at end of file
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
new file mode 100644
index 00000000..e2033486
--- /dev/null
+++ b/includes/GlobalFunctions.php
@@ -0,0 +1,2005 @@
+<?php
+
+/**
+ * Global functions used everywhere
+ * @package MediaWiki
+ */
+
+/**
+ * Some globals and requires needed
+ */
+
+/**
+ * Total number of articles
+ * @global integer $wgNumberOfArticles
+ */
+$wgNumberOfArticles = -1; # Unset
+/**
+ * Total number of views
+ * @global integer $wgTotalViews
+ */
+$wgTotalViews = -1;
+/**
+ * Total number of edits
+ * @global integer $wgTotalEdits
+ */
+$wgTotalEdits = -1;
+
+
+require_once( 'DatabaseFunctions.php' );
+require_once( 'LogPage.php' );
+require_once( 'normal/UtfNormalUtil.php' );
+require_once( 'XmlFunctions.php' );
+
+/**
+ * Compatibility functions
+ * PHP <4.3.x is not actively supported; 4.1.x and 4.2.x might or might not work.
+ * <4.1.x will not work, as we use a number of features introduced in 4.1.0
+ * such as the new autoglobals.
+ */
+if( !function_exists('iconv') ) {
+ # iconv support is not in the default configuration and so may not be present.
+ # Assume will only ever use utf-8 and iso-8859-1.
+ # This will *not* work in all circumstances.
+ function iconv( $from, $to, $string ) {
+ if(strcasecmp( $from, $to ) == 0) return $string;
+ if(strcasecmp( $from, 'utf-8' ) == 0) return utf8_decode( $string );
+ if(strcasecmp( $to, 'utf-8' ) == 0) return utf8_encode( $string );
+ return $string;
+ }
+}
+
+if( !function_exists('file_get_contents') ) {
+ # Exists in PHP 4.3.0+
+ function file_get_contents( $filename ) {
+ return implode( '', file( $filename ) );
+ }
+}
+
+if( !function_exists('is_a') ) {
+ # Exists in PHP 4.2.0+
+ function is_a( $object, $class_name ) {
+ return
+ (strcasecmp( get_class( $object ), $class_name ) == 0) ||
+ is_subclass_of( $object, $class_name );
+ }
+}
+
+# UTF-8 substr function based on a PHP manual comment
+if ( !function_exists( 'mb_substr' ) ) {
+ function mb_substr( $str, $start ) {
+ preg_match_all( '/./us', $str, $ar );
+
+ if( func_num_args() >= 3 ) {
+ $end = func_get_arg( 2 );
+ return join( '', array_slice( $ar[0], $start, $end ) );
+ } else {
+ return join( '', array_slice( $ar[0], $start ) );
+ }
+ }
+}
+
+if( !function_exists( 'floatval' ) ) {
+ /**
+ * First defined in PHP 4.2.0
+ * @param mixed $var;
+ * @return float
+ */
+ function floatval( $var ) {
+ return (float)$var;
+ }
+}
+
+if ( !function_exists( 'array_diff_key' ) ) {
+ /**
+ * Exists in PHP 5.1.0+
+ * Not quite compatible, two-argument version only
+ * Null values will cause problems due to this use of isset()
+ */
+ function array_diff_key( $left, $right ) {
+ $result = $left;
+ foreach ( $left as $key => $value ) {
+ if ( isset( $right[$key] ) ) {
+ unset( $result[$key] );
+ }
+ }
+ return $result;
+ }
+}
+
+
+/**
+ * Wrapper for clone() for PHP 4, for the moment.
+ * PHP 5 won't let you declare a 'clone' function, even conditionally,
+ * so it has to be a wrapper with a different name.
+ */
+function wfClone( $object ) {
+ // WARNING: clone() is not a function in PHP 5, so function_exists fails.
+ if( version_compare( PHP_VERSION, '5.0' ) < 0 ) {
+ return $object;
+ } else {
+ return clone( $object );
+ }
+}
+
+/**
+ * Where as we got a random seed
+ * @var bool $wgTotalViews
+ */
+$wgRandomSeeded = false;
+
+/**
+ * Seed Mersenne Twister
+ * Only necessary in PHP < 4.2.0
+ *
+ * @return bool
+ */
+function wfSeedRandom() {
+ global $wgRandomSeeded;
+
+ if ( ! $wgRandomSeeded && version_compare( phpversion(), '4.2.0' ) < 0 ) {
+ $seed = hexdec(substr(md5(microtime()),-8)) & 0x7fffffff;
+ mt_srand( $seed );
+ $wgRandomSeeded = true;
+ }
+}
+
+/**
+ * Get a random decimal value between 0 and 1, in a way
+ * not likely to give duplicate values for any realistic
+ * number of articles.
+ *
+ * @return string
+ */
+function wfRandom() {
+ # The maximum random value is "only" 2^31-1, so get two random
+ # values to reduce the chance of dupes
+ $max = mt_getrandmax();
+ $rand = number_format( (mt_rand() * $max + mt_rand())
+ / $max / $max, 12, '.', '' );
+ return $rand;
+}
+
+/**
+ * We want / and : to be included as literal characters in our title URLs.
+ * %2F in the page titles seems to fatally break for some reason.
+ *
+ * @param $s String:
+ * @return string
+*/
+function wfUrlencode ( $s ) {
+ $s = urlencode( $s );
+ $s = preg_replace( '/%3[Aa]/', ':', $s );
+ $s = preg_replace( '/%2[Ff]/', '/', $s );
+
+ return $s;
+}
+
+/**
+ * Sends a line to the debug log if enabled or, optionally, to a comment in output.
+ * In normal operation this is a NOP.
+ *
+ * Controlling globals:
+ * $wgDebugLogFile - points to the log file
+ * $wgProfileOnly - if set, normal debug messages will not be recorded.
+ * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output.
+ * $wgDebugComments - if on, some debug items may appear in comments in the HTML output.
+ *
+ * @param $text String
+ * @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set
+ */
+function wfDebug( $text, $logonly = false ) {
+ global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage;
+
+ # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet
+ if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) {
+ return;
+ }
+
+ if ( isset( $wgOut ) && $wgDebugComments && !$logonly ) {
+ $wgOut->debug( $text );
+ }
+ if ( '' != $wgDebugLogFile && !$wgProfileOnly ) {
+ # Strip unprintables; they can switch terminal modes when binary data
+ # gets dumped, which is pretty annoying.
+ $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text );
+ @error_log( $text, 3, $wgDebugLogFile );
+ }
+}
+
+/**
+ * Send a line to a supplementary debug log file, if configured, or main debug log if not.
+ * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log.
+ *
+ * @param $logGroup String
+ * @param $text String
+ * @param $public Bool: whether to log the event in the public log if no private
+ * log file is specified, (default true)
+ */
+function wfDebugLog( $logGroup, $text, $public = true ) {
+ global $wgDebugLogGroups, $wgDBname;
+ if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n";
+ if( isset( $wgDebugLogGroups[$logGroup] ) ) {
+ $time = wfTimestamp( TS_DB );
+ @error_log( "$time $wgDBname: $text", 3, $wgDebugLogGroups[$logGroup] );
+ } else if ( $public === true ) {
+ wfDebug( $text, true );
+ }
+}
+
+/**
+ * Log for database errors
+ * @param $text String: database error message.
+ */
+function wfLogDBError( $text ) {
+ global $wgDBerrorLog;
+ if ( $wgDBerrorLog ) {
+ $host = trim(`hostname`);
+ $text = date('D M j G:i:s T Y') . "\t$host\t".$text;
+ error_log( $text, 3, $wgDBerrorLog );
+ }
+}
+
+/**
+ * @todo document
+ */
+function logProfilingData() {
+ global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest;
+ global $wgProfiling, $wgUser;
+ $now = wfTime();
+
+ $elapsed = $now - $wgRequestTime;
+ if ( $wgProfiling ) {
+ $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed );
+ $forward = '';
+ if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) )
+ $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
+ if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) )
+ $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP'];
+ if( !empty( $_SERVER['HTTP_FROM'] ) )
+ $forward .= ' from ' . $_SERVER['HTTP_FROM'];
+ if( $forward )
+ $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})";
+ if( is_object($wgUser) && $wgUser->isAnon() )
+ $forward .= ' anon';
+ $log = sprintf( "%s\t%04.3f\t%s\n",
+ gmdate( 'YmdHis' ), $elapsed,
+ urldecode( $_SERVER['REQUEST_URI'] . $forward ) );
+ if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) {
+ error_log( $log . $prof, 3, $wgDebugLogFile );
+ }
+ }
+}
+
+/**
+ * Check if the wiki read-only lock file is present. This can be used to lock
+ * off editing functions, but doesn't guarantee that the database will not be
+ * modified.
+ * @return bool
+ */
+function wfReadOnly() {
+ global $wgReadOnlyFile, $wgReadOnly;
+
+ if ( !is_null( $wgReadOnly ) ) {
+ return (bool)$wgReadOnly;
+ }
+ if ( '' == $wgReadOnlyFile ) {
+ return false;
+ }
+ // Set $wgReadOnly for faster access next time
+ if ( is_file( $wgReadOnlyFile ) ) {
+ $wgReadOnly = file_get_contents( $wgReadOnlyFile );
+ } else {
+ $wgReadOnly = false;
+ }
+ return (bool)$wgReadOnly;
+}
+
+
+/**
+ * Get a message from anywhere, for the current user language.
+ *
+ * Use wfMsgForContent() instead if the message should NOT
+ * change depending on the user preferences.
+ *
+ * Note that the message may contain HTML, and is therefore
+ * not safe for insertion anywhere. Some functions such as
+ * addWikiText will do the escaping for you. Use wfMsgHtml()
+ * if you need an escaped message.
+ *
+ * @param $key String: lookup key for the message, usually
+ * defined in languages/Language.php
+ */
+function wfMsg( $key ) {
+ $args = func_get_args();
+ array_shift( $args );
+ return wfMsgReal( $key, $args, true );
+}
+
+/**
+ * Same as above except doesn't transform the message
+ */
+function wfMsgNoTrans( $key ) {
+ $args = func_get_args();
+ array_shift( $args );
+ return wfMsgReal( $key, $args, true, false );
+}
+
+/**
+ * Get a message from anywhere, for the current global language
+ * set with $wgLanguageCode.
+ *
+ * Use this if the message should NOT change dependent on the
+ * language set in the user's preferences. This is the case for
+ * most text written into logs, as well as link targets (such as
+ * the name of the copyright policy page). Link titles, on the
+ * other hand, should be shown in the UI language.
+ *
+ * Note that MediaWiki allows users to change the user interface
+ * language in their preferences, but a single installation
+ * typically only contains content in one language.
+ *
+ * Be wary of this distinction: If you use wfMsg() where you should
+ * use wfMsgForContent(), a user of the software may have to
+ * customize over 70 messages in order to, e.g., fix a link in every
+ * possible language.
+ *
+ * @param $key String: lookup key for the message, usually
+ * defined in languages/Language.php
+ */
+function wfMsgForContent( $key ) {
+ global $wgForceUIMsgAsContentMsg;
+ $args = func_get_args();
+ array_shift( $args );
+ $forcontent = true;
+ if( is_array( $wgForceUIMsgAsContentMsg ) &&
+ in_array( $key, $wgForceUIMsgAsContentMsg ) )
+ $forcontent = false;
+ return wfMsgReal( $key, $args, true, $forcontent );
+}
+
+/**
+ * Same as above except doesn't transform the message
+ */
+function wfMsgForContentNoTrans( $key ) {
+ global $wgForceUIMsgAsContentMsg;
+ $args = func_get_args();
+ array_shift( $args );
+ $forcontent = true;
+ if( is_array( $wgForceUIMsgAsContentMsg ) &&
+ in_array( $key, $wgForceUIMsgAsContentMsg ) )
+ $forcontent = false;
+ return wfMsgReal( $key, $args, true, $forcontent, false );
+}
+
+/**
+ * Get a message from the language file, for the UI elements
+ */
+function wfMsgNoDB( $key ) {
+ $args = func_get_args();
+ array_shift( $args );
+ return wfMsgReal( $key, $args, false );
+}
+
+/**
+ * Get a message from the language file, for the content
+ */
+function wfMsgNoDBForContent( $key ) {
+ global $wgForceUIMsgAsContentMsg;
+ $args = func_get_args();
+ array_shift( $args );
+ $forcontent = true;
+ if( is_array( $wgForceUIMsgAsContentMsg ) &&
+ in_array( $key, $wgForceUIMsgAsContentMsg ) )
+ $forcontent = false;
+ return wfMsgReal( $key, $args, false, $forcontent );
+}
+
+
+/**
+ * Really get a message
+ * @return $key String: key to get.
+ * @return $args
+ * @return $useDB Boolean
+ * @return String: the requested message.
+ */
+function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) {
+ $fname = 'wfMsgReal';
+
+ $message = wfMsgGetKey( $key, $useDB, $forContent, $transform );
+ $message = wfMsgReplaceArgs( $message, $args );
+ return $message;
+}
+
+/**
+ * This function provides the message source for messages to be edited which are *not* stored in the database.
+ * @param $key String:
+ */
+function wfMsgWeirdKey ( $key ) {
+ $subsource = str_replace ( ' ' , '_' , $key ) ;
+ $source = wfMsgForContentNoTrans( $subsource ) ;
+ if ( $source == "&lt;{$subsource}&gt;" ) {
+ # Try again with first char lower case
+ $subsource = strtolower ( substr ( $subsource , 0 , 1 ) ) . substr ( $subsource , 1 ) ;
+ $source = wfMsgForContentNoTrans( $subsource ) ;
+ }
+ if ( $source == "&lt;{$subsource}&gt;" ) {
+ # Didn't work either, return blank text
+ $source = "" ;
+ }
+ return $source ;
+}
+
+/**
+ * Fetch a message string value, but don't replace any keys yet.
+ * @param string $key
+ * @param bool $useDB
+ * @param bool $forContent
+ * @return string
+ * @private
+ */
+function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
+ global $wgParser, $wgMsgParserOptions, $wgContLang, $wgMessageCache, $wgLang;
+
+ if ( is_object( $wgMessageCache ) )
+ $transstat = $wgMessageCache->getTransform();
+
+ if( is_object( $wgMessageCache ) ) {
+ if ( ! $transform )
+ $wgMessageCache->disableTransform();
+ $message = $wgMessageCache->get( $key, $useDB, $forContent );
+ } else {
+ if( $forContent ) {
+ $lang = &$wgContLang;
+ } else {
+ $lang = &$wgLang;
+ }
+
+ wfSuppressWarnings();
+
+ if( is_object( $lang ) ) {
+ $message = $lang->getMessage( $key );
+ } else {
+ $message = false;
+ }
+ wfRestoreWarnings();
+ if($message === false)
+ $message = Language::getMessage($key);
+ if ( $transform && strstr( $message, '{{' ) !== false ) {
+ $message = $wgParser->transformMsg($message, $wgMsgParserOptions);
+ }
+ }
+
+ if ( is_object( $wgMessageCache ) && ! $transform )
+ $wgMessageCache->setTransform( $transstat );
+
+ return $message;
+}
+
+/**
+ * Replace message parameter keys on the given formatted output.
+ *
+ * @param string $message
+ * @param array $args
+ * @return string
+ * @private
+ */
+function wfMsgReplaceArgs( $message, $args ) {
+ # Fix windows line-endings
+ # Some messages are split with explode("\n", $msg)
+ $message = str_replace( "\r", '', $message );
+
+ // Replace arguments
+ if ( count( $args ) ) {
+ if ( is_array( $args[0] ) ) {
+ foreach ( $args[0] as $key => $val ) {
+ $message = str_replace( '$' . $key, $val, $message );
+ }
+ } else {
+ foreach( $args as $n => $param ) {
+ $replacementKeys['$' . ($n + 1)] = $param;
+ }
+ $message = strtr( $message, $replacementKeys );
+ }
+ }
+
+ return $message;
+}
+
+/**
+ * Return an HTML-escaped version of a message.
+ * Parameter replacements, if any, are done *after* the HTML-escaping,
+ * so parameters may contain HTML (eg links or form controls). Be sure
+ * to pre-escape them if you really do want plaintext, or just wrap
+ * the whole thing in htmlspecialchars().
+ *
+ * @param string $key
+ * @param string ... parameters
+ * @return string
+ */
+function wfMsgHtml( $key ) {
+ $args = func_get_args();
+ array_shift( $args );
+ return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key, true ) ), $args );
+}
+
+/**
+ * Return an HTML version of message
+ * Parameter replacements, if any, are done *after* parsing the wiki-text message,
+ * so parameters may contain HTML (eg links or form controls). Be sure
+ * to pre-escape them if you really do want plaintext, or just wrap
+ * the whole thing in htmlspecialchars().
+ *
+ * @param string $key
+ * @param string ... parameters
+ * @return string
+ */
+function wfMsgWikiHtml( $key ) {
+ global $wgOut;
+ $args = func_get_args();
+ array_shift( $args );
+ return wfMsgReplaceArgs( $wgOut->parse( wfMsgGetKey( $key, true ), /* can't be set to false */ true ), $args );
+}
+
+/**
+ * Returns message in the requested format
+ * @param string $key Key of the message
+ * @param array $options Processing rules:
+ * <i>parse<i>: parses wikitext to html
+ * <i>parseinline<i>: parses wikitext to html and removes the surrounding p's added by parser or tidy
+ * <i>escape<i>: filters message trough htmlspecialchars
+ * <i>replaceafter<i>: parameters are substituted after parsing or escaping
+ */
+function wfMsgExt( $key, $options ) {
+ global $wgOut, $wgMsgParserOptions, $wgParser;
+
+ $args = func_get_args();
+ array_shift( $args );
+ array_shift( $args );
+
+ if( !is_array($options) ) {
+ $options = array($options);
+ }
+
+ $string = wfMsgGetKey( $key, true, false, false );
+
+ if( !in_array('replaceafter', $options) ) {
+ $string = wfMsgReplaceArgs( $string, $args );
+ }
+
+ if( in_array('parse', $options) ) {
+ $string = $wgOut->parse( $string, true, true );
+ } elseif ( in_array('parseinline', $options) ) {
+ $string = $wgOut->parse( $string, true, true );
+ $m = array();
+ if( preg_match( "~^<p>(.*)\n?</p>$~", $string, $m ) ) {
+ $string = $m[1];
+ }
+ } elseif ( in_array('parsemag', $options) ) {
+ global $wgTitle;
+ $parser = new Parser();
+ $parserOptions = new ParserOptions();
+ $parserOptions->setInterfaceMessage( true );
+ $parser->startExternalParse( $wgTitle, $parserOptions, OT_MSG );
+ $string = $parser->transformMsg( $string, $parserOptions );
+ }
+
+ if ( in_array('escape', $options) ) {
+ $string = htmlspecialchars ( $string );
+ }
+
+ if( in_array('replaceafter', $options) ) {
+ $string = wfMsgReplaceArgs( $string, $args );
+ }
+
+ return $string;
+}
+
+
+/**
+ * Just like exit() but makes a note of it.
+ * Commits open transactions except if the error parameter is set
+ *
+ * @obsolete Please return control to the caller or throw an exception
+ */
+function wfAbruptExit( $error = false ){
+ global $wgLoadBalancer;
+ static $called = false;
+ if ( $called ){
+ exit( -1 );
+ }
+ $called = true;
+
+ if( function_exists( 'debug_backtrace' ) ){ // PHP >= 4.3
+ $bt = debug_backtrace();
+ for($i = 0; $i < count($bt) ; $i++){
+ $file = isset($bt[$i]['file']) ? $bt[$i]['file'] : "unknown";
+ $line = isset($bt[$i]['line']) ? $bt[$i]['line'] : "unknown";
+ wfDebug("WARNING: Abrupt exit in $file at line $line\n");
+ }
+ } else {
+ wfDebug('WARNING: Abrupt exit\n');
+ }
+
+ wfProfileClose();
+ logProfilingData();
+
+ if ( !$error ) {
+ $wgLoadBalancer->closeAll();
+ }
+ exit( -1 );
+}
+
+/**
+ * @obsolete Please return control the caller or throw an exception
+ */
+function wfErrorExit() {
+ wfAbruptExit( true );
+}
+
+/**
+ * Print a simple message and die, returning nonzero to the shell if any.
+ * Plain die() fails to return nonzero to the shell if you pass a string.
+ * @param string $msg
+ */
+function wfDie( $msg='' ) {
+ echo $msg;
+ die( 1 );
+}
+
+/**
+ * Throw a debugging exception. This function previously once exited the process,
+ * but now throws an exception instead, with similar results.
+ *
+ * @param string $msg Message shown when dieing.
+ */
+function wfDebugDieBacktrace( $msg = '' ) {
+ throw new MWException( $msg );
+}
+
+/**
+ * Fetch server name for use in error reporting etc.
+ * Use real server name if available, so we know which machine
+ * in a server farm generated the current page.
+ * @return string
+ */
+function wfHostname() {
+ if ( function_exists( 'posix_uname' ) ) {
+ // This function not present on Windows
+ $uname = @posix_uname();
+ } else {
+ $uname = false;
+ }
+ if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
+ return $uname['nodename'];
+ } else {
+ # This may be a virtual server.
+ return $_SERVER['SERVER_NAME'];
+ }
+}
+
+ /**
+ * Returns a HTML comment with the elapsed time since request.
+ * This method has no side effects.
+ * @return string
+ */
+ function wfReportTime() {
+ global $wgRequestTime;
+
+ $now = wfTime();
+ $elapsed = $now - $wgRequestTime;
+
+ $com = sprintf( "<!-- Served by %s in %01.3f secs. -->",
+ wfHostname(), $elapsed );
+ return $com;
+ }
+
+function wfBacktrace() {
+ global $wgCommandLineMode;
+ if ( !function_exists( 'debug_backtrace' ) ) {
+ return false;
+ }
+
+ if ( $wgCommandLineMode ) {
+ $msg = '';
+ } else {
+ $msg = "<ul>\n";
+ }
+ $backtrace = debug_backtrace();
+ foreach( $backtrace as $call ) {
+ if( isset( $call['file'] ) ) {
+ $f = explode( DIRECTORY_SEPARATOR, $call['file'] );
+ $file = $f[count($f)-1];
+ } else {
+ $file = '-';
+ }
+ if( isset( $call['line'] ) ) {
+ $line = $call['line'];
+ } else {
+ $line = '-';
+ }
+ if ( $wgCommandLineMode ) {
+ $msg .= "$file line $line calls ";
+ } else {
+ $msg .= '<li>' . $file . ' line ' . $line . ' calls ';
+ }
+ if( !empty( $call['class'] ) ) $msg .= $call['class'] . '::';
+ $msg .= $call['function'] . '()';
+
+ if ( $wgCommandLineMode ) {
+ $msg .= "\n";
+ } else {
+ $msg .= "</li>\n";
+ }
+ }
+ if ( $wgCommandLineMode ) {
+ $msg .= "\n";
+ } else {
+ $msg .= "</ul>\n";
+ }
+
+ return $msg;
+}
+
+
+/* Some generic result counters, pulled out of SearchEngine */
+
+
+/**
+ * @todo document
+ */
+function wfShowingResults( $offset, $limit ) {
+ global $wgLang;
+ return wfMsg( 'showingresults', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) );
+}
+
+/**
+ * @todo document
+ */
+function wfShowingResultsNum( $offset, $limit, $num ) {
+ global $wgLang;
+ return wfMsg( 'showingresultsnum', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) );
+}
+
+/**
+ * @todo document
+ */
+function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
+ global $wgLang;
+ $fmtLimit = $wgLang->formatNum( $limit );
+ $prev = wfMsg( 'prevn', $fmtLimit );
+ $next = wfMsg( 'nextn', $fmtLimit );
+
+ if( is_object( $link ) ) {
+ $title =& $link;
+ } else {
+ $title = Title::newFromText( $link );
+ if( is_null( $title ) ) {
+ return false;
+ }
+ }
+
+ if ( 0 != $offset ) {
+ $po = $offset - $limit;
+ if ( $po < 0 ) { $po = 0; }
+ $q = "limit={$limit}&offset={$po}";
+ if ( '' != $query ) { $q .= '&'.$query; }
+ $plink = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$prev}</a>";
+ } else { $plink = $prev; }
+
+ $no = $offset + $limit;
+ $q = 'limit='.$limit.'&offset='.$no;
+ if ( '' != $query ) { $q .= '&'.$query; }
+
+ if ( $atend ) {
+ $nlink = $next;
+ } else {
+ $nlink = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$next}</a>";
+ }
+ $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' .
+ wfNumLink( $offset, 50, $title, $query ) . ' | ' .
+ wfNumLink( $offset, 100, $title, $query ) . ' | ' .
+ wfNumLink( $offset, 250, $title, $query ) . ' | ' .
+ wfNumLink( $offset, 500, $title, $query );
+
+ return wfMsg( 'viewprevnext', $plink, $nlink, $nums );
+}
+
+/**
+ * @todo document
+ */
+function wfNumLink( $offset, $limit, &$title, $query = '' ) {
+ global $wgLang;
+ if ( '' == $query ) { $q = ''; }
+ else { $q = $query.'&'; }
+ $q .= 'limit='.$limit.'&offset='.$offset;
+
+ $fmtLimit = $wgLang->formatNum( $limit );
+ $s = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$fmtLimit}</a>";
+ return $s;
+}
+
+/**
+ * @todo document
+ * @todo FIXME: we may want to blacklist some broken browsers
+ *
+ * @return bool Whereas client accept gzip compression
+ */
+function wfClientAcceptsGzip() {
+ global $wgUseGzip;
+ if( $wgUseGzip ) {
+ # FIXME: we may want to blacklist some broken browsers
+ if( preg_match(
+ '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/',
+ $_SERVER['HTTP_ACCEPT_ENCODING'],
+ $m ) ) {
+ if( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) return false;
+ wfDebug( " accepts gzip\n" );
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Obtain the offset and limit values from the request string;
+ * used in special pages
+ *
+ * @param $deflimit Default limit if none supplied
+ * @param $optionname Name of a user preference to check against
+ * @return array
+ *
+ */
+function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
+ global $wgRequest;
+ return $wgRequest->getLimitOffset( $deflimit, $optionname );
+}
+
+/**
+ * Escapes the given text so that it may be output using addWikiText()
+ * without any linking, formatting, etc. making its way through. This
+ * is achieved by substituting certain characters with HTML entities.
+ * As required by the callers, <nowiki> is not used. It currently does
+ * not filter out characters which have special meaning only at the
+ * start of a line, such as "*".
+ *
+ * @param string $text Text to be escaped
+ */
+function wfEscapeWikiText( $text ) {
+ $text = str_replace(
+ array( '[', '|', '\'', 'ISBN ' , '://' , "\n=", '{{' ),
+ array( '&#91;', '&#124;', '&#39;', 'ISBN&#32;', '&#58;//' , "\n&#61;", '&#123;&#123;' ),
+ htmlspecialchars($text) );
+ return $text;
+}
+
+/**
+ * @todo document
+ */
+function wfQuotedPrintable( $string, $charset = '' ) {
+ # Probably incomplete; see RFC 2045
+ if( empty( $charset ) ) {
+ global $wgInputEncoding;
+ $charset = $wgInputEncoding;
+ }
+ $charset = strtoupper( $charset );
+ $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ?
+
+ $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff=';
+ $replace = $illegal . '\t ?_';
+ if( !preg_match( "/[$illegal]/", $string ) ) return $string;
+ $out = "=?$charset?Q?";
+ $out .= preg_replace( "/([$replace])/e", 'sprintf("=%02X",ord("$1"))', $string );
+ $out .= '?=';
+ return $out;
+}
+
+
+/**
+ * @todo document
+ * @return float
+ */
+function wfTime() {
+ return microtime(true);
+}
+
+/**
+ * Sets dest to source and returns the original value of dest
+ * If source is NULL, it just returns the value, it doesn't set the variable
+ */
+function wfSetVar( &$dest, $source ) {
+ $temp = $dest;
+ if ( !is_null( $source ) ) {
+ $dest = $source;
+ }
+ return $temp;
+}
+
+/**
+ * As for wfSetVar except setting a bit
+ */
+function wfSetBit( &$dest, $bit, $state = true ) {
+ $temp = (bool)($dest & $bit );
+ if ( !is_null( $state ) ) {
+ if ( $state ) {
+ $dest |= $bit;
+ } else {
+ $dest &= ~$bit;
+ }
+ }
+ return $temp;
+}
+
+/**
+ * This function takes two arrays as input, and returns a CGI-style string, e.g.
+ * "days=7&limit=100". Options in the first array override options in the second.
+ * Options set to "" will not be output.
+ */
+function wfArrayToCGI( $array1, $array2 = NULL )
+{
+ if ( !is_null( $array2 ) ) {
+ $array1 = $array1 + $array2;
+ }
+
+ $cgi = '';
+ foreach ( $array1 as $key => $value ) {
+ if ( '' !== $value ) {
+ if ( '' != $cgi ) {
+ $cgi .= '&';
+ }
+ $cgi .= urlencode( $key ) . '=' . urlencode( $value );
+ }
+ }
+ return $cgi;
+}
+
+/**
+ * This is obsolete, use SquidUpdate::purge()
+ * @deprecated
+ */
+function wfPurgeSquidServers ($urlArr) {
+ SquidUpdate::purge( $urlArr );
+}
+
+/**
+ * Windows-compatible version of escapeshellarg()
+ * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg()
+ * function puts single quotes in regardless of OS
+ */
+function wfEscapeShellArg( ) {
+ $args = func_get_args();
+ $first = true;
+ $retVal = '';
+ foreach ( $args as $arg ) {
+ if ( !$first ) {
+ $retVal .= ' ';
+ } else {
+ $first = false;
+ }
+
+ if ( wfIsWindows() ) {
+ // Escaping for an MSVC-style command line parser
+ // Ref: http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
+ // Double the backslashes before any double quotes. Escape the double quotes.
+ $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE );
+ $arg = '';
+ $delim = false;
+ foreach ( $tokens as $token ) {
+ if ( $delim ) {
+ $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"';
+ } else {
+ $arg .= $token;
+ }
+ $delim = !$delim;
+ }
+ // Double the backslashes before the end of the string, because
+ // we will soon add a quote
+ if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) {
+ $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] );
+ }
+
+ // Add surrounding quotes
+ $retVal .= '"' . $arg . '"';
+ } else {
+ $retVal .= escapeshellarg( $arg );
+ }
+ }
+ return $retVal;
+}
+
+/**
+ * wfMerge attempts to merge differences between three texts.
+ * Returns true for a clean merge and false for failure or a conflict.
+ */
+function wfMerge( $old, $mine, $yours, &$result ){
+ global $wgDiff3;
+
+ # This check may also protect against code injection in
+ # case of broken installations.
+ if(! file_exists( $wgDiff3 ) ){
+ wfDebug( "diff3 not found\n" );
+ return false;
+ }
+
+ # Make temporary files
+ $td = wfTempDir();
+ $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' );
+ $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' );
+ $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' );
+
+ fwrite( $oldtextFile, $old ); fclose( $oldtextFile );
+ fwrite( $mytextFile, $mine ); fclose( $mytextFile );
+ fwrite( $yourtextFile, $yours ); fclose( $yourtextFile );
+
+ # Check for a conflict
+ $cmd = $wgDiff3 . ' -a --overlap-only ' .
+ wfEscapeShellArg( $mytextName ) . ' ' .
+ wfEscapeShellArg( $oldtextName ) . ' ' .
+ wfEscapeShellArg( $yourtextName );
+ $handle = popen( $cmd, 'r' );
+
+ if( fgets( $handle, 1024 ) ){
+ $conflict = true;
+ } else {
+ $conflict = false;
+ }
+ pclose( $handle );
+
+ # Merge differences
+ $cmd = $wgDiff3 . ' -a -e --merge ' .
+ wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName );
+ $handle = popen( $cmd, 'r' );
+ $result = '';
+ do {
+ $data = fread( $handle, 8192 );
+ if ( strlen( $data ) == 0 ) {
+ break;
+ }
+ $result .= $data;
+ } while ( true );
+ pclose( $handle );
+ unlink( $mytextName ); unlink( $oldtextName ); unlink( $yourtextName );
+
+ if ( $result === '' && $old !== '' && $conflict == false ) {
+ wfDebug( "Unexpected null result from diff3. Command: $cmd\n" );
+ $conflict = true;
+ }
+ return ! $conflict;
+}
+
+/**
+ * @todo document
+ */
+function wfVarDump( $var ) {
+ global $wgOut;
+ $s = str_replace("\n","<br />\n", var_export( $var, true ) . "\n");
+ if ( headers_sent() || !@is_object( $wgOut ) ) {
+ print $s;
+ } else {
+ $wgOut->addHTML( $s );
+ }
+}
+
+/**
+ * Provide a simple HTTP error.
+ */
+function wfHttpError( $code, $label, $desc ) {
+ global $wgOut;
+ $wgOut->disable();
+ header( "HTTP/1.0 $code $label" );
+ header( "Status: $code $label" );
+ $wgOut->sendCacheControl();
+
+ header( 'Content-type: text/html' );
+ print "<html><head><title>" .
+ htmlspecialchars( $label ) .
+ "</title></head><body><h1>" .
+ htmlspecialchars( $label ) .
+ "</h1><p>" .
+ htmlspecialchars( $desc ) .
+ "</p></body></html>\n";
+}
+
+/**
+ * Converts an Accept-* header into an array mapping string values to quality
+ * factors
+ */
+function wfAcceptToPrefs( $accept, $def = '*/*' ) {
+ # No arg means accept anything (per HTTP spec)
+ if( !$accept ) {
+ return array( $def => 1 );
+ }
+
+ $prefs = array();
+
+ $parts = explode( ',', $accept );
+
+ foreach( $parts as $part ) {
+ # FIXME: doesn't deal with params like 'text/html; level=1'
+ @list( $value, $qpart ) = explode( ';', $part );
+ if( !isset( $qpart ) ) {
+ $prefs[$value] = 1;
+ } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) {
+ $prefs[$value] = $match[1];
+ }
+ }
+
+ return $prefs;
+}
+
+/**
+ * Checks if a given MIME type matches any of the keys in the given
+ * array. Basic wildcards are accepted in the array keys.
+ *
+ * Returns the matching MIME type (or wildcard) if a match, otherwise
+ * NULL if no match.
+ *
+ * @param string $type
+ * @param array $avail
+ * @return string
+ * @private
+ */
+function mimeTypeMatch( $type, $avail ) {
+ if( array_key_exists($type, $avail) ) {
+ return $type;
+ } else {
+ $parts = explode( '/', $type );
+ if( array_key_exists( $parts[0] . '/*', $avail ) ) {
+ return $parts[0] . '/*';
+ } elseif( array_key_exists( '*/*', $avail ) ) {
+ return '*/*';
+ } else {
+ return NULL;
+ }
+ }
+}
+
+/**
+ * Returns the 'best' match between a client's requested internet media types
+ * and the server's list of available types. Each list should be an associative
+ * array of type to preference (preference is a float between 0.0 and 1.0).
+ * Wildcards in the types are acceptable.
+ *
+ * @param array $cprefs Client's acceptable type list
+ * @param array $sprefs Server's offered types
+ * @return string
+ *
+ * @todo FIXME: doesn't handle params like 'text/plain; charset=UTF-8'
+ * XXX: generalize to negotiate other stuff
+ */
+function wfNegotiateType( $cprefs, $sprefs ) {
+ $combine = array();
+
+ foreach( array_keys($sprefs) as $type ) {
+ $parts = explode( '/', $type );
+ if( $parts[1] != '*' ) {
+ $ckey = mimeTypeMatch( $type, $cprefs );
+ if( $ckey ) {
+ $combine[$type] = $sprefs[$type] * $cprefs[$ckey];
+ }
+ }
+ }
+
+ foreach( array_keys( $cprefs ) as $type ) {
+ $parts = explode( '/', $type );
+ if( $parts[1] != '*' && !array_key_exists( $type, $sprefs ) ) {
+ $skey = mimeTypeMatch( $type, $sprefs );
+ if( $skey ) {
+ $combine[$type] = $sprefs[$skey] * $cprefs[$type];
+ }
+ }
+ }
+
+ $bestq = 0;
+ $besttype = NULL;
+
+ foreach( array_keys( $combine ) as $type ) {
+ if( $combine[$type] > $bestq ) {
+ $besttype = $type;
+ $bestq = $combine[$type];
+ }
+ }
+
+ return $besttype;
+}
+
+/**
+ * Array lookup
+ * Returns an array where the values in the first array are replaced by the
+ * values in the second array with the corresponding keys
+ *
+ * @return array
+ */
+function wfArrayLookup( $a, $b ) {
+ return array_flip( array_intersect( array_flip( $a ), array_keys( $b ) ) );
+}
+
+/**
+ * Convenience function; returns MediaWiki timestamp for the present time.
+ * @return string
+ */
+function wfTimestampNow() {
+ # return NOW
+ return wfTimestamp( TS_MW, time() );
+}
+
+/**
+ * Reference-counted warning suppression
+ */
+function wfSuppressWarnings( $end = false ) {
+ static $suppressCount = 0;
+ static $originalLevel = false;
+
+ if ( $end ) {
+ if ( $suppressCount ) {
+ --$suppressCount;
+ if ( !$suppressCount ) {
+ error_reporting( $originalLevel );
+ }
+ }
+ } else {
+ if ( !$suppressCount ) {
+ $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE ) );
+ }
+ ++$suppressCount;
+ }
+}
+
+/**
+ * Restore error level to previous value
+ */
+function wfRestoreWarnings() {
+ wfSuppressWarnings( true );
+}
+
+# Autodetect, convert and provide timestamps of various types
+
+/**
+ * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC
+ */
+define('TS_UNIX', 0);
+
+/**
+ * MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
+ */
+define('TS_MW', 1);
+
+/**
+ * MySQL DATETIME (YYYY-MM-DD HH:MM:SS)
+ */
+define('TS_DB', 2);
+
+/**
+ * RFC 2822 format, for E-mail and HTTP headers
+ */
+define('TS_RFC2822', 3);
+
+/**
+ * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z
+ *
+ * This is used by Special:Export
+ */
+define('TS_ISO_8601', 4);
+
+/**
+ * An Exif timestamp (YYYY:MM:DD HH:MM:SS)
+ *
+ * @url http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the
+ * DateTime tag and page 36 for the DateTimeOriginal and
+ * DateTimeDigitized tags.
+ */
+define('TS_EXIF', 5);
+
+/**
+ * Oracle format time.
+ */
+define('TS_ORACLE', 6);
+
+/**
+ * @param mixed $outputtype A timestamp in one of the supported formats, the
+ * function will autodetect which format is supplied
+ * and act accordingly.
+ * @return string Time in the format specified in $outputtype
+ */
+function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
+ $uts = 0;
+ $da = array();
+ if ($ts==0) {
+ $uts=time();
+ } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) {
+ # TS_DB
+ $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
+ (int)$da[2],(int)$da[3],(int)$da[1]);
+ } elseif (preg_match("/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) {
+ # TS_EXIF
+ $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
+ (int)$da[2],(int)$da[3],(int)$da[1]);
+ } elseif (preg_match("/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D",$ts,$da)) {
+ # TS_MW
+ $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
+ (int)$da[2],(int)$da[3],(int)$da[1]);
+ } elseif (preg_match("/^(\d{1,13})$/D",$ts,$datearray)) {
+ # TS_UNIX
+ $uts = $ts;
+ } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) {
+ # TS_ORACLE
+ $uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
+ str_replace("+00:00", "UTC", $ts)));
+ } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) {
+ # TS_ISO_8601
+ $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
+ (int)$da[2],(int)$da[3],(int)$da[1]);
+ } else {
+ # Bogus value; fall back to the epoch...
+ wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n");
+ $uts = 0;
+ }
+
+
+ switch($outputtype) {
+ case TS_UNIX:
+ return $uts;
+ case TS_MW:
+ return gmdate( 'YmdHis', $uts );
+ case TS_DB:
+ return gmdate( 'Y-m-d H:i:s', $uts );
+ case TS_ISO_8601:
+ return gmdate( 'Y-m-d\TH:i:s\Z', $uts );
+ // This shouldn't ever be used, but is included for completeness
+ case TS_EXIF:
+ return gmdate( 'Y:m:d H:i:s', $uts );
+ case TS_RFC2822:
+ return gmdate( 'D, d M Y H:i:s', $uts ) . ' GMT';
+ case TS_ORACLE:
+ return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00';
+ default:
+ throw new MWException( 'wfTimestamp() called with illegal output type.');
+ }
+}
+
+/**
+ * Return a formatted timestamp, or null if input is null.
+ * For dealing with nullable timestamp columns in the database.
+ * @param int $outputtype
+ * @param string $ts
+ * @return string
+ */
+function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return wfTimestamp( $outputtype, $ts );
+ }
+}
+
+/**
+ * Check if the operating system is Windows
+ *
+ * @return bool True if it's Windows, False otherwise.
+ */
+function wfIsWindows() {
+ if (substr(php_uname(), 0, 7) == 'Windows') {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Swap two variables
+ */
+function swap( &$x, &$y ) {
+ $z = $x;
+ $x = $y;
+ $y = $z;
+}
+
+function wfGetCachedNotice( $name ) {
+ global $wgOut, $parserMemc, $wgDBname;
+ $fname = 'wfGetCachedNotice';
+ wfProfileIn( $fname );
+
+ $needParse = false;
+ $notice = wfMsgForContent( $name );
+ if( $notice == '&lt;'. $name . ';&gt' || $notice == '-' ) {
+ wfProfileOut( $fname );
+ return( false );
+ }
+
+ $cachedNotice = $parserMemc->get( $wgDBname . ':' . $name );
+ if( is_array( $cachedNotice ) ) {
+ if( md5( $notice ) == $cachedNotice['hash'] ) {
+ $notice = $cachedNotice['html'];
+ } else {
+ $needParse = true;
+ }
+ } else {
+ $needParse = true;
+ }
+
+ if( $needParse ) {
+ if( is_object( $wgOut ) ) {
+ $parsed = $wgOut->parse( $notice );
+ $parserMemc->set( $wgDBname . ':' . $name, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 );
+ $notice = $parsed;
+ } else {
+ wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' );
+ $notice = '';
+ }
+ }
+
+ wfProfileOut( $fname );
+ return $notice;
+}
+
+function wfGetNamespaceNotice() {
+ global $wgTitle;
+
+ # Paranoia
+ if ( !isset( $wgTitle ) || !is_object( $wgTitle ) )
+ return "";
+
+ $fname = 'wfGetNamespaceNotice';
+ wfProfileIn( $fname );
+
+ $key = "namespacenotice-" . $wgTitle->getNsText();
+ $namespaceNotice = wfGetCachedNotice( $key );
+ if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "<p>&lt;" ) {
+ $namespaceNotice = '<div id="namespacebanner">' . $namespaceNotice . "</div>";
+ } else {
+ $namespaceNotice = "";
+ }
+
+ wfProfileOut( $fname );
+ return $namespaceNotice;
+}
+
+function wfGetSiteNotice() {
+ global $wgUser, $wgSiteNotice;
+ $fname = 'wfGetSiteNotice';
+ wfProfileIn( $fname );
+ $siteNotice = '';
+
+ if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) {
+ if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) {
+ $siteNotice = wfGetCachedNotice( 'sitenotice' );
+ $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice;
+ } else {
+ $anonNotice = wfGetCachedNotice( 'anonnotice' );
+ if( !$anonNotice ) {
+ $siteNotice = wfGetCachedNotice( 'sitenotice' );
+ $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice;
+ } else {
+ $siteNotice = $anonNotice;
+ }
+ }
+ }
+
+ wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice ) );
+ wfProfileOut( $fname );
+ return $siteNotice;
+}
+
+/** Global singleton instance of MimeMagic. This is initialized on demand,
+* please always use the wfGetMimeMagic() function to get the instance.
+*
+* @private
+*/
+$wgMimeMagic= NULL;
+
+/** Factory functions for the global MimeMagic object.
+* This function always returns the same singleton instance of MimeMagic.
+* That objects will be instantiated on the first call to this function.
+* If needed, the MimeMagic.php file is automatically included by this function.
+* @return MimeMagic the global MimeMagic objects.
+*/
+function &wfGetMimeMagic() {
+ global $wgMimeMagic;
+
+ if (!is_null($wgMimeMagic)) {
+ return $wgMimeMagic;
+ }
+
+ if (!class_exists("MimeMagic")) {
+ #include on demand
+ require_once("MimeMagic.php");
+ }
+
+ $wgMimeMagic= new MimeMagic();
+
+ return $wgMimeMagic;
+}
+
+
+/**
+ * Tries to get the system directory for temporary files.
+ * The TMPDIR, TMP, and TEMP environment variables are checked in sequence,
+ * and if none are set /tmp is returned as the generic Unix default.
+ *
+ * NOTE: When possible, use the tempfile() function to create temporary
+ * files to avoid race conditions on file creation, etc.
+ *
+ * @return string
+ */
+function wfTempDir() {
+ foreach( array( 'TMPDIR', 'TMP', 'TEMP' ) as $var ) {
+ $tmp = getenv( $var );
+ if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
+ return $tmp;
+ }
+ }
+ # Hope this is Unix of some kind!
+ return '/tmp';
+}
+
+/**
+ * Make directory, and make all parent directories if they don't exist
+ */
+function wfMkdirParents( $fullDir, $mode = 0777 ) {
+ if ( strval( $fullDir ) === '' ) {
+ return true;
+ }
+
+ # Go back through the paths to find the first directory that exists
+ $currentDir = $fullDir;
+ $createList = array();
+ while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) {
+ # Strip trailing slashes
+ $currentDir = rtrim( $currentDir, '/\\' );
+
+ # Add to create list
+ $createList[] = $currentDir;
+
+ # Find next delimiter searching from the end
+ $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) );
+ if ( $p === false ) {
+ $currentDir = false;
+ } else {
+ $currentDir = substr( $currentDir, 0, $p );
+ }
+ }
+
+ if ( count( $createList ) == 0 ) {
+ # Directory specified already exists
+ return true;
+ } elseif ( $currentDir === false ) {
+ # Went all the way back to root and it apparently doesn't exist
+ return false;
+ }
+
+ # Now go forward creating directories
+ $createList = array_reverse( $createList );
+ foreach ( $createList as $dir ) {
+ # use chmod to override the umask, as suggested by the PHP manual
+ if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Increment a statistics counter
+ */
+ function wfIncrStats( $key ) {
+ global $wgDBname, $wgMemc;
+ $key = "$wgDBname:stats:$key";
+ if ( is_null( $wgMemc->incr( $key ) ) ) {
+ $wgMemc->add( $key, 1 );
+ }
+ }
+
+/**
+ * @param mixed $nr The number to format
+ * @param int $acc The number of digits after the decimal point, default 2
+ * @param bool $round Whether or not to round the value, default true
+ * @return float
+ */
+function wfPercent( $nr, $acc = 2, $round = true ) {
+ $ret = sprintf( "%.${acc}f", $nr );
+ return $round ? round( $ret, $acc ) . '%' : "$ret%";
+}
+
+/**
+ * Encrypt a username/password.
+ *
+ * @param string $userid ID of the user
+ * @param string $password Password of the user
+ * @return string Hashed password
+ */
+function wfEncryptPassword( $userid, $password ) {
+ global $wgPasswordSalt;
+ $p = md5( $password);
+
+ if($wgPasswordSalt)
+ return md5( "{$userid}-{$p}" );
+ else
+ return $p;
+}
+
+/**
+ * Appends to second array if $value differs from that in $default
+ */
+function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) {
+ if ( is_null( $changed ) ) {
+ throw new MWException('GlobalFunctions::wfAppendToArrayIfNotDefault got null');
+ }
+ if ( $default[$key] !== $value ) {
+ $changed[$key] = $value;
+ }
+}
+
+/**
+ * Since wfMsg() and co suck, they don't return false if the message key they
+ * looked up didn't exist but a XHTML string, this function checks for the
+ * nonexistance of messages by looking at wfMsg() output
+ *
+ * @param $msg The message key looked up
+ * @param $wfMsgOut The output of wfMsg*()
+ * @return bool
+ */
+function wfEmptyMsg( $msg, $wfMsgOut ) {
+ return $wfMsgOut === "&lt;$msg&gt;";
+}
+
+/**
+ * Find out whether or not a mixed variable exists in a string
+ *
+ * @param mixed needle
+ * @param string haystack
+ * @return bool
+ */
+function in_string( $needle, $str ) {
+ return strpos( $str, $needle ) !== false;
+}
+
+function wfSpecialList( $page, $details ) {
+ global $wgContLang;
+ $details = $details ? ' ' . $wgContLang->getDirMark() . "($details)" : "";
+ return $page . $details;
+}
+
+/**
+ * Returns a regular expression of url protocols
+ *
+ * @return string
+ */
+function wfUrlProtocols() {
+ global $wgUrlProtocols;
+
+ // Support old-style $wgUrlProtocols strings, for backwards compatibility
+ // with LocalSettings files from 1.5
+ if ( is_array( $wgUrlProtocols ) ) {
+ $protocols = array();
+ foreach ($wgUrlProtocols as $protocol)
+ $protocols[] = preg_quote( $protocol, '/' );
+
+ return implode( '|', $protocols );
+ } else {
+ return $wgUrlProtocols;
+ }
+}
+
+/**
+ * Execute a shell command, with time and memory limits mirrored from the PHP
+ * configuration if supported.
+ * @param $cmd Command line, properly escaped for shell.
+ * @param &$retval optional, will receive the program's exit code.
+ * (non-zero is usually failure)
+ * @return collected stdout as a string (trailing newlines stripped)
+ */
+function wfShellExec( $cmd, &$retval=null ) {
+ global $IP, $wgMaxShellMemory;
+
+ if( ini_get( 'safe_mode' ) ) {
+ wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" );
+ $retval = 1;
+ return "Unable to run external programs in safe mode.";
+ }
+
+ if ( php_uname( 's' ) == 'Linux' ) {
+ $time = ini_get( 'max_execution_time' );
+ $mem = intval( $wgMaxShellMemory );
+
+ if ( $time > 0 && $mem > 0 ) {
+ $script = "$IP/bin/ulimit.sh";
+ if ( is_executable( $script ) ) {
+ $cmd = escapeshellarg( $script ) . " $time $mem $cmd";
+ }
+ }
+ } elseif ( php_uname( 's' ) == 'Windows NT' ) {
+ # This is a hack to work around PHP's flawed invocation of cmd.exe
+ # http://news.php.net/php.internals/21796
+ $cmd = '"' . $cmd . '"';
+ }
+ wfDebug( "wfShellExec: $cmd\n" );
+
+ $output = array();
+ $retval = 1; // error by default?
+ $lastline = exec( $cmd, $output, $retval );
+ return implode( "\n", $output );
+
+}
+
+/**
+ * This function works like "use VERSION" in Perl, the program will die with a
+ * backtrace if the current version of PHP is less than the version provided
+ *
+ * This is useful for extensions which due to their nature are not kept in sync
+ * with releases, and might depend on other versions of PHP than the main code
+ *
+ * Note: PHP might die due to parsing errors in some cases before it ever
+ * manages to call this function, such is life
+ *
+ * @see perldoc -f use
+ *
+ * @param mixed $version The version to check, can be a string, an integer, or
+ * a float
+ */
+function wfUsePHP( $req_ver ) {
+ $php_ver = PHP_VERSION;
+
+ if ( version_compare( $php_ver, (string)$req_ver, '<' ) )
+ throw new MWException( "PHP $req_ver required--this is only $php_ver" );
+}
+
+/**
+ * This function works like "use VERSION" in Perl except it checks the version
+ * of MediaWiki, the program will die with a backtrace if the current version
+ * of MediaWiki is less than the version provided.
+ *
+ * This is useful for extensions which due to their nature are not kept in sync
+ * with releases
+ *
+ * @see perldoc -f use
+ *
+ * @param mixed $version The version to check, can be a string, an integer, or
+ * a float
+ */
+function wfUseMW( $req_ver ) {
+ global $wgVersion;
+
+ if ( version_compare( $wgVersion, (string)$req_ver, '<' ) )
+ throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" );
+}
+
+/**
+ * Escape a string to make it suitable for inclusion in a preg_replace()
+ * replacement parameter.
+ *
+ * @param string $string
+ * @return string
+ */
+function wfRegexReplacement( $string ) {
+ $string = str_replace( '\\', '\\\\', $string );
+ $string = str_replace( '$', '\\$', $string );
+ return $string;
+}
+
+/**
+ * Return the final portion of a pathname.
+ * Reimplemented because PHP5's basename() is buggy with multibyte text.
+ * http://bugs.php.net/bug.php?id=33898
+ *
+ * PHP's basename() only considers '\' a pathchar on Windows and Netware.
+ * We'll consider it so always, as we don't want \s in our Unix paths either.
+ *
+ * @param string $path
+ * @return string
+ */
+function wfBaseName( $path ) {
+ if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) {
+ return $matches[1];
+ } else {
+ return '';
+ }
+}
+
+/**
+ * Make a URL index, appropriate for the el_index field of externallinks.
+ */
+function wfMakeUrlIndex( $url ) {
+ wfSuppressWarnings();
+ $bits = parse_url( $url );
+ wfRestoreWarnings();
+ if ( !$bits || $bits['scheme'] !== 'http' ) {
+ return false;
+ }
+ // Reverse the labels in the hostname, convert to lower case
+ $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) );
+ // Add an extra dot to the end
+ if ( substr( $reversedHost, -1, 1 ) !== '.' ) {
+ $reversedHost .= '.';
+ }
+ // Reconstruct the pseudo-URL
+ $index = "http://$reversedHost";
+ // Leave out user and password. Add the port, path, query and fragment
+ if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port'];
+ if ( isset( $bits['path'] ) ) {
+ $index .= $bits['path'];
+ } else {
+ $index .= '/';
+ }
+ if ( isset( $bits['query'] ) ) $index .= '?' . $bits['query'];
+ if ( isset( $bits['fragment'] ) ) $index .= '#' . $bits['fragment'];
+ return $index;
+}
+
+/**
+ * Do any deferred updates and clear the list
+ * TODO: This could be in Wiki.php if that class made any sense at all
+ */
+function wfDoUpdates()
+{
+ global $wgPostCommitUpdateList, $wgDeferredUpdateList;
+ foreach ( $wgDeferredUpdateList as $update ) {
+ $update->doUpdate();
+ }
+ foreach ( $wgPostCommitUpdateList as $update ) {
+ $update->doUpdate();
+ }
+ $wgDeferredUpdateList = array();
+ $wgPostCommitUpdateList = array();
+}
+
+/**
+ * More or less "markup-safe" explode()
+ * Ignores any instances of the separator inside <...>
+ * @param string $separator
+ * @param string $text
+ * @return array
+ */
+function wfExplodeMarkup( $separator, $text ) {
+ $placeholder = "\x00";
+
+ // Just in case...
+ $text = str_replace( $placeholder, '', $text );
+
+ // Trim stuff
+ $replacer = new ReplacerCallback( $separator, $placeholder );
+ $cleaned = preg_replace_callback( '/(<.*?>)/', array( $replacer, 'go' ), $text );
+
+ $items = explode( $separator, $cleaned );
+ foreach( $items as $i => $str ) {
+ $items[$i] = str_replace( $placeholder, $separator, $str );
+ }
+
+ return $items;
+}
+
+class ReplacerCallback {
+ function ReplacerCallback( $from, $to ) {
+ $this->from = $from;
+ $this->to = $to;
+ }
+
+ function go( $matches ) {
+ return str_replace( $this->from, $this->to, $matches[1] );
+ }
+}
+
+
+/**
+ * Convert an arbitrarily-long digit string from one numeric base
+ * to another, optionally zero-padding to a minimum column width.
+ *
+ * Supports base 2 through 36; digit values 10-36 are represented
+ * as lowercase letters a-z. Input is case-insensitive.
+ *
+ * @param $input string of digits
+ * @param $sourceBase int 2-36
+ * @param $destBase int 2-36
+ * @param $pad int 1 or greater
+ * @return string or false on invalid input
+ */
+function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1 ) {
+ if( $sourceBase < 2 ||
+ $sourceBase > 36 ||
+ $destBase < 2 ||
+ $destBase > 36 ||
+ $pad < 1 ||
+ $sourceBase != intval( $sourceBase ) ||
+ $destBase != intval( $destBase ) ||
+ $pad != intval( $pad ) ||
+ !is_string( $input ) ||
+ $input == '' ) {
+ return false;
+ }
+
+ $digitChars = '0123456789abcdefghijklmnopqrstuvwxyz';
+ $inDigits = array();
+ $outChars = '';
+
+ // Decode and validate input string
+ $input = strtolower( $input );
+ for( $i = 0; $i < strlen( $input ); $i++ ) {
+ $n = strpos( $digitChars, $input{$i} );
+ if( $n === false || $n > $sourceBase ) {
+ return false;
+ }
+ $inDigits[] = $n;
+ }
+
+ // Iterate over the input, modulo-ing out an output digit
+ // at a time until input is gone.
+ while( count( $inDigits ) ) {
+ $work = 0;
+ $workDigits = array();
+
+ // Long division...
+ foreach( $inDigits as $digit ) {
+ $work *= $sourceBase;
+ $work += $digit;
+
+ if( $work < $destBase ) {
+ // Gonna need to pull another digit.
+ if( count( $workDigits ) ) {
+ // Avoid zero-padding; this lets us find
+ // the end of the input very easily when
+ // length drops to zero.
+ $workDigits[] = 0;
+ }
+ } else {
+ // Finally! Actual division!
+ $workDigits[] = intval( $work / $destBase );
+
+ // Isn't it annoying that most programming languages
+ // don't have a single divide-and-remainder operator,
+ // even though the CPU implements it that way?
+ $work = $work % $destBase;
+ }
+ }
+
+ // All that division leaves us with a remainder,
+ // which is conveniently our next output digit.
+ $outChars .= $digitChars[$work];
+
+ // And we continue!
+ $inDigits = $workDigits;
+ }
+
+ while( strlen( $outChars ) < $pad ) {
+ $outChars .= '0';
+ }
+
+ return strrev( $outChars );
+}
+
+/**
+ * Create an object with a given name and an array of construct parameters
+ * @param string $name
+ * @param array $p parameters
+ */
+function wfCreateObject( $name, $p ){
+ $p = array_values( $p );
+ switch ( count( $p ) ) {
+ case 0:
+ return new $name;
+ case 1:
+ return new $name( $p[0] );
+ case 2:
+ return new $name( $p[0], $p[1] );
+ case 3:
+ return new $name( $p[0], $p[1], $p[2] );
+ case 4:
+ return new $name( $p[0], $p[1], $p[2], $p[3] );
+ case 5:
+ return new $name( $p[0], $p[1], $p[2], $p[3], $p[4] );
+ case 6:
+ return new $name( $p[0], $p[1], $p[2], $p[3], $p[4], $p[5] );
+ default:
+ throw new MWException( "Too many arguments to construtor in wfCreateObject" );
+ }
+}
+
+/**
+ * Aliases for modularized functions
+ */
+function wfGetHTTP( $url, $timeout = 'default' ) {
+ return Http::get( $url, $timeout );
+}
+function wfIsLocalURL( $url ) {
+ return Http::isLocalURL( $url );
+}
+
+?>
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
new file mode 100644
index 00000000..47703b20
--- /dev/null
+++ b/includes/HTMLCacheUpdate.php
@@ -0,0 +1,230 @@
+<?php
+
+/**
+ * Class to invalidate the HTML cache of all the pages linking to a given title.
+ * Small numbers of links will be done immediately, large numbers are pushed onto
+ * the job queue.
+ *
+ * This class is designed to work efficiently with small numbers of links, and
+ * to work reasonably well with up to ~10^5 links. Above ~10^6 links, the memory
+ * and time requirements of loading all backlinked IDs in doUpdate() might become
+ * prohibitive. The requirements measured at Wikimedia are approximately:
+ *
+ * memory: 48 bytes per row
+ * time: 16us per row for the query plus processing
+ *
+ * The reason this query is done is to support partitioning of the job
+ * by backlinked ID. The memory issue could be allieviated by doing this query in
+ * batches, but of course LIMIT with an offset is inefficient on the DB side.
+ *
+ * The class is nevertheless a vast improvement on the previous method of using
+ * Image::getLinksTo() and Title::touchArray(), which uses about 2KB of memory per
+ * link.
+ */
+class HTMLCacheUpdate
+{
+ public $mTitle, $mTable, $mPrefix;
+ public $mRowsPerJob, $mRowsPerQuery;
+
+ function __construct( $titleTo, $table ) {
+ global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
+
+ $this->mTitle = $titleTo;
+ $this->mTable = $table;
+ $this->mRowsPerJob = $wgUpdateRowsPerJob;
+ $this->mRowsPerQuery = $wgUpdateRowsPerQuery;
+ }
+
+ function doUpdate() {
+ # Fetch the IDs
+ $cond = $this->getToCondition();
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ );
+ $resWrap = new ResultWrapper( $dbr, $res );
+ if ( $dbr->numRows( $res ) != 0 ) {
+ if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) {
+ $this->insertJobs( $resWrap );
+ } else {
+ $this->invalidateIDs( $resWrap );
+ }
+ }
+ $dbr->freeResult( $res );
+ }
+
+ function insertJobs( ResultWrapper $res ) {
+ $numRows = $res->numRows();
+ $numBatches = ceil( $numRows / $this->mRowsPerJob );
+ $realBatchSize = $numRows / $numBatches;
+ $boundaries = array();
+ $start = false;
+ $jobs = array();
+ do {
+ for ( $i = 0; $i < $realBatchSize - 1; $i++ ) {
+ $row = $res->fetchRow();
+ if ( $row ) {
+ $id = $row[0];
+ } else {
+ $id = false;
+ break;
+ }
+ }
+ if ( $id !== false ) {
+ // One less on the end to avoid duplicating the boundary
+ $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, $id - 1 );
+ } else {
+ $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, false );
+ }
+ $jobs[] = $job;
+
+ $start = $id;
+ } while ( $start );
+
+ Job::batchInsert( $jobs );
+ }
+
+ function getPrefix() {
+ static $prefixes = array(
+ 'pagelinks' => 'pl',
+ 'imagelinks' => 'il',
+ 'categorylinks' => 'cl',
+ 'templatelinks' => 'tl',
+
+ # Not needed
+ # 'externallinks' => 'el',
+ # 'langlinks' => 'll'
+ );
+
+ if ( is_null( $this->mPrefix ) ) {
+ $this->mPrefix = $prefixes[$this->mTable];
+ if ( is_null( $this->mPrefix ) ) {
+ throw new MWException( "Invalid table type \"{$this->mTable}\" in " . __CLASS__ );
+ }
+ }
+ return $this->mPrefix;
+ }
+
+ function getFromField() {
+ return $this->getPrefix() . '_from';
+ }
+
+ function getToCondition() {
+ switch ( $this->mTable ) {
+ case 'pagelinks':
+ return array(
+ 'pl_namespace' => $this->mTitle->getNamespace(),
+ 'pl_title' => $this->mTitle->getDBkey()
+ );
+ case 'templatelinks':
+ return array(
+ 'tl_namespace' => $this->mTitle->getNamespace(),
+ 'tl_title' => $this->mTitle->getDBkey()
+ );
+ case 'imagelinks':
+ return array( 'il_to' => $this->mTitle->getDBkey() );
+ case 'categorylinks':
+ return array( 'cl_to' => $this->mTitle->getDBkey() );
+ }
+ throw new MWException( 'Invalid table type in ' . __CLASS__ );
+ }
+
+ /**
+ * Invalidate a set of IDs, right now
+ */
+ function invalidateIDs( ResultWrapper $res ) {
+ global $wgUseFileCache, $wgUseSquid;
+
+ if ( $res->numRows() == 0 ) {
+ return;
+ }
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $timestamp = $dbw->timestamp();
+ $done = false;
+
+ while ( !$done ) {
+ # Get all IDs in this query into an array
+ $ids = array();
+ for ( $i = 0; $i < $this->mRowsPerQuery; $i++ ) {
+ $row = $res->fetchRow();
+ if ( $row ) {
+ $ids[] = $row[0];
+ } else {
+ $done = true;
+ break;
+ }
+ }
+
+ if ( !count( $ids ) ) {
+ break;
+ }
+
+ # Update page_touched
+ $dbw->update( 'page',
+ array( 'page_touched' => $timestamp ),
+ array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ),
+ __METHOD__
+ );
+
+ # Update squid
+ if ( $wgUseSquid || $wgUseFileCache ) {
+ $titles = Title::newFromIDs( $ids );
+ if ( $wgUseSquid ) {
+ $u = SquidUpdate::newFromTitles( $titles );
+ $u->doUpdate();
+ }
+
+ # Update file cache
+ if ( $wgUseFileCache ) {
+ foreach ( $titles as $title ) {
+ $cm = new CacheManager($title);
+ @unlink($cm->fileCacheName());
+ }
+ }
+ }
+ }
+ }
+}
+
+class HTMLCacheUpdateJob extends Job {
+ var $table, $start, $end;
+
+ /**
+ * Construct a job
+ * @param Title $title The title linked to
+ * @param string $table The name of the link table.
+ * @param integer $start Beginning page_id or false for open interval
+ * @param integer $end End page_id or false for open interval
+ * @param integer $id job_id
+ */
+ function __construct( $title, $table, $start, $end, $id = 0 ) {
+ $params = array(
+ 'table' => $table,
+ 'start' => $start,
+ 'end' => $end );
+ parent::__construct( 'htmlCacheUpdate', $title, $params, $id );
+ $this->table = $table;
+ $this->start = intval( $start );
+ $this->end = intval( $end );
+ }
+
+ function run() {
+ $update = new HTMLCacheUpdate( $this->title, $this->table );
+
+ $fromField = $update->getFromField();
+ $conds = $update->getToCondition();
+ if ( $this->start ) {
+ $conds[] = "$fromField >= {$this->start}";
+ }
+ if ( $this->end ) {
+ $conds[] = "$fromField <= {$this->end}";
+ }
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ );
+ $update->invalidateIDs( new ResultWrapper( $dbr, $res ) );
+ $dbr->freeResult( $res );
+
+ return true;
+ }
+}
+?>
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php
new file mode 100644
index 00000000..c3d74b20
--- /dev/null
+++ b/includes/HTMLForm.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * This file contain a class to easily build HTML forms as well as custom
+ * functions used by SpecialUserrights.php
+ * @package MediaWiki
+ */
+
+/**
+ * Class to build various forms
+ *
+ * @package MediaWiki
+ * @author jeluf, hashar
+ */
+class HTMLForm {
+ /** name of our form. Used as prefix for labels */
+ var $mName, $mRequest;
+
+ function HTMLForm( &$request ) {
+ $this->mRequest = $request;
+ }
+
+ /**
+ * @private
+ * @param $name String: name of the fieldset.
+ * @param $content String: HTML content to put in.
+ * @return string HTML fieldset
+ */
+ function fieldset( $name, $content ) {
+ return "<fieldset><legend>".wfMsg($this->mName.'-'.$name)."</legend>\n" .
+ $content . "\n</fieldset>\n";
+ }
+
+ /**
+ * @private
+ * @param $varname String: name of the checkbox.
+ * @param $checked Boolean: set true to check the box (default False).
+ */
+ function checkbox( $varname, $checked=false ) {
+ if ( $this->mRequest->wasPosted() && !is_null( $this->mRequest->getVal( $varname ) ) ) {
+ $checked = $this->mRequest->getCheck( $varname );
+ }
+ return "<div><input type='checkbox' value=\"1\" id=\"{$varname}\" name=\"wpOp{$varname}\"" .
+ ( $checked ? ' checked="checked"' : '' ) .
+ " /><label for=\"{$varname}\">". wfMsg( $this->mName.'-'.$varname ) .
+ "</label></div>\n";
+ }
+
+ /**
+ * @private
+ * @param $varname String: name of the textbox.
+ * @param $value String: optional value (default empty)
+ * @param $size Integer: optional size of the textbox (default 20)
+ */
+ function textbox( $varname, $value='', $size=20 ) {
+ if ( $this->mRequest->wasPosted() ) {
+ $value = $this->mRequest->getText( $varname, $value );
+ }
+ $value = htmlspecialchars( $value );
+ return "<div><label>". wfMsg( $this->mName.'-'.$varname ) .
+ "<input type='text' name=\"{$varname}\" value=\"{$value}\" size=\"{$size}\" /></label></div>\n";
+ }
+
+ /**
+ * @private
+ * @param $varname String: name of the radiobox.
+ * @param $fields Array: Various fields.
+ */
+ function radiobox( $varname, $fields ) {
+ foreach ( $fields as $value => $checked ) {
+ $s .= "<div><label><input type='radio' name=\"{$varname}\" value=\"{$value}\"" .
+ ( $checked ? ' checked="checked"' : '' ) . " />" . wfMsg( $this->mName.'-'.$varname.'-'.$value ) .
+ "</label></div>\n";
+ }
+ return $this->fieldset( $this->mName.'-'.$varname, $s );
+ }
+
+ /**
+ * @private
+ * @param $varname String: name of the textareabox.
+ * @param $value String: optional value (default empty)
+ * @param $size Integer: optional size of the textarea (default 20)
+ */
+ function textareabox ( $varname, $value='', $size=20 ) {
+ if ( $this->mRequest->wasPosted() ) {
+ $value = $this->mRequest->getText( $varname, $value );
+ }
+ $value = htmlspecialchars( $value );
+ return '<div><label>'.wfMsg( $this->mName.'-'.$varname ).
+ "<textarea name=\"{$varname}\" rows=\"5\" cols=\"{$size}\">$value</textarea></label></div>\n";
+ }
+
+ /**
+ * @private
+ * @param $varname String: name of the arraybox.
+ * @param $size Integer: Optional size of the textarea (default 20)
+ */
+ function arraybox( $varname , $size=20 ) {
+ $s = '';
+ if ( $this->mRequest->wasPosted() ) {
+ $arr = $this->mRequest->getArray( $varname );
+ if ( is_array( $arr ) ) {
+ foreach ( $_POST[$varname] as $index => $element ) {
+ $s .= htmlspecialchars( $element )."\n";
+ }
+ }
+ }
+ return "<div><label>".wfMsg( $this->mName.'-'.$varname ).
+ "<textarea name=\"{$varname}\" rows=\"5\" cols=\"{$size}\">{$s}</textarea>\n";
+ }
+} // end class
+
+
+// functions used by SpecialUserrights.php
+
+/** Build a select with all defined groups
+ * @param $selectname String: name of this element. Name of form is automaticly prefixed.
+ * @param $selectmsg String: FIXME
+ * @param $selected Array: array of element selected when posted. Only multiples will show them.
+ * @param $multiple Boolean: A multiple elements select.
+ * @param $size Integer: number of elements to be shown ignored for non-multiple (default 6).
+ * @param $reverse Boolean: if true, multiple select will hide selected elements (default false).
+ * @todo Document $selectmsg
+*/
+function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple=false, $size=6, $reverse=false) {
+ $groups = User::getAllGroups();
+ $out = htmlspecialchars( wfMsg( $selectmsg ) );
+
+ if( $multiple ) {
+ $attribs = array(
+ 'name' => $selectname . '[]',
+ 'multiple'=> 'multiple',
+ 'size' => $size );
+ } else {
+ $attribs = array( 'name' => $selectname );
+ }
+ $out .= wfElement( 'select', $attribs, null );
+
+ foreach( $groups as $group ) {
+ $attribs = array( 'value' => $group );
+ if( $multiple ) {
+ // for multiple will only show the things we want
+ if( !in_array( $group, $selected ) xor $reverse ) {
+ continue;
+ }
+ } else {
+ if( in_array( $group, $selected ) ) {
+ $attribs['selected'] = 'selected';
+ }
+ }
+ $out .= wfElement( 'option', $attribs, User::getGroupName( $group ) ) . "\n";
+ }
+
+ $out .= "</select>\n";
+ return $out;
+}
+
+/** Build a select with all existent rights
+ * @param $selected Array: Names(?) of user rights that should be selected.
+ * @return string HTML select.
+ */
+function HTMLSelectRights($selected='') {
+ global $wgAvailableRights;
+ $out = '<select name="editgroup-getrights[]" multiple="multiple">';
+ $groupRights = explode(',',$selected);
+
+ foreach($wgAvailableRights as $right) {
+
+ // check box when right exist
+ if(in_array($right, $groupRights)) { $selected = 'selected="selected" '; }
+ else { $selected = ''; }
+
+ $out .= '<option value="'.$right.'" '.$selected.'>'.$right."</option>\n";
+ }
+ $out .= "</select>\n";
+ return $out;
+}
+?>
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
new file mode 100644
index 00000000..8f5d3624
--- /dev/null
+++ b/includes/HistoryBlob.php
@@ -0,0 +1,308 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Pure virtual parent
+ * @package MediaWiki
+ */
+class HistoryBlob
+{
+ /**
+ * setMeta and getMeta currently aren't used for anything, I just thought
+ * they might be useful in the future.
+ * @param $meta String: a single string.
+ */
+ function setMeta( $meta ) {}
+
+ /**
+ * setMeta and getMeta currently aren't used for anything, I just thought
+ * they might be useful in the future.
+ * Gets the meta-value
+ */
+ function getMeta() {}
+
+ /**
+ * Adds an item of text, returns a stub object which points to the item.
+ * You must call setLocation() on the stub object before storing it to the
+ * database
+ */
+ function addItem() {}
+
+ /**
+ * Get item by hash
+ */
+ function getItem( $hash ) {}
+
+ # Set the "default text"
+ # This concept is an odd property of the current DB schema, whereby each text item has a revision
+ # associated with it. The default text is the text of the associated revision. There may, however,
+ # be other revisions in the same object
+ function setText() {}
+
+ /**
+ * Get default text. This is called from Revision::getRevisionText()
+ */
+ function getText() {}
+}
+
+/**
+ * The real object
+ * @package MediaWiki
+ */
+class ConcatenatedGzipHistoryBlob extends HistoryBlob
+{
+ /* private */ var $mVersion = 0, $mCompressed = false, $mItems = array(), $mDefaultHash = '';
+ /* private */ var $mFast = 0, $mSize = 0;
+
+ function ConcatenatedGzipHistoryBlob() {
+ if ( !function_exists( 'gzdeflate' ) ) {
+ throw new MWException( "Need zlib support to read or write this kind of history object (ConcatenatedGzipHistoryBlob)\n" );
+ }
+ }
+
+ /** @todo document */
+ function setMeta( $metaData ) {
+ $this->uncompress();
+ $this->mItems['meta'] = $metaData;
+ }
+
+ /** @todo document */
+ function getMeta() {
+ $this->uncompress();
+ return $this->mItems['meta'];
+ }
+
+ /** @todo document */
+ function addItem( $text ) {
+ $this->uncompress();
+ $hash = md5( $text );
+ $this->mItems[$hash] = $text;
+ $this->mSize += strlen( $text );
+
+ $stub = new HistoryBlobStub( $hash );
+ return $stub;
+ }
+
+ /** @todo document */
+ function getItem( $hash ) {
+ $this->uncompress();
+ if ( array_key_exists( $hash, $this->mItems ) ) {
+ return $this->mItems[$hash];
+ } else {
+ return false;
+ }
+ }
+
+ /** @todo document */
+ function removeItem( $hash ) {
+ $this->mSize -= strlen( $this->mItems[$hash] );
+ unset( $this->mItems[$hash] );
+ }
+
+ /** @todo document */
+ function compress() {
+ if ( !$this->mCompressed ) {
+ $this->mItems = gzdeflate( serialize( $this->mItems ) );
+ $this->mCompressed = true;
+ }
+ }
+
+ /** @todo document */
+ function uncompress() {
+ if ( $this->mCompressed ) {
+ $this->mItems = unserialize( gzinflate( $this->mItems ) );
+ $this->mCompressed = false;
+ }
+ }
+
+ /** @todo document */
+ function getText() {
+ $this->uncompress();
+ return $this->getItem( $this->mDefaultHash );
+ }
+
+ /** @todo document */
+ function setText( $text ) {
+ $this->uncompress();
+ $stub = $this->addItem( $text );
+ $this->mDefaultHash = $stub->mHash;
+ }
+
+ /** @todo document */
+ function __sleep() {
+ $this->compress();
+ return array( 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' );
+ }
+
+ /** @todo document */
+ function __wakeup() {
+ $this->uncompress();
+ }
+
+ /**
+ * Determines if this object is happy
+ */
+ function isHappy( $maxFactor, $factorThreshold ) {
+ if ( count( $this->mItems ) == 0 ) {
+ return true;
+ }
+ if ( !$this->mFast ) {
+ $this->uncompress();
+ $record = serialize( $this->mItems );
+ $size = strlen( $record );
+ $avgUncompressed = $size / count( $this->mItems );
+ $compressed = strlen( gzdeflate( $record ) );
+
+ if ( $compressed < $factorThreshold * 1024 ) {
+ return true;
+ } else {
+ return $avgUncompressed * $maxFactor < $compressed;
+ }
+ } else {
+ return count( $this->mItems ) <= 10;
+ }
+ }
+}
+
+
+/**
+ * One-step cache variable to hold base blobs; operations that
+ * pull multiple revisions may often pull multiple times from
+ * the same blob. By keeping the last-used one open, we avoid
+ * redundant unserialization and decompression overhead.
+ */
+global $wgBlobCache;
+$wgBlobCache = array();
+
+
+/**
+ * @package MediaWiki
+ */
+class HistoryBlobStub {
+ var $mOldId, $mHash, $mRef;
+
+ /** @todo document */
+ function HistoryBlobStub( $hash = '', $oldid = 0 ) {
+ $this->mHash = $hash;
+ }
+
+ /**
+ * Sets the location (old_id) of the main object to which this object
+ * points
+ */
+ function setLocation( $id ) {
+ $this->mOldId = $id;
+ }
+
+ /**
+ * Sets the location (old_id) of the referring object
+ */
+ function setReferrer( $id ) {
+ $this->mRef = $id;
+ }
+
+ /**
+ * Gets the location of the referring object
+ */
+ function getReferrer() {
+ return $this->mRef;
+ }
+
+ /** @todo document */
+ function getText() {
+ $fname = 'HistoryBlob::getText';
+ global $wgBlobCache;
+ if( isset( $wgBlobCache[$this->mOldId] ) ) {
+ $obj = $wgBlobCache[$this->mOldId];
+ } else {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'text', array( 'old_flags', 'old_text' ), array( 'old_id' => $this->mOldId ) );
+ if( !$row ) {
+ return false;
+ }
+ $flags = explode( ',', $row->old_flags );
+ if( in_array( 'external', $flags ) ) {
+ $url=$row->old_text;
+ @list($proto,$path)=explode('://',$url,2);
+ if ($path=="") {
+ wfProfileOut( $fname );
+ return false;
+ }
+ require_once('ExternalStore.php');
+ $row->old_text=ExternalStore::fetchFromUrl($url);
+
+ }
+ if( !in_array( 'object', $flags ) ) {
+ return false;
+ }
+
+ if( in_array( 'gzip', $flags ) ) {
+ // This shouldn't happen, but a bug in the compress script
+ // may at times gzip-compress a HistoryBlob object row.
+ $obj = unserialize( gzinflate( $row->old_text ) );
+ } else {
+ $obj = unserialize( $row->old_text );
+ }
+
+ if( !is_object( $obj ) ) {
+ // Correct for old double-serialization bug.
+ $obj = unserialize( $obj );
+ }
+
+ // Save this item for reference; if pulling many
+ // items in a row we'll likely use it again.
+ $obj->uncompress();
+ $wgBlobCache = array( $this->mOldId => $obj );
+ }
+ return $obj->getItem( $this->mHash );
+ }
+
+ /** @todo document */
+ function getHash() {
+ return $this->mHash;
+ }
+}
+
+
+/**
+ * To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the
+ * leftover cur table as the backend. This avoids expensively copying hundreds
+ * of megabytes of data during the conversion downtime.
+ *
+ * Serialized HistoryBlobCurStub objects will be inserted into the text table
+ * on conversion if $wgFastSchemaUpgrades is set to true.
+ *
+ * @package MediaWiki
+ */
+class HistoryBlobCurStub {
+ var $mCurId;
+
+ /** @todo document */
+ function HistoryBlobCurStub( $curid = 0 ) {
+ $this->mCurId = $curid;
+ }
+
+ /**
+ * Sets the location (cur_id) of the main object to which this object
+ * points
+ */
+ function setLocation( $id ) {
+ $this->mCurId = $id;
+ }
+
+ /** @todo document */
+ function getText() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) );
+ if( !$row ) {
+ return false;
+ }
+ return $row->cur_text;
+ }
+}
+
+
+?>
diff --git a/includes/Hooks.php b/includes/Hooks.php
new file mode 100644
index 00000000..4daffaf3
--- /dev/null
+++ b/includes/Hooks.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Hooks.php -- a tool for running hook functions
+ * Copyright 2004, 2005 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 Prodromou <evan@wikitravel.org>
+ * @package MediaWiki
+ * @see hooks.txt
+ */
+
+
+/**
+ * Because programmers assign to $wgHooks, we need to be very
+ * careful about its contents. So, there's a lot more error-checking
+ * in here than would normally be necessary.
+ */
+function wfRunHooks($event, $args = null) {
+
+ global $wgHooks;
+ $fname = 'wfRunHooks';
+
+ if (!is_array($wgHooks)) {
+ throw new MWException("Global hooks array is not an array!\n");
+ return false;
+ }
+
+ if (!array_key_exists($event, $wgHooks)) {
+ return true;
+ }
+
+ if (!is_array($wgHooks[$event])) {
+ throw new MWException("Hooks array for event '$event' is not an array!\n");
+ return false;
+ }
+
+ foreach ($wgHooks[$event] as $index => $hook) {
+
+ $object = NULL;
+ $method = NULL;
+ $func = NULL;
+ $data = NULL;
+ $have_data = false;
+
+ /* $hook can be: a function, an object, an array of $function and $data,
+ * an array of just a function, an array of object and method, or an
+ * array of object, method, and data.
+ */
+
+ if (is_array($hook)) {
+ if (count($hook) < 1) {
+ throw new MWException("Empty array in hooks for " . $event . "\n");
+ } else if (is_object($hook[0])) {
+ $object =& $wgHooks[$event][$index][0];
+ if (count($hook) < 2) {
+ $method = "on" . $event;
+ } else {
+ $method = $hook[1];
+ if (count($hook) > 2) {
+ $data = $hook[2];
+ $have_data = true;
+ }
+ }
+ } else if (is_string($hook[0])) {
+ $func = $hook[0];
+ if (count($hook) > 1) {
+ $data = $hook[1];
+ $have_data = true;
+ }
+ } else {
+ var_dump( $wgHooks );
+ throw new MWException("Unknown datatype in hooks for " . $event . "\n");
+ }
+ } else if (is_string($hook)) { # functions look like strings, too
+ $func = $hook;
+ } else if (is_object($hook)) {
+ $object =& $wgHooks[$event][$index];
+ $method = "on" . $event;
+ } else {
+ throw new MWException("Unknown datatype in hooks for " . $event . "\n");
+ }
+
+ /* We put the first data element on, if needed. */
+
+ if ($have_data) {
+ $hook_args = array_merge(array($data), $args);
+ } else {
+ $hook_args = $args;
+ }
+
+
+ if ( isset( $object ) ) {
+ $func = get_class( $object ) . '::' . $method;
+ }
+
+ /* Call the hook. */
+ wfProfileIn( $func );
+ if( isset( $object ) ) {
+ $retval = call_user_func_array(array(&$object, $method), $hook_args);
+ } else {
+ $retval = call_user_func_array($func, $hook_args);
+ }
+ wfProfileOut( $func );
+
+ /* String return is an error; false return means stop processing. */
+
+ if (is_string($retval)) {
+ global $wgOut;
+ $wgOut->showFatalError($retval);
+ return false;
+ } else if (!$retval) {
+ return false;
+ }
+ }
+
+ return true;
+}
+?>
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
new file mode 100644
index 00000000..a9fb13ca
--- /dev/null
+++ b/includes/HttpFunctions.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * Various HTTP related functions
+ */
+class Http {
+ /**
+ * Get the contents of a file by HTTP
+ *
+ * if $timeout is 'default', $wgHTTPTimeout is used
+ */
+ static function get( $url, $timeout = 'default' ) {
+ global $wgHTTPTimeout, $wgHTTPProxy, $wgVersion, $wgTitle;
+
+ # Use curl if available
+ if ( function_exists( 'curl_init' ) ) {
+ $c = curl_init( $url );
+ if ( wfIsLocalURL( $url ) ) {
+ curl_setopt( $c, CURLOPT_PROXY, 'localhost:80' );
+ } else if ($wgHTTPProxy) {
+ curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy);
+ }
+
+ if ( $timeout == 'default' ) {
+ $timeout = $wgHTTPTimeout;
+ }
+ curl_setopt( $c, CURLOPT_TIMEOUT, $timeout );
+ curl_setopt( $c, CURLOPT_USERAGENT, "MediaWiki/$wgVersion" );
+
+ # Set the referer to $wgTitle, even in command-line mode
+ # This is useful for interwiki transclusion, where the foreign
+ # server wants to know what the referring page is.
+ # $_SERVER['REQUEST_URI'] gives a less reliable indication of the
+ # referring page.
+ if ( is_object( $wgTitle ) ) {
+ curl_setopt( $c, CURLOPT_REFERER, $wgTitle->getFullURL() );
+ }
+
+ ob_start();
+ curl_exec( $c );
+ $text = ob_get_contents();
+ ob_end_clean();
+
+ # Don't return the text of error messages, return false on error
+ if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) {
+ $text = false;
+ }
+ curl_close( $c );
+ } else {
+ # Otherwise use file_get_contents, or its compatibility function from GlobalFunctions.php
+ # This may take 3 minutes to time out, and doesn't have local fetch capabilities
+ $url_fopen = ini_set( 'allow_url_fopen', 1 );
+ $text = file_get_contents( $url );
+ ini_set( 'allow_url_fopen', $url_fopen );
+ }
+ return $text;
+ }
+
+ /**
+ * Check if the URL can be served by localhost
+ */
+ static function isLocalURL( $url ) {
+ global $wgCommandLineMode, $wgConf;
+ if ( $wgCommandLineMode ) {
+ return false;
+ }
+
+ // Extract host part
+ $matches = array();
+ if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
+ $host = $matches[1];
+ // Split up dotwise
+ $domainParts = explode( '.', $host );
+ // Check if this domain or any superdomain is listed in $wgConf as a local virtual host
+ $domainParts = array_reverse( $domainParts );
+ for ( $i = 0; $i < count( $domainParts ); $i++ ) {
+ $domainPart = $domainParts[$i];
+ if ( $i == 0 ) {
+ $domain = $domainPart;
+ } else {
+ $domain = $domainPart . '.' . $domain;
+ }
+ if ( $wgConf->isLocalVHost( $domain ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
+?>
diff --git a/includes/Image.php b/includes/Image.php
new file mode 100644
index 00000000..185d732a
--- /dev/null
+++ b/includes/Image.php
@@ -0,0 +1,2265 @@
+<?php
+/**
+ * @package MediaWiki
+ */
+
+/**
+ * NOTE FOR WINDOWS USERS:
+ * To enable EXIF functions, add the folloing lines to the
+ * "Windows extensions" section of php.ini:
+ *
+ * extension=extensions/php_mbstring.dll
+ * extension=extensions/php_exif.dll
+ */
+
+/**
+ * Bump this number when serialized cache records may be incompatible.
+ */
+define( 'MW_IMAGE_VERSION', 1 );
+
+/**
+ * Class to represent an image
+ *
+ * Provides methods to retrieve paths (physical, logical, URL),
+ * to generate thumbnails or for uploading.
+ * @package MediaWiki
+ */
+class Image
+{
+ /**#@+
+ * @private
+ */
+ var $name, # name of the image (constructor)
+ $imagePath, # Path of the image (loadFromXxx)
+ $url, # Image URL (accessor)
+ $title, # Title object for this image (constructor)
+ $fileExists, # does the image file exist on disk? (loadFromXxx)
+ $fromSharedDirectory, # load this image from $wgSharedUploadDirectory (loadFromXxx)
+ $historyLine, # Number of line to return by nextHistoryLine() (constructor)
+ $historyRes, # result of the query for the image's history (nextHistoryLine)
+ $width, # \
+ $height, # |
+ $bits, # --- returned by getimagesize (loadFromXxx)
+ $attr, # /
+ $type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
+ $mime, # MIME type, determined by MimeMagic::guessMimeType
+ $size, # Size in bytes (loadFromXxx)
+ $metadata, # Metadata
+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+ $lastError; # Error string associated with a thumbnail display error
+
+
+ /**#@-*/
+
+ /**
+ * Create an Image object from an image name
+ *
+ * @param string $name name of the image, used to create a title object using Title::makeTitleSafe
+ * @public
+ */
+ function newFromName( $name ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $name );
+ if ( is_object( $title ) ) {
+ return new Image( $title );
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Obsolete factory function, use constructor
+ * @deprecated
+ */
+ function newFromTitle( $title ) {
+ return new Image( $title );
+ }
+
+ function Image( $title ) {
+ if( !is_object( $title ) ) {
+ throw new MWException( 'Image constructor given bogus title.' );
+ }
+ $this->title =& $title;
+ $this->name = $title->getDBkey();
+ $this->metadata = serialize ( array() ) ;
+
+ $n = strrpos( $this->name, '.' );
+ $this->extension = Image::normalizeExtension( $n ?
+ substr( $this->name, $n + 1 ) : '' );
+ $this->historyLine = 0;
+
+ $this->dataLoaded = false;
+ }
+
+
+ /**
+ * Normalize a file extension to the common form, and ensure it's clean.
+ * Extensions with non-alphanumeric characters will be discarded.
+ *
+ * @param $ext string (without the .)
+ * @return string
+ */
+ static function normalizeExtension( $ext ) {
+ $lower = strtolower( $ext );
+ $squish = array(
+ 'htm' => 'html',
+ 'jpeg' => 'jpg',
+ 'mpeg' => 'mpg',
+ 'tiff' => 'tif' );
+ if( isset( $squish[$lower] ) ) {
+ return $squish[$lower];
+ } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
+ return $lower;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the memcached keys
+ * Returns an array, first element is the local cache key, second is the shared cache key, if there is one
+ */
+ function getCacheKeys( ) {
+ global $wgDBname, $wgUseSharedUploads, $wgSharedUploadDBname, $wgCacheSharedUploads;
+
+ $hashedName = md5($this->name);
+ $keys = array( "$wgDBname:Image:$hashedName" );
+ if ( $wgUseSharedUploads && $wgSharedUploadDBname && $wgCacheSharedUploads ) {
+ $keys[] = "$wgSharedUploadDBname:Image:$hashedName";
+ }
+ return $keys;
+ }
+
+ /**
+ * Try to load image metadata from memcached. Returns true on success.
+ */
+ function loadFromCache() {
+ global $wgUseSharedUploads, $wgMemc;
+ wfProfileIn( __METHOD__ );
+ $this->dataLoaded = false;
+ $keys = $this->getCacheKeys();
+ $cachedValues = $wgMemc->get( $keys[0] );
+
+ // Check if the key existed and belongs to this version of MediaWiki
+ if (!empty($cachedValues) && is_array($cachedValues)
+ && isset($cachedValues['version']) && ( $cachedValues['version'] == MW_IMAGE_VERSION )
+ && $cachedValues['fileExists'] && isset( $cachedValues['mime'] ) && isset( $cachedValues['metadata'] ) )
+ {
+ if ( $wgUseSharedUploads && $cachedValues['fromShared']) {
+ # if this is shared file, we need to check if image
+ # in shared repository has not changed
+ if ( isset( $keys[1] ) ) {
+ $commonsCachedValues = $wgMemc->get( $keys[1] );
+ if (!empty($commonsCachedValues) && is_array($commonsCachedValues)
+ && isset($commonsCachedValues['version'])
+ && ( $commonsCachedValues['version'] == MW_IMAGE_VERSION )
+ && isset($commonsCachedValues['mime'])) {
+ wfDebug( "Pulling image metadata from shared repository cache\n" );
+ $this->name = $commonsCachedValues['name'];
+ $this->imagePath = $commonsCachedValues['imagePath'];
+ $this->fileExists = $commonsCachedValues['fileExists'];
+ $this->width = $commonsCachedValues['width'];
+ $this->height = $commonsCachedValues['height'];
+ $this->bits = $commonsCachedValues['bits'];
+ $this->type = $commonsCachedValues['type'];
+ $this->mime = $commonsCachedValues['mime'];
+ $this->metadata = $commonsCachedValues['metadata'];
+ $this->size = $commonsCachedValues['size'];
+ $this->fromSharedDirectory = true;
+ $this->dataLoaded = true;
+ $this->imagePath = $this->getFullPath(true);
+ }
+ }
+ } else {
+ wfDebug( "Pulling image metadata from local cache\n" );
+ $this->name = $cachedValues['name'];
+ $this->imagePath = $cachedValues['imagePath'];
+ $this->fileExists = $cachedValues['fileExists'];
+ $this->width = $cachedValues['width'];
+ $this->height = $cachedValues['height'];
+ $this->bits = $cachedValues['bits'];
+ $this->type = $cachedValues['type'];
+ $this->mime = $cachedValues['mime'];
+ $this->metadata = $cachedValues['metadata'];
+ $this->size = $cachedValues['size'];
+ $this->fromSharedDirectory = false;
+ $this->dataLoaded = true;
+ $this->imagePath = $this->getFullPath();
+ }
+ }
+ if ( $this->dataLoaded ) {
+ wfIncrStats( 'image_cache_hit' );
+ } else {
+ wfIncrStats( 'image_cache_miss' );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $this->dataLoaded;
+ }
+
+ /**
+ * Save the image metadata to memcached
+ */
+ function saveToCache() {
+ global $wgMemc;
+ $this->load();
+ $keys = $this->getCacheKeys();
+ if ( $this->fileExists ) {
+ // We can't cache negative metadata for non-existent files,
+ // because if the file later appears in commons, the local
+ // keys won't be purged.
+ $cachedValues = array(
+ 'version' => MW_IMAGE_VERSION,
+ 'name' => $this->name,
+ 'imagePath' => $this->imagePath,
+ 'fileExists' => $this->fileExists,
+ 'fromShared' => $this->fromSharedDirectory,
+ 'width' => $this->width,
+ 'height' => $this->height,
+ 'bits' => $this->bits,
+ 'type' => $this->type,
+ 'mime' => $this->mime,
+ 'metadata' => $this->metadata,
+ 'size' => $this->size );
+
+ $wgMemc->set( $keys[0], $cachedValues, 60 * 60 * 24 * 7 ); // A week
+ } else {
+ // However we should clear them, so they aren't leftover
+ // if we've deleted the file.
+ $wgMemc->delete( $keys[0] );
+ }
+ }
+
+ /**
+ * Load metadata from the file itself
+ */
+ function loadFromFile() {
+ global $wgUseSharedUploads, $wgSharedUploadDirectory, $wgContLang, $wgShowEXIF;
+ wfProfileIn( __METHOD__ );
+ $this->imagePath = $this->getFullPath();
+ $this->fileExists = file_exists( $this->imagePath );
+ $this->fromSharedDirectory = false;
+ $gis = array();
+
+ if (!$this->fileExists) wfDebug(__METHOD__.': '.$this->imagePath." not found locally!\n");
+
+ # If the file is not found, and a shared upload directory is used, look for it there.
+ if (!$this->fileExists && $wgUseSharedUploads && $wgSharedUploadDirectory) {
+ # In case we're on a wgCapitalLinks=false wiki, we
+ # capitalize the first letter of the filename before
+ # looking it up in the shared repository.
+ $sharedImage = Image::newFromName( $wgContLang->ucfirst($this->name) );
+ $this->fileExists = $sharedImage && file_exists( $sharedImage->getFullPath(true) );
+ if ( $this->fileExists ) {
+ $this->name = $sharedImage->name;
+ $this->imagePath = $this->getFullPath(true);
+ $this->fromSharedDirectory = true;
+ }
+ }
+
+
+ if ( $this->fileExists ) {
+ $magic=& wfGetMimeMagic();
+
+ $this->mime = $magic->guessMimeType($this->imagePath,true);
+ $this->type = $magic->getMediaType($this->imagePath,$this->mime);
+
+ # Get size in bytes
+ $this->size = filesize( $this->imagePath );
+
+ $magic=& wfGetMimeMagic();
+
+ # Height and width
+ wfSuppressWarnings();
+ if( $this->mime == 'image/svg' ) {
+ $gis = wfGetSVGsize( $this->imagePath );
+ } elseif( $this->mime == 'image/vnd.djvu' ) {
+ $deja = new DjVuImage( $this->imagePath );
+ $gis = $deja->getImageSize();
+ } elseif ( !$magic->isPHPImageType( $this->mime ) ) {
+ # Don't try to get the width and height of sound and video files, that's bad for performance
+ $gis = false;
+ } else {
+ $gis = getimagesize( $this->imagePath );
+ }
+ wfRestoreWarnings();
+
+ wfDebug(__METHOD__.': '.$this->imagePath." loaded, ".$this->size." bytes, ".$this->mime.".\n");
+ }
+ else {
+ $this->mime = NULL;
+ $this->type = MEDIATYPE_UNKNOWN;
+ wfDebug(__METHOD__.': '.$this->imagePath." NOT FOUND!\n");
+ }
+
+ if( $gis ) {
+ $this->width = $gis[0];
+ $this->height = $gis[1];
+ } else {
+ $this->width = 0;
+ $this->height = 0;
+ }
+
+ #NOTE: $gis[2] contains a code for the image type. This is no longer used.
+
+ #NOTE: we have to set this flag early to avoid load() to be called
+ # be some of the functions below. This may lead to recursion or other bad things!
+ # as ther's only one thread of execution, this should be safe anyway.
+ $this->dataLoaded = true;
+
+
+ $this->metadata = serialize( $this->retrieveExifData( $this->imagePath ) );
+
+ if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits'];
+ else $this->bits = 0;
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Load image metadata from the DB
+ */
+ function loadFromDB() {
+ global $wgUseSharedUploads, $wgSharedUploadDBname, $wgSharedUploadDBprefix, $wgContLang;
+ wfProfileIn( __METHOD__ );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $this->checkDBSchema($dbr);
+
+ $row = $dbr->selectRow( 'image',
+ array( 'img_size', 'img_width', 'img_height', 'img_bits',
+ 'img_media_type', 'img_major_mime', 'img_minor_mime', 'img_metadata' ),
+ array( 'img_name' => $this->name ), __METHOD__ );
+ if ( $row ) {
+ $this->fromSharedDirectory = false;
+ $this->fileExists = true;
+ $this->loadFromRow( $row );
+ $this->imagePath = $this->getFullPath();
+ // Check for rows from a previous schema, quietly upgrade them
+ if ( is_null($this->type) ) {
+ $this->upgradeRow();
+ }
+ } elseif ( $wgUseSharedUploads && $wgSharedUploadDBname ) {
+ # In case we're on a wgCapitalLinks=false wiki, we
+ # capitalize the first letter of the filename before
+ # looking it up in the shared repository.
+ $name = $wgContLang->ucfirst($this->name);
+ $dbc =& wfGetDB( DB_SLAVE, 'commons' );
+
+ $row = $dbc->selectRow( "`$wgSharedUploadDBname`.{$wgSharedUploadDBprefix}image",
+ array(
+ 'img_size', 'img_width', 'img_height', 'img_bits',
+ 'img_media_type', 'img_major_mime', 'img_minor_mime', 'img_metadata' ),
+ array( 'img_name' => $name ), __METHOD__ );
+ if ( $row ) {
+ $this->fromSharedDirectory = true;
+ $this->fileExists = true;
+ $this->imagePath = $this->getFullPath(true);
+ $this->name = $name;
+ $this->loadFromRow( $row );
+
+ // Check for rows from a previous schema, quietly upgrade them
+ if ( is_null($this->type) ) {
+ $this->upgradeRow();
+ }
+ }
+ }
+
+ if ( !$row ) {
+ $this->size = 0;
+ $this->width = 0;
+ $this->height = 0;
+ $this->bits = 0;
+ $this->type = 0;
+ $this->fileExists = false;
+ $this->fromSharedDirectory = false;
+ $this->metadata = serialize ( array() ) ;
+ }
+
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ $this->dataLoaded = true;
+ wfProfileOut( __METHOD__ );
+ }
+
+ /*
+ * Load image metadata from a DB result row
+ */
+ function loadFromRow( &$row ) {
+ $this->size = $row->img_size;
+ $this->width = $row->img_width;
+ $this->height = $row->img_height;
+ $this->bits = $row->img_bits;
+ $this->type = $row->img_media_type;
+
+ $major= $row->img_major_mime;
+ $minor= $row->img_minor_mime;
+
+ if (!$major) $this->mime = "unknown/unknown";
+ else {
+ if (!$minor) $minor= "unknown";
+ $this->mime = $major.'/'.$minor;
+ }
+
+ $this->metadata = $row->img_metadata;
+ if ( $this->metadata == "" ) $this->metadata = serialize ( array() ) ;
+
+ $this->dataLoaded = true;
+ }
+
+ /**
+ * Load image metadata from cache or DB, unless already loaded
+ */
+ function load() {
+ global $wgSharedUploadDBname, $wgUseSharedUploads;
+ if ( !$this->dataLoaded ) {
+ if ( !$this->loadFromCache() ) {
+ $this->loadFromDB();
+ if ( !$wgSharedUploadDBname && $wgUseSharedUploads ) {
+ $this->loadFromFile();
+ } elseif ( $this->fileExists ) {
+ $this->saveToCache();
+ }
+ }
+ $this->dataLoaded = true;
+ }
+ }
+
+ /**
+ * Metadata was loaded from the database, but the row had a marker indicating it needs to be
+ * upgraded from the 1.4 schema, which had no width, height, bits or type. Upgrade the row.
+ */
+ function upgradeRow() {
+ global $wgDBname, $wgSharedUploadDBname;
+ wfProfileIn( __METHOD__ );
+
+ $this->loadFromFile();
+
+ if ( $this->fromSharedDirectory ) {
+ if ( !$wgSharedUploadDBname ) {
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ // Write to the other DB using selectDB, not database selectors
+ // This avoids breaking replication in MySQL
+ $dbw =& wfGetDB( DB_MASTER, 'commons' );
+ $dbw->selectDB( $wgSharedUploadDBname );
+ } else {
+ $dbw =& wfGetDB( DB_MASTER );
+ }
+
+ $this->checkDBSchema($dbw);
+
+ list( $major, $minor ) = self::splitMime( $this->mime );
+
+ wfDebug(__METHOD__.': upgrading '.$this->name." to 1.5 schema\n");
+
+ $dbw->update( 'image',
+ array(
+ 'img_width' => $this->width,
+ 'img_height' => $this->height,
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_metadata' => $this->metadata,
+ ), array( 'img_name' => $this->name ), __METHOD__
+ );
+ if ( $this->fromSharedDirectory ) {
+ $dbw->selectDB( $wgDBname );
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Split an internet media type into its two components; if not
+ * a two-part name, set the minor type to 'unknown'.
+ *
+ * @param $mime "text/html" etc
+ * @return array ("text", "html") etc
+ */
+ static function splitMime( $mime ) {
+ if( strpos( $mime, '/' ) !== false ) {
+ return explode( '/', $mime, 2 );
+ } else {
+ return array( $mime, 'unknown' );
+ }
+ }
+
+ /**
+ * Return the name of this image
+ * @public
+ */
+ function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Return the associated title object
+ * @public
+ */
+ function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Return the URL of the image file
+ * @public
+ */
+ function getURL() {
+ if ( !$this->url ) {
+ $this->load();
+ if($this->fileExists) {
+ $this->url = Image::imageUrl( $this->name, $this->fromSharedDirectory );
+ } else {
+ $this->url = '';
+ }
+ }
+ return $this->url;
+ }
+
+ function getViewURL() {
+ if( $this->mustRender()) {
+ if( $this->canRender() ) {
+ return $this->createThumb( $this->getWidth() );
+ }
+ else {
+ wfDebug('Image::getViewURL(): supposed to render '.$this->name.' ('.$this->mime."), but can't!\n");
+ return $this->getURL(); #hm... return NULL?
+ }
+ } else {
+ return $this->getURL();
+ }
+ }
+
+ /**
+ * Return the image path of the image in the
+ * local file system as an absolute path
+ * @public
+ */
+ function getImagePath() {
+ $this->load();
+ return $this->imagePath;
+ }
+
+ /**
+ * Return the width of the image
+ *
+ * Returns -1 if the file specified is not a known image type
+ * @public
+ */
+ function getWidth() {
+ $this->load();
+ return $this->width;
+ }
+
+ /**
+ * Return the height of the image
+ *
+ * Returns -1 if the file specified is not a known image type
+ * @public
+ */
+ function getHeight() {
+ $this->load();
+ return $this->height;
+ }
+
+ /**
+ * Return the size of the image file, in bytes
+ * @public
+ */
+ function getSize() {
+ $this->load();
+ return $this->size;
+ }
+
+ /**
+ * Returns the mime type of the file.
+ */
+ function getMimeType() {
+ $this->load();
+ return $this->mime;
+ }
+
+ /**
+ * Return the type of the media in the file.
+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
+ */
+ function getMediaType() {
+ $this->load();
+ return $this->type;
+ }
+
+ /**
+ * Checks if the file can be presented to the browser as a bitmap.
+ *
+ * Currently, this checks if the file is an image format
+ * that can be converted to a format
+ * supported by all browsers (namely GIF, PNG and JPEG),
+ * or if it is an SVG image and SVG conversion is enabled.
+ *
+ * @todo remember the result of this check.
+ */
+ function canRender() {
+ global $wgUseImageMagick;
+
+ if( $this->getWidth()<=0 || $this->getHeight()<=0 ) return false;
+
+ $mime= $this->getMimeType();
+
+ if (!$mime || $mime==='unknown' || $mime==='unknown/unknown') return false;
+
+ #if it's SVG, check if there's a converter enabled
+ if ($mime === 'image/svg') {
+ global $wgSVGConverters, $wgSVGConverter;
+
+ if ($wgSVGConverter && isset( $wgSVGConverters[$wgSVGConverter])) {
+ wfDebug( "Image::canRender: SVG is ready!\n" );
+ return true;
+ } else {
+ wfDebug( "Image::canRender: SVG renderer missing\n" );
+ }
+ }
+
+ #image formats available on ALL browsers
+ if ( $mime === 'image/gif'
+ || $mime === 'image/png'
+ || $mime === 'image/jpeg' ) return true;
+
+ #image formats that can be converted to the above formats
+ if ($wgUseImageMagick) {
+ #convertable by ImageMagick (there are more...)
+ if ( $mime === 'image/vnd.wap.wbmp'
+ || $mime === 'image/x-xbitmap'
+ || $mime === 'image/x-xpixmap'
+ #|| $mime === 'image/x-icon' #file may be split into multiple parts
+ || $mime === 'image/x-portable-anymap'
+ || $mime === 'image/x-portable-bitmap'
+ || $mime === 'image/x-portable-graymap'
+ || $mime === 'image/x-portable-pixmap'
+ #|| $mime === 'image/x-photoshop' #this takes a lot of CPU and RAM!
+ || $mime === 'image/x-rgb'
+ || $mime === 'image/x-bmp'
+ || $mime === 'image/tiff' ) return true;
+ }
+ else {
+ #convertable by the PHP GD image lib
+ if ( $mime === 'image/vnd.wap.wbmp'
+ || $mime === 'image/x-xbitmap' ) return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Return true if the file is of a type that can't be directly
+ * rendered by typical browsers and needs to be re-rasterized.
+ *
+ * This returns true for everything but the bitmap types
+ * supported by all browsers, i.e. JPEG; GIF and PNG. It will
+ * also return true for any non-image formats.
+ *
+ * @return bool
+ */
+ function mustRender() {
+ $mime= $this->getMimeType();
+
+ if ( $mime === "image/gif"
+ || $mime === "image/png"
+ || $mime === "image/jpeg" ) return false;
+
+ return true;
+ }
+
+ /**
+ * Determines if this media file may be shown inline on a page.
+ *
+ * This is currently synonymous to canRender(), but this could be
+ * extended to also allow inline display of other media,
+ * like flash animations or videos. If you do so, please keep in mind that
+ * that could be a security risk.
+ */
+ function allowInlineDisplay() {
+ return $this->canRender();
+ }
+
+ /**
+ * Determines if this media file is in a format that is unlikely to
+ * contain viruses or malicious content. It uses the global
+ * $wgTrustedMediaFormats list to determine if the file is safe.
+ *
+ * This is used to show a warning on the description page of non-safe files.
+ * It may also be used to disallow direct [[media:...]] links to such files.
+ *
+ * Note that this function will always return true if allowInlineDisplay()
+ * or isTrustedFile() is true for this file.
+ */
+ function isSafeFile() {
+ if ($this->allowInlineDisplay()) return true;
+ if ($this->isTrustedFile()) return true;
+
+ global $wgTrustedMediaFormats;
+
+ $type= $this->getMediaType();
+ $mime= $this->getMimeType();
+ #wfDebug("Image::isSafeFile: type= $type, mime= $mime\n");
+
+ if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted
+ if ( in_array( $type, $wgTrustedMediaFormats) ) return true;
+
+ if ($mime==="unknown/unknown") return false; #unknown type, not trusted
+ if ( in_array( $mime, $wgTrustedMediaFormats) ) return true;
+
+ return false;
+ }
+
+ /** Returns true if the file is flagged as trusted. Files flagged that way
+ * can be linked to directly, even if that is not allowed for this type of
+ * file normally.
+ *
+ * This is a dummy function right now and always returns false. It could be
+ * implemented to extract a flag from the database. The trusted flag could be
+ * set on upload, if the user has sufficient privileges, to bypass script-
+ * and html-filters. It may even be coupled with cryptographics signatures
+ * or such.
+ */
+ function isTrustedFile() {
+ #this could be implemented to check a flag in the databas,
+ #look for signatures, etc
+ return false;
+ }
+
+ /**
+ * Return the escapeLocalURL of this image
+ * @public
+ */
+ function getEscapeLocalURL() {
+ $this->getTitle();
+ return $this->title->escapeLocalURL();
+ }
+
+ /**
+ * Return the escapeFullURL of this image
+ * @public
+ */
+ function getEscapeFullURL() {
+ $this->getTitle();
+ return $this->title->escapeFullURL();
+ }
+
+ /**
+ * Return the URL of an image, provided its name.
+ *
+ * @param string $name Name of the image, without the leading "Image:"
+ * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath?
+ * @return string URL of $name image
+ * @public
+ * @static
+ */
+ function imageUrl( $name, $fromSharedDirectory = false ) {
+ global $wgUploadPath,$wgUploadBaseUrl,$wgSharedUploadPath;
+ if($fromSharedDirectory) {
+ $base = '';
+ $path = $wgSharedUploadPath;
+ } else {
+ $base = $wgUploadBaseUrl;
+ $path = $wgUploadPath;
+ }
+ $url = "{$base}{$path}" . wfGetHashPath($name, $fromSharedDirectory) . "{$name}";
+ return wfUrlencode( $url );
+ }
+
+ /**
+ * Returns true if the image file exists on disk.
+ * @return boolean Whether image file exist on disk.
+ * @public
+ */
+ function exists() {
+ $this->load();
+ return $this->fileExists;
+ }
+
+ /**
+ * @todo document
+ * @private
+ */
+ function thumbUrl( $width, $subdir='thumb') {
+ global $wgUploadPath, $wgUploadBaseUrl, $wgSharedUploadPath;
+ global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath;
+
+ // Generate thumb.php URL if possible
+ $script = false;
+ $url = false;
+
+ if ( $this->fromSharedDirectory ) {
+ if ( $wgSharedThumbnailScriptPath ) {
+ $script = $wgSharedThumbnailScriptPath;
+ }
+ } else {
+ if ( $wgThumbnailScriptPath ) {
+ $script = $wgThumbnailScriptPath;
+ }
+ }
+ if ( $script ) {
+ $url = $script . '?f=' . urlencode( $this->name ) . '&w=' . urlencode( $width );
+ if( $this->mustRender() ) {
+ $url.= '&r=1';
+ }
+ } else {
+ $name = $this->thumbName( $width );
+ if($this->fromSharedDirectory) {
+ $base = '';
+ $path = $wgSharedUploadPath;
+ } else {
+ $base = $wgUploadBaseUrl;
+ $path = $wgUploadPath;
+ }
+ if ( Image::isHashed( $this->fromSharedDirectory ) ) {
+ $url = "{$base}{$path}/{$subdir}" .
+ wfGetHashPath($this->name, $this->fromSharedDirectory)
+ . $this->name.'/'.$name;
+ $url = wfUrlencode( $url );
+ } else {
+ $url = "{$base}{$path}/{$subdir}/{$name}";
+ }
+ }
+ return array( $script !== false, $url );
+ }
+
+ /**
+ * Return the file name of a thumbnail of the specified width
+ *
+ * @param integer $width Width of the thumbnail image
+ * @param boolean $shared Does the thumbnail come from the shared repository?
+ * @private
+ */
+ function thumbName( $width ) {
+ $thumb = $width."px-".$this->name;
+
+ if( $this->mustRender() ) {
+ if( $this->canRender() ) {
+ # Rasterize to PNG (for SVG vector images, etc)
+ $thumb .= '.png';
+ }
+ else {
+ #should we use iconThumb here to get a symbolic thumbnail?
+ #or should we fail with an internal error?
+ return NULL; //can't make bitmap
+ }
+ }
+ return $thumb;
+ }
+
+ /**
+ * Create a thumbnail of the image having the specified width/height.
+ * The thumbnail will not be created if the width is larger than the
+ * image's width. Let the browser do the scaling in this case.
+ * The thumbnail is stored on disk and is only computed if the thumbnail
+ * file does not exist OR if it is older than the image.
+ * Returns the URL.
+ *
+ * Keeps aspect ratio of original image. If both width and height are
+ * specified, the generated image will be no bigger than width x height,
+ * and will also have correct aspect ratio.
+ *
+ * @param integer $width maximum width of the generated thumbnail
+ * @param integer $height maximum height of the image (optional)
+ * @public
+ */
+ function createThumb( $width, $height=-1 ) {
+ $thumb = $this->getThumbnail( $width, $height );
+ if( is_null( $thumb ) ) return '';
+ return $thumb->getUrl();
+ }
+
+ /**
+ * As createThumb, but returns a ThumbnailImage object. This can
+ * provide access to the actual file, the real size of the thumb,
+ * and can produce a convenient <img> tag for you.
+ *
+ * For non-image formats, this may return a filetype-specific icon.
+ *
+ * @param integer $width maximum width of the generated thumbnail
+ * @param integer $height maximum height of the image (optional)
+ * @param boolean $render True to render the thumbnail if it doesn't exist,
+ * false to just return the URL
+ *
+ * @return ThumbnailImage or null on failure
+ * @public
+ */
+ function getThumbnail( $width, $height=-1, $render = true ) {
+ wfProfileIn( __METHOD__ );
+ if ($this->canRender()) {
+ if ( $height > 0 ) {
+ $this->load();
+ if ( $width > $this->width * $height / $this->height ) {
+ $width = wfFitBoxWidth( $this->width, $this->height, $height );
+ }
+ }
+ if ( $render ) {
+ $thumb = $this->renderThumb( $width );
+ } else {
+ // Don't render, just return the URL
+ if ( $this->validateThumbParams( $width, $height ) ) {
+ if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) {
+ $url = $this->getURL();
+ } else {
+ list( $isScriptUrl, $url ) = $this->thumbUrl( $width );
+ }
+ $thumb = new ThumbnailImage( $url, $width, $height );
+ } else {
+ $thumb = null;
+ }
+ }
+ } else {
+ // not a bitmap or renderable image, don't try.
+ $thumb = $this->iconThumb();
+ }
+ wfProfileOut( __METHOD__ );
+ return $thumb;
+ }
+
+ /**
+ * @return ThumbnailImage
+ */
+ function iconThumb() {
+ global $wgStylePath, $wgStyleDirectory;
+
+ $try = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' );
+ foreach( $try as $icon ) {
+ $path = '/common/images/icons/' . $icon;
+ $filepath = $wgStyleDirectory . $path;
+ if( file_exists( $filepath ) ) {
+ return new ThumbnailImage( $wgStylePath . $path, 120, 120 );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Validate thumbnail parameters and fill in the correct height
+ *
+ * @param integer &$width Specified width (input/output)
+ * @param integer &$height Height (output only)
+ * @return false to indicate that an error should be returned to the user.
+ */
+ function validateThumbParams( &$width, &$height ) {
+ global $wgSVGMaxSize, $wgMaxImageArea;
+
+ $this->load();
+
+ if ( ! $this->exists() )
+ {
+ # If there is no image, there will be no thumbnail
+ return false;
+ }
+
+ $width = intval( $width );
+
+ # Sanity check $width
+ if( $width <= 0 || $this->width <= 0) {
+ # BZZZT
+ return false;
+ }
+
+ # Don't thumbnail an image so big that it will fill hard drives and send servers into swap
+ # JPEG has the handy property of allowing thumbnailing without full decompression, so we make
+ # an exception for it.
+ if ( $this->getMediaType() == MEDIATYPE_BITMAP &&
+ $this->getMimeType() !== 'image/jpeg' &&
+ $this->width * $this->height > $wgMaxImageArea )
+ {
+ return false;
+ }
+
+ # Don't make an image bigger than the source, or wgMaxSVGSize for SVGs
+ if ( $this->mustRender() ) {
+ $width = min( $width, $wgSVGMaxSize );
+ } elseif ( $width > $this->width - 1 ) {
+ $width = $this->width;
+ $height = $this->height;
+ return true;
+ }
+
+ $height = round( $this->height * $width / $this->width );
+ return true;
+ }
+
+ /**
+ * Create a thumbnail of the image having the specified width.
+ * The thumbnail will not be created if the width is larger than the
+ * image's width. Let the browser do the scaling in this case.
+ * The thumbnail is stored on disk and is only computed if the thumbnail
+ * file does not exist OR if it is older than the image.
+ * Returns an object which can return the pathname, URL, and physical
+ * pixel size of the thumbnail -- or null on failure.
+ *
+ * @return ThumbnailImage or null on failure
+ * @private
+ */
+ function renderThumb( $width, $useScript = true ) {
+ global $wgUseSquid, $wgThumbnailEpoch;
+
+ wfProfileIn( __METHOD__ );
+
+ $this->load();
+ $height = -1;
+ if ( !$this->validateThumbParams( $width, $height ) ) {
+ # Validation error
+ wfProfileOut( __METHOD__ );
+ return null;
+ }
+
+ if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) {
+ # validateThumbParams (or the user) wants us to return the unscaled image
+ $thumb = new ThumbnailImage( $this->getURL(), $width, $height );
+ wfProfileOut( __METHOD__ );
+ return $thumb;
+ }
+
+ list( $isScriptUrl, $url ) = $this->thumbUrl( $width );
+ if ( $isScriptUrl && $useScript ) {
+ // Use thumb.php to render the image
+ $thumb = new ThumbnailImage( $url, $width, $height );
+ wfProfileOut( __METHOD__ );
+ return $thumb;
+ }
+
+ $thumbName = $this->thumbName( $width, $this->fromSharedDirectory );
+ $thumbDir = wfImageThumbDir( $this->name, $this->fromSharedDirectory );
+ $thumbPath = $thumbDir.'/'.$thumbName;
+
+ if ( is_dir( $thumbPath ) ) {
+ // Directory where file should be
+ // This happened occasionally due to broken migration code in 1.5
+ // Rename to broken-*
+ global $wgUploadDirectory;
+ for ( $i = 0; $i < 100 ; $i++ ) {
+ $broken = "$wgUploadDirectory/broken-$i-$thumbName";
+ if ( !file_exists( $broken ) ) {
+ rename( $thumbPath, $broken );
+ break;
+ }
+ }
+ // Code below will ask if it exists, and the answer is now no
+ clearstatcache();
+ }
+
+ $done = true;
+ if ( !file_exists( $thumbPath ) ||
+ filemtime( $thumbPath ) < wfTimestamp( TS_UNIX, $wgThumbnailEpoch ) )
+ {
+ // Create the directory if it doesn't exist
+ if ( is_file( $thumbDir ) ) {
+ // File where thumb directory should be, destroy if possible
+ @unlink( $thumbDir );
+ }
+ wfMkdirParents( $thumbDir );
+
+ $oldThumbPath = wfDeprecatedThumbDir( $thumbName, 'thumb', $this->fromSharedDirectory ).
+ '/'.$thumbName;
+ $done = false;
+
+ // Migration from old directory structure
+ if ( is_file( $oldThumbPath ) ) {
+ if ( filemtime($oldThumbPath) >= filemtime($this->imagePath) ) {
+ if ( file_exists( $thumbPath ) ) {
+ if ( !is_dir( $thumbPath ) ) {
+ // Old image in the way of rename
+ unlink( $thumbPath );
+ } else {
+ // This should have been dealt with already
+ throw new MWException( "Directory where image should be: $thumbPath" );
+ }
+ }
+ // Rename the old image into the new location
+ rename( $oldThumbPath, $thumbPath );
+ $done = true;
+ } else {
+ unlink( $oldThumbPath );
+ }
+ }
+ if ( !$done ) {
+ $this->lastError = $this->reallyRenderThumb( $thumbPath, $width, $height );
+ if ( $this->lastError === true ) {
+ $done = true;
+ } elseif( $GLOBALS['wgIgnoreImageErrors'] ) {
+ // Log the error but output anyway.
+ // With luck it's a transitory error...
+ $done = true;
+ }
+
+ # Purge squid
+ # This has to be done after the image is updated and present for all machines on NFS,
+ # or else the old version might be stored into the squid again
+ if ( $wgUseSquid ) {
+ $urlArr = array( $url );
+ wfPurgeSquidServers($urlArr);
+ }
+ }
+ }
+
+ if ( $done ) {
+ $thumb = new ThumbnailImage( $url, $width, $height, $thumbPath );
+ } else {
+ $thumb = null;
+ }
+ wfProfileOut( __METHOD__ );
+ return $thumb;
+ } // END OF function renderThumb
+
+ /**
+ * Really render a thumbnail
+ * Call this only for images for which canRender() returns true.
+ *
+ * @param string $thumbPath Path to thumbnail
+ * @param int $width Desired width in pixels
+ * @param int $height Desired height in pixels
+ * @return bool True on error, false or error string on failure.
+ * @private
+ */
+ function reallyRenderThumb( $thumbPath, $width, $height ) {
+ global $wgSVGConverters, $wgSVGConverter;
+ global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ global $wgCustomConvertCommand;
+
+ $this->load();
+
+ $err = false;
+ $cmd = "";
+ $retval = 0;
+
+ if( $this->mime === "image/svg" ) {
+ #Right now we have only SVG
+
+ global $wgSVGConverters, $wgSVGConverter;
+ if( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
+ global $wgSVGConverterPath;
+ $cmd = str_replace(
+ array( '$path/', '$width', '$height', '$input', '$output' ),
+ array( $wgSVGConverterPath ? "$wgSVGConverterPath/" : "",
+ intval( $width ),
+ intval( $height ),
+ wfEscapeShellArg( $this->imagePath ),
+ wfEscapeShellArg( $thumbPath ) ),
+ $wgSVGConverters[$wgSVGConverter] );
+ wfProfileIn( 'rsvg' );
+ wfDebug( "reallyRenderThumb SVG: $cmd\n" );
+ $err = wfShellExec( $cmd, $retval );
+ wfProfileOut( 'rsvg' );
+ }
+ } elseif ( $wgUseImageMagick ) {
+ # use ImageMagick
+
+ if ( $this->mime == 'image/jpeg' ) {
+ $quality = "-quality 80"; // 80%
+ } elseif ( $this->mime == 'image/png' ) {
+ $quality = "-quality 95"; // zlib 9, adaptive filtering
+ } else {
+ $quality = ''; // default
+ }
+
+ # Specify white background color, will be used for transparent images
+ # in Internet Explorer/Windows instead of default black.
+
+ # Note, we specify "-size {$width}" and NOT "-size {$width}x{$height}".
+ # It seems that ImageMagick has a bug wherein it produces thumbnails of
+ # the wrong size in the second case.
+
+ $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) .
+ " {$quality} -background white -size {$width} ".
+ wfEscapeShellArg($this->imagePath) .
+ // Coalesce is needed to scale animated GIFs properly (bug 1017).
+ ' -coalesce ' .
+ // For the -resize option a "!" is needed to force exact size,
+ // or ImageMagick may decide your ratio is wrong and slice off
+ // a pixel.
+ " -resize " . wfEscapeShellArg( "{$width}x{$height}!" ) .
+ " -depth 8 " .
+ wfEscapeShellArg($thumbPath) . " 2>&1";
+ wfDebug("reallyRenderThumb: running ImageMagick: $cmd\n");
+ wfProfileIn( 'convert' );
+ $err = wfShellExec( $cmd, $retval );
+ wfProfileOut( 'convert' );
+ } elseif( $wgCustomConvertCommand ) {
+ # Use a custom convert command
+ # Variables: %s %d %w %h
+ $src = wfEscapeShellArg( $this->imagePath );
+ $dst = wfEscapeShellArg( $thumbPath );
+ $cmd = $wgCustomConvertCommand;
+ $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
+ $cmd = str_replace( '%h', $height, str_replace( '%w', $width, $cmd ) ); # Size
+ wfDebug( "reallyRenderThumb: Running custom convert command $cmd\n" );
+ wfProfileIn( 'convert' );
+ $err = wfShellExec( $cmd, $retval );
+ wfProfileOut( 'convert' );
+ } else {
+ # Use PHP's builtin GD library functions.
+ #
+ # First find out what kind of file this is, and select the correct
+ # input routine for this.
+
+ $typemap = array(
+ 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
+ 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( &$this, 'imageJpegWrapper' ) ),
+ 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
+ 'image/vnd.wap.wmbp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
+ 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
+ );
+ if( !isset( $typemap[$this->mime] ) ) {
+ $err = 'Image type not supported';
+ wfDebug( "$err\n" );
+ return $err;
+ }
+ list( $loader, $colorStyle, $saveType ) = $typemap[$this->mime];
+
+ if( !function_exists( $loader ) ) {
+ $err = "Incomplete GD library configuration: missing function $loader";
+ wfDebug( "$err\n" );
+ return $err;
+ }
+ if( $colorStyle == 'palette' ) {
+ $truecolor = false;
+ } elseif( $colorStyle == 'truecolor' ) {
+ $truecolor = true;
+ } elseif( $colorStyle == 'bits' ) {
+ $truecolor = ( $this->bits > 8 );
+ }
+
+ $src_image = call_user_func( $loader, $this->imagePath );
+ if ( $truecolor ) {
+ $dst_image = imagecreatetruecolor( $width, $height );
+ } else {
+ $dst_image = imagecreate( $width, $height );
+ }
+ imagecopyresampled( $dst_image, $src_image,
+ 0,0,0,0,
+ $width, $height, $this->width, $this->height );
+ call_user_func( $saveType, $dst_image, $thumbPath );
+ imagedestroy( $dst_image );
+ imagedestroy( $src_image );
+ }
+
+ #
+ # Check for zero-sized thumbnails. Those can be generated when
+ # no disk space is available or some other error occurs
+ #
+ if( file_exists( $thumbPath ) ) {
+ $thumbstat = stat( $thumbPath );
+ if( $thumbstat['size'] == 0 || $retval != 0 ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Removing bad %d-byte thumbnail "%s"',
+ $thumbstat['size'], $thumbPath ) );
+ unlink( $thumbPath );
+ }
+ }
+ if ( $retval != 0 ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
+ wfHostname(), $retval, trim($err), $cmd ) );
+ return wfMsg( 'thumbnail_error', $err );
+ } else {
+ return true;
+ }
+ }
+
+ function getLastError() {
+ return $this->lastError;
+ }
+
+ function imageJpegWrapper( $dst_image, $thumbPath ) {
+ imageinterlace( $dst_image );
+ imagejpeg( $dst_image, $thumbPath, 95 );
+ }
+
+ /**
+ * Get all thumbnail names previously generated for this image
+ */
+ function getThumbnails( $shared = false ) {
+ if ( Image::isHashed( $shared ) ) {
+ $this->load();
+ $files = array();
+ $dir = wfImageThumbDir( $this->name, $shared );
+
+ // This generates an error on failure, hence the @
+ $handle = @opendir( $dir );
+
+ if ( $handle ) {
+ while ( false !== ( $file = readdir($handle) ) ) {
+ if ( $file{0} != '.' ) {
+ $files[] = $file;
+ }
+ }
+ closedir( $handle );
+ }
+ } else {
+ $files = array();
+ }
+
+ return $files;
+ }
+
+ /**
+ * Refresh metadata in memcached, but don't touch thumbnails or squid
+ */
+ function purgeMetadataCache() {
+ clearstatcache();
+ $this->loadFromFile();
+ $this->saveToCache();
+ }
+
+ /**
+ * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
+ */
+ function purgeCache( $archiveFiles = array(), $shared = false ) {
+ global $wgUseSquid;
+
+ // Refresh metadata cache
+ $this->purgeMetadataCache();
+
+ // Delete thumbnails
+ $files = $this->getThumbnails( $shared );
+ $dir = wfImageThumbDir( $this->name, $shared );
+ $urls = array();
+ foreach ( $files as $file ) {
+ if ( preg_match( '/^(\d+)px/', $file, $m ) ) {
+ $urls[] = $this->thumbUrl( $m[1], $this->fromSharedDirectory );
+ @unlink( "$dir/$file" );
+ }
+ }
+
+ // Purge the squid
+ if ( $wgUseSquid ) {
+ $urls[] = $this->getViewURL();
+ foreach ( $archiveFiles as $file ) {
+ $urls[] = wfImageArchiveUrl( $file );
+ }
+ wfPurgeSquidServers( $urls );
+ }
+ }
+
+ /**
+ * Purge the image description page, but don't go after
+ * pages using the image. Use when modifying file history
+ * but not the current data.
+ */
+ function purgeDescription() {
+ $page = Title::makeTitle( NS_IMAGE, $this->name );
+ $page->invalidateCache();
+ $page->purgeSquid();
+ }
+
+ /**
+ * Purge metadata and all affected pages when the image is created,
+ * deleted, or majorly updated. A set of additional URLs may be
+ * passed to purge, such as specific image files which have changed.
+ * @param $urlArray array
+ */
+ function purgeEverything( $urlArr=array() ) {
+ // Delete thumbnails and refresh image metadata cache
+ $this->purgeCache();
+ $this->purgeDescription();
+
+ // Purge cache of all pages using this image
+ $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
+ $update->doUpdate();
+ }
+
+ function checkDBSchema(&$db) {
+ global $wgCheckDBSchema;
+ if (!$wgCheckDBSchema) {
+ return;
+ }
+ # img_name must be unique
+ if ( !$db->indexUnique( 'image', 'img_name' ) && !$db->indexExists('image','PRIMARY') ) {
+ throw new MWException( 'Database schema not up to date, please run maintenance/archives/patch-image_name_unique.sql' );
+ }
+
+ # new fields must exist
+ #
+ # Not really, there's hundreds of checks like this that we could do and they're all pointless, because
+ # if the fields are missing, the database will loudly report a query error, the first time you try to do
+ # something. The only reason I put the above schema check in was because the absence of that particular
+ # index would lead to an annoying subtle bug. No error message, just some very odd behaviour on duplicate
+ # uploads. -- TS
+ /*
+ if ( !$db->fieldExists( 'image', 'img_media_type' )
+ || !$db->fieldExists( 'image', 'img_metadata' )
+ || !$db->fieldExists( 'image', 'img_width' ) ) {
+
+ throw new MWException( 'Database schema not up to date, please run maintenance/update.php' );
+ }
+ */
+ }
+
+ /**
+ * Return the image history of this image, line by line.
+ * starts with current version, then old versions.
+ * uses $this->historyLine to check which line to return:
+ * 0 return line for current version
+ * 1 query for old versions, return first one
+ * 2, ... return next old version from above query
+ *
+ * @public
+ */
+ function nextHistoryLine() {
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $this->checkDBSchema($dbr);
+
+ if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
+ $this->historyRes = $dbr->select( 'image',
+ array(
+ 'img_size',
+ 'img_description',
+ 'img_user','img_user_text',
+ 'img_timestamp',
+ 'img_width',
+ 'img_height',
+ "'' AS oi_archive_name"
+ ),
+ array( 'img_name' => $this->title->getDBkey() ),
+ __METHOD__
+ );
+ if ( 0 == wfNumRows( $this->historyRes ) ) {
+ return FALSE;
+ }
+ } else if ( $this->historyLine == 1 ) {
+ $this->historyRes = $dbr->select( 'oldimage',
+ array(
+ 'oi_size AS img_size',
+ 'oi_description AS img_description',
+ 'oi_user AS img_user',
+ 'oi_user_text AS img_user_text',
+ 'oi_timestamp AS img_timestamp',
+ 'oi_width as img_width',
+ 'oi_height as img_height',
+ 'oi_archive_name'
+ ),
+ array( 'oi_name' => $this->title->getDBkey() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'oi_timestamp DESC' )
+ );
+ }
+ $this->historyLine ++;
+
+ return $dbr->fetchObject( $this->historyRes );
+ }
+
+ /**
+ * Reset the history pointer to the first element of the history
+ * @public
+ */
+ function resetHistory() {
+ $this->historyLine = 0;
+ }
+
+ /**
+ * Return the full filesystem path to the file. Note that this does
+ * not mean that a file actually exists under that location.
+ *
+ * This path depends on whether directory hashing is active or not,
+ * i.e. whether the images are all found in the same directory,
+ * or in hashed paths like /images/3/3c.
+ *
+ * @public
+ * @param boolean $fromSharedDirectory Return the path to the file
+ * in a shared repository (see $wgUseSharedRepository and related
+ * options in DefaultSettings.php) instead of a local one.
+ *
+ */
+ function getFullPath( $fromSharedRepository = false ) {
+ global $wgUploadDirectory, $wgSharedUploadDirectory;
+
+ $dir = $fromSharedRepository ? $wgSharedUploadDirectory :
+ $wgUploadDirectory;
+
+ // $wgSharedUploadDirectory may be false, if thumb.php is used
+ if ( $dir ) {
+ $fullpath = $dir . wfGetHashPath($this->name, $fromSharedRepository) . $this->name;
+ } else {
+ $fullpath = false;
+ }
+
+ return $fullpath;
+ }
+
+ /**
+ * @return bool
+ * @static
+ */
+ function isHashed( $shared ) {
+ global $wgHashedUploadDirectory, $wgHashedSharedUploadDirectory;
+ return $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory;
+ }
+
+ /**
+ * Record an image upload in the upload log and the image table
+ */
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
+ global $wgUser, $wgUseCopyrightUpload;
+
+ $dbw =& wfGetDB( DB_MASTER );
+
+ $this->checkDBSchema($dbw);
+
+ // Delete thumbnails and refresh the metadata cache
+ $this->purgeCache();
+
+ // Fail now if the image isn't there
+ if ( !$this->fileExists || $this->fromSharedDirectory ) {
+ wfDebug( "Image::recordUpload: File ".$this->imagePath." went missing!\n" );
+ return false;
+ }
+
+ if ( $wgUseCopyrightUpload ) {
+ if ( $license != '' ) {
+ $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ }
+ $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" .
+ '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+ "$licensetxt" .
+ '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
+ } else {
+ if ( $license != '' ) {
+ $filedesc = $desc == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n";
+ $textdesc = $filedesc .
+ '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ } else {
+ $textdesc = $desc;
+ }
+ }
+
+ $now = $dbw->timestamp();
+
+ #split mime type
+ if (strpos($this->mime,'/')!==false) {
+ list($major,$minor)= explode('/',$this->mime,2);
+ }
+ else {
+ $major= $this->mime;
+ $minor= "unknown";
+ }
+
+ # Test to see if the row exists using INSERT IGNORE
+ # This avoids race conditions by locking the row until the commit, and also
+ # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
+ $dbw->insert( 'image',
+ array(
+ 'img_name' => $this->name,
+ 'img_size'=> $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_timestamp' => $now,
+ 'img_description' => $desc,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ ),
+ __METHOD__,
+ 'IGNORE'
+ );
+
+ if( $dbw->affectedRows() == 0 ) {
+ # Collision, this is an update of an image
+ # Insert previous contents into oldimage
+ $dbw->insertSelect( 'oldimage', 'image',
+ array(
+ 'oi_name' => 'img_name',
+ 'oi_archive_name' => $dbw->addQuotes( $oldver ),
+ 'oi_size' => 'img_size',
+ 'oi_width' => 'img_width',
+ 'oi_height' => 'img_height',
+ 'oi_bits' => 'img_bits',
+ 'oi_timestamp' => 'img_timestamp',
+ 'oi_description' => 'img_description',
+ 'oi_user' => 'img_user',
+ 'oi_user_text' => 'img_user_text',
+ ), array( 'img_name' => $this->name ), __METHOD__
+ );
+
+ # Update the current image row
+ $dbw->update( 'image',
+ array( /* SET */
+ 'img_size' => $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_timestamp' => $now,
+ 'img_description' => $desc,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ ), array( /* WHERE */
+ 'img_name' => $this->name
+ ), __METHOD__
+ );
+ } else {
+ # This is a new image
+ # Update the image count
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+ }
+
+ $descTitle = $this->getTitle();
+ $article = new Article( $descTitle );
+ $minor = false;
+ $watch = $watch || $wgUser->isWatched( $descTitle );
+ $suppressRC = true; // There's already a log entry, so don't double the RC load
+
+ if( $descTitle->exists() ) {
+ // TODO: insert a null revision into the page history for this update.
+ if( $watch ) {
+ $wgUser->addWatch( $descTitle );
+ }
+
+ # Invalidate the cache for the description page
+ $descTitle->invalidateCache();
+ $descTitle->purgeSquid();
+ } else {
+ // New image; create the description page.
+ $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC );
+ }
+
+ # Add the log entry
+ $log = new LogPage( 'upload' );
+ $log->addEntry( 'upload', $descTitle, $desc );
+
+ # Commit the transaction now, in case something goes wrong later
+ # The most important thing is that images don't get lost, especially archives
+ $dbw->immediateCommit();
+
+ # Invalidate cache for all pages using this image
+ $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
+ $update->doUpdate();
+
+ return true;
+ }
+
+ /**
+ * Get an array of Title objects which are articles which use this image
+ * Also adds their IDs to the link cache
+ *
+ * This is mostly copied from Title::getLinksTo()
+ *
+ * @deprecated Use HTMLCacheUpdate, this function uses too much memory
+ */
+ function getLinksTo( $options = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $options ) {
+ $db =& wfGetDB( DB_MASTER );
+ } else {
+ $db =& wfGetDB( DB_SLAVE );
+ }
+ $linkCache =& LinkCache::singleton();
+
+ extract( $db->tableNames( 'page', 'imagelinks' ) );
+ $encName = $db->addQuotes( $this->name );
+ $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options";
+ $res = $db->query( $sql, __METHOD__ );
+
+ $retVal = array();
+ if ( $db->numRows( $res ) ) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
+ $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
+ $retVal[] = $titleObj;
+ }
+ }
+ }
+ $db->freeResult( $res );
+ wfProfileOut( __METHOD__ );
+ return $retVal;
+ }
+
+ /**
+ * Retrive Exif data from the file and prune unrecognized tags
+ * and/or tags with invalid contents
+ *
+ * @param $filename
+ * @return array
+ */
+ private function retrieveExifData( $filename ) {
+ global $wgShowEXIF;
+
+ /*
+ if ( $this->getMimeType() !== "image/jpeg" )
+ return array();
+ */
+
+ if( $wgShowEXIF && file_exists( $filename ) ) {
+ $exif = new Exif( $filename );
+ return $exif->getFilteredData();
+ }
+
+ return array();
+ }
+
+ function getExifData() {
+ global $wgRequest;
+ if ( $this->metadata === '0' )
+ return array();
+
+ $purge = $wgRequest->getVal( 'action' ) == 'purge';
+ $ret = unserialize( $this->metadata );
+
+ $oldver = isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) ? $ret['MEDIAWIKI_EXIF_VERSION'] : 0;
+ $newver = Exif::version();
+
+ if ( !count( $ret ) || $purge || $oldver != $newver ) {
+ $this->purgeMetadataCache();
+ $this->updateExifData( $newver );
+ }
+ if ( isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) )
+ unset( $ret['MEDIAWIKI_EXIF_VERSION'] );
+ $format = new FormatExif( $ret );
+
+ return $format->getFormattedData();
+ }
+
+ function updateExifData( $version ) {
+ if ( $this->getImagePath() === false ) # Not a local image
+ return;
+
+ # Get EXIF data from image
+ $exif = $this->retrieveExifData( $this->imagePath );
+ if ( count( $exif ) ) {
+ $exif['MEDIAWIKI_EXIF_VERSION'] = $version;
+ $this->metadata = serialize( $exif );
+ } else {
+ $this->metadata = '0';
+ }
+
+ # Update EXIF data in database
+ $dbw =& wfGetDB( DB_MASTER );
+
+ $this->checkDBSchema($dbw);
+
+ $dbw->update( 'image',
+ array( 'img_metadata' => $this->metadata ),
+ array( 'img_name' => $this->name ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * Returns true if the image does not come from the shared
+ * image repository.
+ *
+ * @return bool
+ */
+ function isLocal() {
+ return !$this->fromSharedDirectory;
+ }
+
+ /**
+ * Was this image ever deleted from the wiki?
+ *
+ * @return bool
+ */
+ function wasDeleted() {
+ $title = Title::makeTitle( NS_IMAGE, $this->name );
+ return ( $title->isDeleted() > 0 );
+ }
+
+ /**
+ * Delete all versions of the image.
+ *
+ * Moves the files into an archive directory (or deletes them)
+ * and removes the database rows.
+ *
+ * Cache purging is done; logging is caller's responsibility.
+ *
+ * @param $reason
+ * @return true on success, false on some kind of failure
+ */
+ function delete( $reason ) {
+ $transaction = new FSTransaction();
+ $urlArr = array( $this->getURL() );
+
+ if( !FileStore::lock() ) {
+ wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
+ return false;
+ }
+
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+
+ // Delete old versions
+ $result = $dbw->select( 'oldimage',
+ array( 'oi_archive_name' ),
+ array( 'oi_name' => $this->name ) );
+
+ while( $row = $dbw->fetchObject( $result ) ) {
+ $oldName = $row->oi_archive_name;
+
+ $transaction->add( $this->prepareDeleteOld( $oldName, $reason ) );
+
+ // We'll need to purge this URL from caches...
+ $urlArr[] = wfImageArchiveUrl( $oldName );
+ }
+ $dbw->freeResult( $result );
+
+ // And the current version...
+ $transaction->add( $this->prepareDeleteCurrent( $reason ) );
+
+ $dbw->immediateCommit();
+ } catch( MWException $e ) {
+ wfDebug( __METHOD__.": db error, rolling back file transactions\n" );
+ $transaction->rollback();
+ FileStore::unlock();
+ throw $e;
+ }
+
+ wfDebug( __METHOD__.": deleted db items, applying file transactions\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+
+ // Update site_stats
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
+
+ $this->purgeEverything( $urlArr );
+
+ return true;
+ }
+
+
+ /**
+ * Delete an old version of the image.
+ *
+ * Moves the file into an archive directory (or deletes it)
+ * and removes the database row.
+ *
+ * Cache purging is done; logging is caller's responsibility.
+ *
+ * @param $reason
+ * @throws MWException or FSException on database or filestore failure
+ * @return true on success, false on some kind of failure
+ */
+ function deleteOld( $archiveName, $reason ) {
+ $transaction = new FSTransaction();
+ $urlArr = array();
+
+ if( !FileStore::lock() ) {
+ wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
+ return false;
+ }
+
+ $transaction = new FSTransaction();
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+ $transaction->add( $this->prepareDeleteOld( $archiveName, $reason ) );
+ $dbw->immediateCommit();
+ } catch( MWException $e ) {
+ wfDebug( __METHOD__.": db error, rolling back file transaction\n" );
+ $transaction->rollback();
+ FileStore::unlock();
+ throw $e;
+ }
+
+ wfDebug( __METHOD__.": deleted db items, applying file transaction\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+ $this->purgeDescription();
+
+ // Squid purging
+ global $wgUseSquid;
+ if ( $wgUseSquid ) {
+ $urlArr = array(
+ wfImageArchiveUrl( $archiveName ),
+ );
+ wfPurgeSquidServers( $urlArr );
+ }
+ return true;
+ }
+
+ /**
+ * Delete the current version of a file.
+ * May throw a database error.
+ * @return true on success, false on failure
+ */
+ private function prepareDeleteCurrent( $reason ) {
+ return $this->prepareDeleteVersion(
+ $this->getFullPath(),
+ $reason,
+ 'image',
+ array(
+ 'fa_name' => 'img_name',
+ 'fa_archive_name' => 'NULL',
+ 'fa_size' => 'img_size',
+ 'fa_width' => 'img_width',
+ 'fa_height' => 'img_height',
+ 'fa_metadata' => 'img_metadata',
+ 'fa_bits' => 'img_bits',
+ 'fa_media_type' => 'img_media_type',
+ 'fa_major_mime' => 'img_major_mime',
+ 'fa_minor_mime' => 'img_minor_mime',
+ 'fa_description' => 'img_description',
+ 'fa_user' => 'img_user',
+ 'fa_user_text' => 'img_user_text',
+ 'fa_timestamp' => 'img_timestamp' ),
+ array( 'img_name' => $this->name ),
+ __METHOD__ );
+ }
+
+ /**
+ * Delete a given older version of a file.
+ * May throw a database error.
+ * @return true on success, false on failure
+ */
+ private function prepareDeleteOld( $archiveName, $reason ) {
+ $oldpath = wfImageArchiveDir( $this->name ) .
+ DIRECTORY_SEPARATOR . $archiveName;
+ return $this->prepareDeleteVersion(
+ $oldpath,
+ $reason,
+ 'oldimage',
+ array(
+ 'fa_name' => 'oi_name',
+ 'fa_archive_name' => 'oi_archive_name',
+ 'fa_size' => 'oi_size',
+ 'fa_width' => 'oi_width',
+ 'fa_height' => 'oi_height',
+ 'fa_metadata' => 'NULL',
+ 'fa_bits' => 'oi_bits',
+ 'fa_media_type' => 'NULL',
+ 'fa_major_mime' => 'NULL',
+ 'fa_minor_mime' => 'NULL',
+ 'fa_description' => 'oi_description',
+ 'fa_user' => 'oi_user',
+ 'fa_user_text' => 'oi_user_text',
+ 'fa_timestamp' => 'oi_timestamp' ),
+ array(
+ 'oi_name' => $this->name,
+ 'oi_archive_name' => $archiveName ),
+ __METHOD__ );
+ }
+
+ /**
+ * Do the dirty work of backing up an image row and its file
+ * (if $wgSaveDeletedFiles is on) and removing the originals.
+ *
+ * Must be run while the file store is locked and a database
+ * transaction is open to avoid race conditions.
+ *
+ * @return FSTransaction
+ */
+ private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $fname ) {
+ global $wgUser, $wgSaveDeletedFiles;
+
+ // Dupe the file into the file store
+ if( file_exists( $path ) ) {
+ if( $wgSaveDeletedFiles ) {
+ $group = 'deleted';
+
+ $store = FileStore::get( $group );
+ $key = FileStore::calculateKey( $path, $this->extension );
+ $transaction = $store->insert( $key, $path,
+ FileStore::DELETE_ORIGINAL );
+ } else {
+ $group = null;
+ $key = null;
+ $transaction = FileStore::deleteFile( $path );
+ }
+ } else {
+ wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" );
+ $group = null;
+ $key = null;
+ $transaction = new FSTransaction(); // empty
+ }
+
+ if( $transaction === false ) {
+ // Fail to restore?
+ wfDebug( __METHOD__.": import to file store failed, aborting\n" );
+ throw new MWException( "Could not archive and delete file $path" );
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $storageMap = array(
+ 'fa_storage_group' => $dbw->addQuotes( $group ),
+ 'fa_storage_key' => $dbw->addQuotes( $key ),
+
+ 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ),
+ 'fa_deleted_timestamp' => $dbw->timestamp(),
+ 'fa_deleted_reason' => $dbw->addQuotes( $reason ) );
+ $allFields = array_merge( $storageMap, $fieldMap );
+
+ try {
+ if( $wgSaveDeletedFiles ) {
+ $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
+ }
+ $dbw->delete( $table, $where, $fname );
+ } catch( DBQueryError $e ) {
+ // Something went horribly wrong!
+ // Leave the file as it was...
+ wfDebug( __METHOD__.": database error, rolling back file transaction\n" );
+ $transaction->rollback();
+ throw $e;
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Restore all or specified deleted revisions to the given file.
+ * Permissions and logging are left to the caller.
+ *
+ * May throw database exceptions on error.
+ *
+ * @param $versions set of record ids of deleted items to restore,
+ * or empty to restore all revisions.
+ * @return the number of file revisions restored if successful,
+ * or false on failure
+ */
+ function restore( $versions=array() ) {
+ if( !FileStore::lock() ) {
+ wfDebug( __METHOD__." could not acquire filestore lock\n" );
+ return false;
+ }
+
+ $transaction = new FSTransaction();
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+
+ // Re-confirm whether this image presently exists;
+ // if no we'll need to create an image record for the
+ // first item we restore.
+ $exists = $dbw->selectField( 'image', '1',
+ array( 'img_name' => $this->name ),
+ __METHOD__ );
+
+ // Fetch all or selected archived revisions for the file,
+ // sorted from the most recent to the oldest.
+ $conditions = array( 'fa_name' => $this->name );
+ if( $versions ) {
+ $conditions['fa_id'] = $versions;
+ }
+
+ $result = $dbw->select( 'filearchive', '*',
+ $conditions,
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+
+ if( $dbw->numRows( $result ) < count( $versions ) ) {
+ // There's some kind of conflict or confusion;
+ // we can't restore everything we were asked to.
+ wfDebug( __METHOD__.": couldn't find requested items\n" );
+ $dbw->rollback();
+ FileStore::unlock();
+ return false;
+ }
+
+ if( $dbw->numRows( $result ) == 0 ) {
+ // Nothing to do.
+ wfDebug( __METHOD__.": nothing to do\n" );
+ $dbw->rollback();
+ FileStore::unlock();
+ return true;
+ }
+
+ $revisions = 0;
+ while( $row = $dbw->fetchObject( $result ) ) {
+ $revisions++;
+ $store = FileStore::get( $row->fa_storage_group );
+ if( !$store ) {
+ wfDebug( __METHOD__.": skipping row with no file.\n" );
+ continue;
+ }
+
+ if( $revisions == 1 && !$exists ) {
+ $destDir = wfImageDir( $row->fa_name );
+ if ( !is_dir( $destDir ) ) {
+ wfMkdirParents( $destDir );
+ }
+ $destPath = $destDir . DIRECTORY_SEPARATOR . $row->fa_name;
+
+ // We may have to fill in data if this was originally
+ // an archived file revision.
+ if( is_null( $row->fa_metadata ) ) {
+ $tempFile = $store->filePath( $row->fa_storage_key );
+ $metadata = serialize( $this->retrieveExifData( $tempFile ) );
+
+ $magic = wfGetMimeMagic();
+ $mime = $magic->guessMimeType( $tempFile, true );
+ $media_type = $magic->getMediaType( $tempFile, $mime );
+ list( $major_mime, $minor_mime ) = self::splitMime( $mime );
+ } else {
+ $metadata = $row->fa_metadata;
+ $major_mime = $row->fa_major_mime;
+ $minor_mime = $row->fa_minor_mime;
+ $media_type = $row->fa_media_type;
+ }
+
+ $table = 'image';
+ $fields = array(
+ 'img_name' => $row->fa_name,
+ 'img_size' => $row->fa_size,
+ 'img_width' => $row->fa_width,
+ 'img_height' => $row->fa_height,
+ 'img_metadata' => $metadata,
+ 'img_bits' => $row->fa_bits,
+ 'img_media_type' => $media_type,
+ 'img_major_mime' => $major_mime,
+ 'img_minor_mime' => $minor_mime,
+ 'img_description' => $row->fa_description,
+ 'img_user' => $row->fa_user,
+ 'img_user_text' => $row->fa_user_text,
+ 'img_timestamp' => $row->fa_timestamp );
+ } else {
+ $archiveName = $row->fa_archive_name;
+ if( $archiveName == '' ) {
+ // This was originally a current version; we
+ // have to devise a new archive name for it.
+ // Format is <timestamp of archiving>!<name>
+ $archiveName =
+ wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) .
+ '!' . $row->fa_name;
+ }
+ $destDir = wfImageArchiveDir( $row->fa_name );
+ if ( !is_dir( $destDir ) ) {
+ wfMkdirParents( $destDir );
+ }
+ $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName;
+
+ $table = 'oldimage';
+ $fields = array(
+ 'oi_name' => $row->fa_name,
+ 'oi_archive_name' => $archiveName,
+ 'oi_size' => $row->fa_size,
+ 'oi_width' => $row->fa_width,
+ 'oi_height' => $row->fa_height,
+ 'oi_bits' => $row->fa_bits,
+ 'oi_description' => $row->fa_description,
+ 'oi_user' => $row->fa_user,
+ 'oi_user_text' => $row->fa_user_text,
+ 'oi_timestamp' => $row->fa_timestamp );
+ }
+
+ $dbw->insert( $table, $fields, __METHOD__ );
+ /// @fixme this delete is not totally safe, potentially
+ $dbw->delete( 'filearchive',
+ array( 'fa_id' => $row->fa_id ),
+ __METHOD__ );
+
+ // Check if any other stored revisions use this file;
+ // if so, we shouldn't remove the file from the deletion
+ // archives so they will still work.
+ $useCount = $dbw->selectField( 'filearchive',
+ 'COUNT(*)',
+ array(
+ 'fa_storage_group' => $row->fa_storage_group,
+ 'fa_storage_key' => $row->fa_storage_key ),
+ __METHOD__ );
+ if( $useCount == 0 ) {
+ wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
+ $flags = FileStore::DELETE_ORIGINAL;
+ } else {
+ $flags = 0;
+ }
+
+ $transaction->add( $store->export( $row->fa_storage_key,
+ $destPath, $flags ) );
+ }
+
+ $dbw->immediateCommit();
+ } catch( MWException $e ) {
+ wfDebug( __METHOD__." caught error, aborting\n" );
+ $transaction->rollback();
+ throw $e;
+ }
+
+ $transaction->commit();
+ FileStore::unlock();
+
+ if( $revisions > 0 ) {
+ if( !$exists ) {
+ wfDebug( __METHOD__." restored $revisions items, creating a new current\n" );
+
+ // Update site_stats
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+
+ $this->purgeEverything();
+ } else {
+ wfDebug( __METHOD__." restored $revisions as archived versions\n" );
+ $this->purgeDescription();
+ }
+ }
+
+ return $revisions;
+ }
+
+} //class
+
+/**
+ * Wrapper class for thumbnail images
+ * @package MediaWiki
+ */
+class ThumbnailImage {
+ /**
+ * @param string $path Filesystem path to the thumb
+ * @param string $url URL path to the thumb
+ * @private
+ */
+ function ThumbnailImage( $url, $width, $height, $path = false ) {
+ $this->url = $url;
+ $this->width = round( $width );
+ $this->height = round( $height );
+ # These should be integers when they get here.
+ # If not, there's a bug somewhere. But let's at
+ # least produce valid HTML code regardless.
+ $this->path = $path;
+ }
+
+ /**
+ * @return string The thumbnail URL
+ */
+ function getUrl() {
+ return $this->url;
+ }
+
+ /**
+ * Return HTML <img ... /> tag for the thumbnail, will include
+ * width and height attributes and a blank alt text (as required).
+ *
+ * You can set or override additional attributes by passing an
+ * associative array of name => data pairs. The data will be escaped
+ * for HTML output, so should be in plaintext.
+ *
+ * @param array $attribs
+ * @return string
+ * @public
+ */
+ function toHtml( $attribs = array() ) {
+ $attribs['src'] = $this->url;
+ $attribs['width'] = $this->width;
+ $attribs['height'] = $this->height;
+ if( !isset( $attribs['alt'] ) ) $attribs['alt'] = '';
+
+ $html = '<img ';
+ foreach( $attribs as $name => $data ) {
+ $html .= $name . '="' . htmlspecialchars( $data ) . '" ';
+ }
+ $html .= '/>';
+ return $html;
+ }
+
+}
+
+?>
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
new file mode 100644
index 00000000..a66b4d79
--- /dev/null
+++ b/includes/ImageFunctions.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * Returns the image directory of an image
+ * The result is an absolute path.
+ *
+ * This function is called from thumb.php before Setup.php is included
+ *
+ * @param $fname String: file name of the image file.
+ * @public
+ */
+function wfImageDir( $fname ) {
+ global $wgUploadDirectory, $wgHashedUploadDirectory;
+
+ if (!$wgHashedUploadDirectory) { return $wgUploadDirectory; }
+
+ $hash = md5( $fname );
+ $dest = $wgUploadDirectory . '/' . $hash{0} . '/' . substr( $hash, 0, 2 );
+
+ return $dest;
+}
+
+/**
+ * Returns the image directory of an image's thubnail
+ * The result is an absolute path.
+ *
+ * This function is called from thumb.php before Setup.php is included
+ *
+ * @param $fname String: file name of the original image file
+ * @param $shared Boolean: (optional) use the shared upload directory (default: 'false').
+ * @public
+ */
+function wfImageThumbDir( $fname, $shared = false ) {
+ $base = wfImageArchiveDir( $fname, 'thumb', $shared );
+ if ( Image::isHashed( $shared ) ) {
+ $dir = "$base/$fname";
+ } else {
+ $dir = $base;
+ }
+
+ return $dir;
+}
+
+/**
+ * Old thumbnail directory, kept for conversion
+ */
+function wfDeprecatedThumbDir( $thumbName , $subdir='thumb', $shared=false) {
+ return wfImageArchiveDir( $thumbName, $subdir, $shared );
+}
+
+/**
+ * Returns the image directory of an image's old version
+ * The result is an absolute path.
+ *
+ * This function is called from thumb.php before Setup.php is included
+ *
+ * @param $fname String: file name of the thumbnail file, including file size prefix.
+ * @param $subdir String: subdirectory of the image upload directory that should be used for storing the old version. Default is 'archive'.
+ * @param $shared Boolean use the shared upload directory (only relevant for other functions which call this one). Default is 'false'.
+ * @public
+ */
+function wfImageArchiveDir( $fname , $subdir='archive', $shared=false ) {
+ global $wgUploadDirectory, $wgHashedUploadDirectory;
+ global $wgSharedUploadDirectory, $wgHashedSharedUploadDirectory;
+ $dir = $shared ? $wgSharedUploadDirectory : $wgUploadDirectory;
+ $hashdir = $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory;
+ if (!$hashdir) { return $dir.'/'.$subdir; }
+ $hash = md5( $fname );
+
+ return $dir.'/'.$subdir.'/'.$hash[0].'/'.substr( $hash, 0, 2 );
+}
+
+
+/*
+ * Return the hash path component of an image path (URL or filesystem),
+ * e.g. "/3/3c/", or just "/" if hashing is not used.
+ *
+ * @param $dbkey The filesystem / database name of the file
+ * @param $fromSharedDirectory Use the shared file repository? It may
+ * use different hash settings from the local one.
+ */
+function wfGetHashPath ( $dbkey, $fromSharedDirectory = false ) {
+ if( Image::isHashed( $fromSharedDirectory ) ) {
+ $hash = md5($dbkey);
+ return '/' . $hash{0} . '/' . substr( $hash, 0, 2 ) . '/';
+ } else {
+ return '/';
+ }
+}
+
+/**
+ * Returns the image URL of an image's old version
+ *
+ * @param $name String: file name of the image file
+ * @param $subdir String: (optional) subdirectory of the image upload directory that is used by the old version. Default is 'archive'
+ * @public
+ */
+function wfImageArchiveUrl( $name, $subdir='archive' ) {
+ global $wgUploadPath, $wgHashedUploadDirectory;
+
+ if ($wgHashedUploadDirectory) {
+ $hash = md5( substr( $name, 15) );
+ $url = $wgUploadPath.'/'.$subdir.'/' . $hash{0} . '/' .
+ substr( $hash, 0, 2 ) . '/'.$name;
+ } else {
+ $url = $wgUploadPath.'/'.$subdir.'/'.$name;
+ }
+ return wfUrlencode($url);
+}
+
+/**
+ * Return a rounded pixel equivalent for a labeled CSS/SVG length.
+ * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
+ *
+ * @param $length String: CSS/SVG length.
+ * @return Integer: length in pixels
+ */
+function wfScaleSVGUnit( $length ) {
+ static $unitLength = array(
+ 'px' => 1.0,
+ 'pt' => 1.25,
+ 'pc' => 15.0,
+ 'mm' => 3.543307,
+ 'cm' => 35.43307,
+ 'in' => 90.0,
+ '' => 1.0, // "User units" pixels by default
+ '%' => 2.0, // Fake it!
+ );
+ if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) {
+ $length = floatval( $matches[1] );
+ $unit = $matches[2];
+ return round( $length * $unitLength[$unit] );
+ } else {
+ // Assume pixels
+ return round( floatval( $length ) );
+ }
+}
+
+/**
+ * Compatible with PHP getimagesize()
+ * @todo support gzipped SVGZ
+ * @todo check XML more carefully
+ * @todo sensible defaults
+ *
+ * @param $filename String: full name of the file (passed to php fopen()).
+ * @return array
+ */
+function wfGetSVGsize( $filename ) {
+ $width = 256;
+ $height = 256;
+
+ // Read a chunk of the file
+ $f = fopen( $filename, "rt" );
+ if( !$f ) return false;
+ $chunk = fread( $f, 4096 );
+ fclose( $f );
+
+ // Uber-crappy hack! Run through a real XML parser.
+ if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) {
+ return false;
+ }
+ $tag = $matches[1];
+ if( preg_match( '/\bwidth\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
+ $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
+ }
+ if( preg_match( '/\bheight\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
+ $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
+ }
+
+ return array( $width, $height, 'SVG',
+ "width=\"$width\" height=\"$height\"" );
+}
+
+/**
+ * Determine if an image exists on the 'bad image list'.
+ *
+ * @param $name String: the image name to check
+ * @return bool
+ */
+function wfIsBadImage( $name ) {
+ static $titleList = false;
+ wfProfileIn( __METHOD__ );
+ $bad = false;
+ if( wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
+ if( !$titleList ) {
+ # Build the list now
+ $titleList = array();
+ $lines = explode( "\n", wfMsgForContent( 'bad_image_list' ) );
+ foreach( $lines as $line ) {
+ if( preg_match( '/^\*\s*\[\[:?(.*?)\]\]/i', $line, $matches ) ) {
+ $title = Title::newFromText( $matches[1] );
+ if( is_object( $title ) && $title->getNamespace() == NS_IMAGE )
+ $titleList[ $title->getDBkey() ] = true;
+ }
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ return array_key_exists( $name, $titleList );
+ } else {
+ wfProfileOut( __METHOD__ );
+ return $bad;
+ }
+}
+
+/**
+ * Calculate the largest thumbnail width for a given original file size
+ * such that the thumbnail's height is at most $maxHeight.
+ * @param $boxWidth Integer Width of the thumbnail box.
+ * @param $boxHeight Integer Height of the thumbnail box.
+ * @param $maxHeight Integer Maximum height expected for the thumbnail.
+ * @return Integer.
+ */
+function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
+ $idealWidth = $boxWidth * $maxHeight / $boxHeight;
+ $roundedUp = ceil( $idealWidth );
+ if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight )
+ return floor( $idealWidth );
+ else
+ return $roundedUp;
+}
+
+
+?>
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
new file mode 100644
index 00000000..0935ac30
--- /dev/null
+++ b/includes/ImageGallery.php
@@ -0,0 +1,211 @@
+<?php
+if ( ! defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+/**
+ * @package MediaWiki
+ */
+
+/**
+ * Image gallery
+ *
+ * Add images to the gallery using add(), then render that list to HTML using toHTML().
+ *
+ * @package MediaWiki
+ */
+class ImageGallery
+{
+ var $mImages, $mShowBytes, $mShowFilename;
+ var $mCaption = false;
+ var $mSkin = false;
+
+ /**
+ * Is the gallery on a wiki page (i.e. not a special page)
+ */
+ var $mParsing;
+
+ /**
+ * Create a new image gallery object.
+ */
+ function ImageGallery( ) {
+ $this->mImages = array();
+ $this->mShowBytes = true;
+ $this->mShowFilename = true;
+ $this->mParsing = false;
+ }
+
+ /**
+ * Set the "parse" bit so we know to hide "bad" images
+ */
+ function setParsing( $val = true ) {
+ $this->mParsing = $val;
+ }
+
+ /**
+ * Set the caption
+ *
+ * @param $caption Caption
+ */
+ function setCaption( $caption ) {
+ $this->mCaption = $caption;
+ }
+
+ /**
+ * Instruct the class to use a specific skin for rendering
+ *
+ * @param $skin Skin object
+ */
+ function useSkin( &$skin ) {
+ $this->mSkin =& $skin;
+ }
+
+ /**
+ * Return the skin that should be used
+ *
+ * @return Skin object
+ */
+ function getSkin() {
+ if( !$this->mSkin ) {
+ global $wgUser;
+ $skin =& $wgUser->getSkin();
+ } else {
+ $skin =& $this->mSkin;
+ }
+ return $skin;
+ }
+
+ /**
+ * Add an image to the gallery.
+ *
+ * @param $image Image object that is added to the gallery
+ * @param $html String: additional HTML text to be shown. The name and size of the image are always shown.
+ */
+ function add( $image, $html='' ) {
+ $this->mImages[] = array( &$image, $html );
+ }
+
+ /**
+ * Add an image at the beginning of the gallery.
+ *
+ * @param $image Image object that is added to the gallery
+ * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
+ */
+ function insert( $image, $html='' ) {
+ array_unshift( $this->mImages, array( &$image, $html ) );
+ }
+
+
+ /**
+ * isEmpty() returns true if the gallery contains no images
+ */
+ function isEmpty() {
+ return empty( $this->mImages );
+ }
+
+ /**
+ * Enable/Disable showing of the file size of an image in the gallery.
+ * Enabled by default.
+ *
+ * @param $f Boolean: set to false to disable.
+ */
+ function setShowBytes( $f ) {
+ $this->mShowBytes = ( $f == true);
+ }
+
+ /**
+ * Enable/Disable showing of the filename of an image in the gallery.
+ * Enabled by default.
+ *
+ * @param $f Boolean: set to false to disable.
+ */
+ function setShowFilename( $f ) {
+ $this->mShowFilename = ( $f == true);
+ }
+
+ /**
+ * Return a HTML representation of the image gallery
+ *
+ * For each image in the gallery, display
+ * - a thumbnail
+ * - the image name
+ * - the additional text provided when adding the image
+ * - the size of the image
+ *
+ */
+ function toHTML() {
+ global $wgLang, $wgIgnoreImageErrors, $wgGenerateThumbnailOnParse;
+
+ $sk =& $this->getSkin();
+
+ $s = '<table class="gallery" cellspacing="0" cellpadding="0">';
+ if( $this->mCaption )
+ $s .= '<td class="galleryheader" colspan="4"><big>' . htmlspecialchars( $this->mCaption ) . '</big></td>';
+
+ $i = 0;
+ foreach ( $this->mImages as $pair ) {
+ $img =& $pair[0];
+ $text = $pair[1];
+
+ $name = $img->getName();
+ $nt = $img->getTitle();
+
+ if( $nt->getNamespace() != NS_IMAGE ) {
+ # We're dealing with a non-image, spit out the name and be done with it.
+ $thumbhtml = '<div style="height: 152px;">' . htmlspecialchars( $nt->getText() ) . '</div>';
+ }
+ else if( $this->mParsing && wfIsBadImage( $nt->getDBkey() ) ) {
+ # The image is blacklisted, just show it as a text link.
+ $thumbhtml = '<div style="height: 152px;">'
+ . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>';
+ }
+ else if( !( $thumb = $img->getThumbnail( 120, 120, $wgGenerateThumbnailOnParse ) ) ) {
+ # Error generating thumbnail.
+ $thumbhtml = '<div style="height: 152px;">'
+ . htmlspecialchars( $img->getLastError() ) . '</div>';
+ }
+ else {
+ $vpad = floor( ( 150 - $thumb->height ) /2 ) - 2;
+ $thumbhtml = '<div class="thumb" style="padding: ' . $vpad . 'px 0;">'
+ . $sk->makeKnownLinkObj( $nt, $thumb->toHtml() ) . '</div>';
+ }
+
+ //TODO
+ //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut );
+
+ if( $this->mShowBytes ) {
+ if( $img->exists() ) {
+ $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $img->getSize() ) );
+ } else {
+ $nb = wfMsgHtml( 'filemissing' );
+ }
+ $nb = "$nb<br />\n";
+ } else {
+ $nb = '';
+ }
+
+ $textlink = $this->mShowFilename ?
+ $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20, '...' ) ) ) . "<br />\n" :
+ '' ;
+
+ # ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which
+ # in version 4.8.6 generated crackpot html in its absence, see:
+ # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar
+
+ $s .= ($i%4==0) ? '<tr>' : '';
+ $s .= '<td><div class="gallerybox">' . $thumbhtml
+ . '<div class="gallerytext">' . "\n" . $textlink . $text . $nb
+ . "</div></div></td>\n";
+ $s .= ($i%4==3) ? '</tr>' : '';
+ $i++;
+ }
+ if( $i %4 != 0 ) {
+ $s .= "</tr>\n";
+ }
+ $s .= '</table>';
+
+ return $s;
+ }
+
+} //class
+?>
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
new file mode 100644
index 00000000..dac9602d
--- /dev/null
+++ b/includes/ImagePage.php
@@ -0,0 +1,726 @@
+<?php
+/**
+ * @package MediaWiki
+ */
+
+/**
+ *
+ */
+if( !defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+require_once( 'Image.php' );
+
+/**
+ * Special handling for image description pages
+ * @package MediaWiki
+ */
+class ImagePage extends Article {
+
+ /* private */ var $img; // Image object this page is shown for
+ var $mExtraDescription = false;
+
+ /**
+ * Handler for action=render
+ * Include body text only; none of the image extras
+ */
+ function render() {
+ global $wgOut;
+ $wgOut->setArticleBodyOnly( true );
+ $wgOut->addSecondaryWikitext( $this->getContent() );
+ }
+
+ function view() {
+ global $wgOut, $wgShowEXIF;
+
+ $this->img = new Image( $this->mTitle );
+
+ if( $this->mTitle->getNamespace() == NS_IMAGE ) {
+ if ($wgShowEXIF && $this->img->exists()) {
+ $exif = $this->img->getExifData();
+ $showmeta = count($exif) ? true : false;
+ } else {
+ $exif = false;
+ $showmeta = false;
+ }
+
+ if ($this->img->exists())
+ $wgOut->addHTML($this->showTOC($showmeta));
+
+ $this->openShowImage();
+
+ # No need to display noarticletext, we use our own message, output in openShowImage()
+ if( $this->getID() ) {
+ Article::view();
+ } else {
+ # Just need to set the right headers
+ $wgOut->setArticleFlag( true );
+ $wgOut->setRobotpolicy( 'index,follow' );
+ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ $this->viewUpdates();
+ }
+
+ # Show shared description, if needed
+ if( $this->mExtraDescription ) {
+ $fol = wfMsg( 'shareddescriptionfollows' );
+ if( $fol != '-' ) {
+ $wgOut->addWikiText( $fol );
+ }
+ $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' );
+ }
+
+ $this->closeShowImage();
+ $this->imageHistory();
+ $this->imageLinks();
+ if( $exif ) {
+ global $wgStylePath;
+ $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) );
+ $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) );
+ $wgOut->addHTML( "<h2 id=\"metadata\">" . wfMsgHtml( 'metadata' ) . "</h2>\n" );
+ $wgOut->addWikiText( $this->makeMetadataTable( $exif ) );
+ $wgOut->addHTML(
+ "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js\"></script>\n" .
+ "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" );
+ }
+ } else {
+ Article::view();
+ }
+ }
+
+ /**
+ * Create the TOC
+ *
+ * @access private
+ *
+ * @param bool $metadata Whether or not to show the metadata link
+ * @return string
+ */
+ function showTOC( $metadata ) {
+ global $wgLang;
+ $r = '<ul id="filetoc">
+ <li><a href="#file">' . $wgLang->getNsText( NS_IMAGE ) . '</a></li>
+ <li><a href="#filehistory">' . wfMsgHtml( 'imghistory' ) . '</a></li>
+ <li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>' .
+ ($metadata ? '<li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . '
+ </ul>';
+ return $r;
+ }
+
+ /**
+ * Make a table with metadata to be shown in the output page.
+ *
+ * @access private
+ *
+ * @param array $exif The array containing the EXIF data
+ * @return string
+ */
+ function makeMetadataTable( $exif ) {
+ $r = wfMsg( 'metadata-help' ) . "\n\n";
+ $r .= "{| id=mw_metadata class=mw_metadata\n";
+ $visibleFields = $this->visibleMetadataFields();
+ foreach( $exif as $k => $v ) {
+ $tag = strtolower( $k );
+ $msg = wfMsg( "exif-$tag" );
+ $class = "exif-$tag";
+ if( !in_array( $tag, $visibleFields ) ) {
+ $class .= ' collapsable';
+ }
+ $r .= "|- class=\"$class\"\n";
+ $r .= "!| $msg\n";
+ $r .= "|| $v\n";
+ }
+ $r .= '|}';
+ return $r;
+ }
+
+ /**
+ * Get a list of EXIF metadata items which should be displayed when
+ * the metadata table is collapsed.
+ *
+ * @return array of strings
+ * @access private
+ */
+ function visibleMetadataFields() {
+ $fields = array();
+ $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
+ foreach( $lines as $line ) {
+ if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
+ $fields[] = $matches[1];
+ }
+ }
+ return $fields;
+ }
+
+ /**
+ * Overloading Article's getContent method.
+ *
+ * Omit noarticletext if sharedupload; text will be fetched from the
+ * shared upload server if possible.
+ */
+ function getContent() {
+ if( $this->img && $this->img->fromSharedDirectory && 0 == $this->getID() ) {
+ return '';
+ }
+ return Article::getContent();
+ }
+
+ function openShowImage() {
+ global $wgOut, $wgUser, $wgImageLimits, $wgRequest;
+ global $wgUseImageResize, $wgGenerateThumbnailOnParse;
+
+ $full_url = $this->img->getURL();
+ $anchoropen = '';
+ $anchorclose = '';
+
+ if( $wgUser->getOption( 'imagesize' ) == '' ) {
+ $sizeSel = User::getDefaultOption( 'imagesize' );
+ } else {
+ $sizeSel = intval( $wgUser->getOption( 'imagesize' ) );
+ }
+ if( !isset( $wgImageLimits[$sizeSel] ) ) {
+ $sizeSel = User::getDefaultOption( 'imagesize' );
+ }
+ $max = $wgImageLimits[$sizeSel];
+ $maxWidth = $max[0];
+ $maxHeight = $max[1];
+ $sk = $wgUser->getSkin();
+
+ if ( $this->img->exists() ) {
+ # image
+ $width = $this->img->getWidth();
+ $height = $this->img->getHeight();
+ $showLink = false;
+
+ if ( $this->img->allowInlineDisplay() and $width and $height) {
+ # image
+
+ # "Download high res version" link below the image
+ $msg = wfMsgHtml('showbigimage', $width, $height, intval( $this->img->getSize()/1024 ) );
+
+ # We'll show a thumbnail of this image
+ if ( $width > $maxWidth || $height > $maxHeight ) {
+ # Calculate the thumbnail size.
+ # First case, the limiting factor is the width, not the height.
+ if ( $width / $height >= $maxWidth / $maxHeight ) {
+ $height = round( $height * $maxWidth / $width);
+ $width = $maxWidth;
+ # Note that $height <= $maxHeight now.
+ } else {
+ $newwidth = floor( $width * $maxHeight / $height);
+ $height = round( $height * $newwidth / $width );
+ $width = $newwidth;
+ # Note that $height <= $maxHeight now, but might not be identical
+ # because of rounding.
+ }
+
+ if( $wgUseImageResize ) {
+ $thumbnail = $this->img->getThumbnail( $width, -1, $wgGenerateThumbnailOnParse );
+ if ( $thumbnail == null ) {
+ $url = $this->img->getViewURL();
+ } else {
+ $url = $thumbnail->getURL();
+ }
+ } else {
+ # No resize ability? Show the full image, but scale
+ # it down in the browser so it fits on the page.
+ $url = $this->img->getViewURL();
+ }
+ $anchoropen = "<a href=\"{$full_url}\">";
+ $anchorclose = "</a><br />";
+ if( $this->img->mustRender() ) {
+ $showLink = true;
+ } else {
+ $anchorclose .= "\n$anchoropen{$msg}</a>";
+ }
+ } else {
+ $url = $this->img->getViewURL();
+ $showLink = true;
+ }
+ $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $anchoropen .
+ "<img border=\"0\" src=\"{$url}\" width=\"{$width}\" height=\"{$height}\" alt=\"" .
+ htmlspecialchars( $wgRequest->getVal( 'image' ) ).'" />' . $anchorclose . '</div>' );
+ } else {
+ #if direct link is allowed but it's not a renderable image, show an icon.
+ if ($this->img->isSafeFile()) {
+ $icon= $this->img->iconThumb();
+
+ $wgOut->addHTML( '<div class="fullImageLink" id="file"><a href="' . $full_url . '">' .
+ $icon->toHtml() .
+ '</a></div>' );
+ }
+
+ $showLink = true;
+ }
+
+
+ if ($showLink) {
+ $filename = wfEscapeWikiText( $this->img->getName() );
+ $info = wfMsg( 'fileinfo',
+ ceil($this->img->getSize()/1024.0),
+ $this->img->getMimeType() );
+
+ global $wgContLang;
+ $dirmark = $wgContLang->getDirMark();
+ if (!$this->img->isSafeFile()) {
+ $warning = wfMsg( 'mediawarning' );
+ $wgOut->addWikiText( <<<END
+<div class="fullMedia">
+<span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark
+<span class="fileInfo"> ($info)</span>
+</div>
+
+<div class="mediaWarning">$warning</div>
+END
+ );
+ } else {
+ $wgOut->addWikiText( <<<END
+<div class="fullMedia">
+[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> ($info)</span>
+</div>
+END
+ );
+ }
+ }
+
+ if($this->img->fromSharedDirectory) {
+ $this->printSharedImageText();
+ }
+ } else {
+ # Image does not exist
+
+ $title = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'),
+ 'wpDestFile=' . urlencode( $this->img->getName() ) );
+ $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) );
+ }
+ }
+
+ function printSharedImageText() {
+ global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut, $wgUser;
+
+ $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey());
+ $sharedtext = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml("sharedupload");
+ if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) {
+
+ $sk = $wgUser->getSkin();
+ $title = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'),
+ array( 'wpDestFile' => urlencode( $this->img->getName() )));
+ $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link);
+ }
+ $sharedtext .= "</div>";
+ $wgOut->addHTML($sharedtext);
+
+ if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) {
+ require_once("HttpFunctions.php");
+ $ur = ini_set('allow_url_fopen', true);
+ $text = wfGetHTTP($url . '?action=render');
+ ini_set('allow_url_fopen', $ur);
+ if ($text)
+ $this->mExtraDescription = $text;
+ }
+ }
+
+ function getUploadUrl() {
+ global $wgServer;
+ $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) );
+ }
+
+ /**
+ * Print out the various links at the bottom of the image page, e.g. reupload,
+ * external editing (and instructions link) etc.
+ */
+ function uploadLinksBox() {
+ global $wgUser, $wgOut;
+
+ if( $this->img->fromSharedDirectory )
+ return;
+
+ $sk = $wgUser->getSkin();
+
+ $wgOut->addHtml( '<br /><ul>' );
+
+ # "Upload a new version of this file" link
+ if( $wgUser->isAllowed( 'reupload' ) ) {
+ $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
+ $wgOut->addHtml( "<li><div>{$ulink}</div></li>" );
+ }
+
+ # External editing link
+ $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' );
+ $wgOut->addHtml( '<li>' . $elink . '<div>' . wfMsgWikiHtml( 'edit-externally-help' ) . '</div></li>' );
+
+ $wgOut->addHtml( '</ul>' );
+ }
+
+ function closeShowImage()
+ {
+ # For overloading
+
+ }
+
+ /**
+ * If the page we've just displayed is in the "Image" namespace,
+ * we follow it with an upload history of the image and its usage.
+ */
+ function imageHistory()
+ {
+ global $wgUser, $wgOut, $wgUseExternalEditor;
+
+ $sk = $wgUser->getSkin();
+
+ $line = $this->img->nextHistoryLine();
+
+ if ( $line ) {
+ $list =& new ImageHistoryList( $sk );
+ $s = $list->beginImageHistoryList() .
+ $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp),
+ $this->mTitle->getDBkey(), $line->img_user,
+ $line->img_user_text, $line->img_size, $line->img_description,
+ $line->img_width, $line->img_height
+ );
+
+ while ( $line = $this->img->nextHistoryLine() ) {
+ $s .= $list->imageHistoryLine( false, $line->img_timestamp,
+ $line->oi_archive_name, $line->img_user,
+ $line->img_user_text, $line->img_size, $line->img_description,
+ $line->img_width, $line->img_height
+ );
+ }
+ $s .= $list->endImageHistoryList();
+ } else { $s=''; }
+ $wgOut->addHTML( $s );
+
+ # Exist check because we don't want to show this on pages where an image
+ # doesn't exist along with the noimage message, that would suck. -ævar
+ if( $wgUseExternalEditor && $this->img->exists() ) {
+ $this->uploadLinksBox();
+ }
+
+ }
+
+ function imageLinks()
+ {
+ global $wgUser, $wgOut;
+
+ $wgOut->addHTML( '<h2 id="filelinks">' . wfMsg( 'imagelinks' ) . "</h2>\n" );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $imagelinks = $dbr->tableName( 'imagelinks' );
+
+ $sql = "SELECT page_namespace,page_title FROM $imagelinks,$page WHERE il_to=" .
+ $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id";
+ $sql = $dbr->limitResult($sql, 500, 0);
+ $res = $dbr->query( $sql, "ImagePage::imageLinks" );
+
+ if ( 0 == $dbr->numRows( $res ) ) {
+ $wgOut->addHtml( '<p>' . wfMsg( "nolinkstoimage" ) . "</p>\n" );
+ return;
+ }
+ $wgOut->addHTML( '<p>' . wfMsg( 'linkstoimage' ) . "</p>\n<ul>" );
+
+ $sk = $wgUser->getSkin();
+ while ( $s = $dbr->fetchObject( $res ) ) {
+ $name = Title::MakeTitle( $s->page_namespace, $s->page_title );
+ $link = $sk->makeKnownLinkObj( $name, "" );
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+ $wgOut->addHTML( "</ul>\n" );
+ }
+
+ function delete()
+ {
+ global $wgUser, $wgOut, $wgRequest;
+
+ $confirm = $wgRequest->wasPosted();
+ $image = $wgRequest->getVal( 'image' );
+ $oldimage = $wgRequest->getVal( 'oldimage' );
+
+ # Only sysops can delete images. Previously ordinary users could delete
+ # old revisions, but this is no longer the case.
+ if ( !$wgUser->isAllowed('delete') ) {
+ $wgOut->sysopRequired();
+ return;
+ }
+ if ( $wgUser->isBlocked() ) {
+ return $this->blockedIPpage();
+ }
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ # Better double-check that it hasn't been deleted yet!
+ $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
+ if ( ( !is_null( $image ) )
+ && ( '' == trim( $image ) ) ) {
+ $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
+ return;
+ }
+
+ $this->img = new Image( $this->mTitle );
+
+ # Deleting old images doesn't require confirmation
+ if ( !is_null( $oldimage ) || $confirm ) {
+ if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) {
+ $this->doDelete();
+ } else {
+ $wgOut->showFatalError( wfMsg( 'sessionfailure' ) );
+ }
+ return;
+ }
+
+ if ( !is_null( $image ) ) {
+ $q = '&image=' . urlencode( $image );
+ } else if ( !is_null( $oldimage ) ) {
+ $q = '&oldimage=' . urlencode( $oldimage );
+ } else {
+ $q = '';
+ }
+ return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) );
+ }
+
+ function doDelete() {
+ global $wgOut, $wgRequest, $wgUseSquid;
+ global $wgPostCommitUpdateList;
+
+ $fname = 'ImagePage::doDelete';
+
+ $reason = $wgRequest->getVal( 'wpReason' );
+ $oldimage = $wgRequest->getVal( 'oldimage' );
+
+ $dbw =& wfGetDB( DB_MASTER );
+
+ if ( !is_null( $oldimage ) ) {
+ if ( strlen( $oldimage ) < 16 ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
+ return;
+ }
+ if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
+ return;
+ }
+ if ( !$this->doDeleteOldImage( $oldimage ) ) {
+ return;
+ }
+ $deleted = $oldimage;
+ } else {
+ $ok = $this->img->delete( $reason );
+ if( !$ok ) {
+ # If the deletion operation actually failed, bug out:
+ $wgOut->showFileDeleteError( $this->img->getName() );
+ return;
+ }
+
+ # Image itself is now gone, and database is cleaned.
+ # Now we remove the image description page.
+
+ $article = new Article( $this->mTitle );
+ $article->doDeleteArticle( $reason ); # ignore errors
+
+ $deleted = $this->img->getName();
+ }
+
+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+ $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
+ $text = wfMsg( 'deletedtext', $deleted, $loglink );
+
+ $wgOut->addWikiText( $text );
+
+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
+ }
+
+ /**
+ * @return success
+ */
+ function doDeleteOldImage( $oldimage )
+ {
+ global $wgOut;
+
+ $ok = $this->img->deleteOld( $oldimage, '' );
+ if( !$ok ) {
+ # If we actually have a file and can't delete it, throw an error.
+ # Something went awry...
+ $wgOut->showFileDeleteError( "$oldimage" );
+ } else {
+ # Log the deletion
+ $log = new LogPage( 'delete' );
+ $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) );
+ }
+ return $ok;
+ }
+
+ function revert() {
+ global $wgOut, $wgRequest, $wgUser;
+
+ $oldimage = $wgRequest->getText( 'oldimage' );
+ if ( strlen( $oldimage ) < 16 ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
+ return;
+ }
+ if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
+ return;
+ }
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if( $wgUser->isAnon() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ return;
+ }
+ if ( ! $this->mTitle->userCanEdit() ) {
+ $wgOut->sysopRequired();
+ return;
+ }
+ if ( $wgUser->isBlocked() ) {
+ return $this->blockedIPpage();
+ }
+ if( !$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) {
+ $wgOut->showErrorPage( 'internalerror', 'sessionfailure' );
+ return;
+ }
+ $name = substr( $oldimage, 15 );
+
+ $dest = wfImageDir( $name );
+ $archive = wfImageArchiveDir( $name );
+ $curfile = "{$dest}/{$name}";
+
+ if ( !is_dir( $dest ) ) wfMkdirParents( $dest );
+ if ( !is_dir( $archive ) ) wfMkdirParents( $archive );
+
+ if ( ! is_file( $curfile ) ) {
+ $wgOut->showFileNotFoundError( htmlspecialchars( $curfile ) );
+ return;
+ }
+ $oldver = wfTimestampNow() . "!{$name}";
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $size = $dbr->selectField( 'oldimage', 'oi_size', array( 'oi_archive_name' => $oldimage ) );
+
+ if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) {
+ $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" );
+ return;
+ }
+ if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) {
+ $wgOut->showFileCopyError( "${archive}/{$oldimage}", $curfile );
+ return;
+ }
+
+ # Record upload and update metadata cache
+ $img = Image::newFromName( $name );
+ $img->recordUpload( $oldver, wfMsg( "reverted" ) );
+
+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->addHTML( wfMsg( 'imagereverted' ) );
+
+ $descTitle = $img->getTitle();
+ $wgOut->returnToMain( false, $descTitle->getPrefixedText() );
+ }
+
+ function blockedIPpage() {
+ $edit = new EditPage( $this );
+ return $edit->blockedIPpage();
+ }
+
+ /**
+ * Override handling of action=purge
+ */
+ function doPurge() {
+ $this->img = new Image( $this->mTitle );
+ if( $this->img->exists() ) {
+ wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" );
+ $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
+ $update->doUpdate();
+ $this->img->purgeCache();
+ } else {
+ wfDebug( "ImagePage::doPurge no image\n" );
+ }
+ parent::doPurge();
+ }
+
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class ImageHistoryList {
+ function ImageHistoryList( &$skin ) {
+ $this->skin =& $skin;
+ }
+
+ function beginImageHistoryList() {
+ $s = "\n<h2 id=\"filehistory\">" . wfMsg( 'imghistory' ) . "</h2>\n" .
+ "<p>" . wfMsg( 'imghistlegend' ) . "</p>\n".'<ul class="special">';
+ return $s;
+ }
+
+ function endImageHistoryList() {
+ $s = "</ul>\n";
+ return $s;
+ }
+
+ function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $width, $height ) {
+ global $wgUser, $wgLang, $wgTitle, $wgContLang;
+
+ $datetime = $wgLang->timeanddate( $timestamp, true );
+ $del = wfMsg( 'deleteimg' );
+ $delall = wfMsg( 'deleteimgcompletely' );
+ $cur = wfMsg( 'cur' );
+
+ if ( $iscur ) {
+ $url = Image::imageUrl( $img );
+ $rlink = $cur;
+ if ( $wgUser->isAllowed('delete') ) {
+ $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() .
+ '&action=delete' );
+ $style = $this->skin->getInternalLinkAttributes( $link, $delall );
+
+ $dlink = '<a href="'.$link.'"'.$style.'>'.$delall.'</a>';
+ } else {
+ $dlink = $del;
+ }
+ } else {
+ $url = htmlspecialchars( wfImageArchiveUrl( $img ) );
+ if( $wgUser->getID() != 0 && $wgTitle->userCanEdit() ) {
+ $token = urlencode( $wgUser->editToken( $img ) );
+ $rlink = $this->skin->makeKnownLinkObj( $wgTitle,
+ wfMsg( 'revertimg' ), 'action=revert&oldimage=' .
+ urlencode( $img ) . "&wpEditToken=$token" );
+ $dlink = $this->skin->makeKnownLinkObj( $wgTitle,
+ $del, 'action=delete&oldimage=' . urlencode( $img ) .
+ "&wpEditToken=$token" );
+ } else {
+ # Having live active links for non-logged in users
+ # means that bots and spiders crawling our site can
+ # inadvertently change content. Baaaad idea.
+ $rlink = wfMsg( 'revertimg' );
+ $dlink = $del;
+ }
+ }
+
+ $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
+ $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $size ) );
+ $widthheight = wfMsg( 'widthheight', $width, $height );
+ $style = $this->skin->getInternalLinkAttributes( $url, $datetime );
+
+ $s = "<li> ({$dlink}) ({$rlink}) <a href=\"{$url}\"{$style}>{$datetime}</a> . . {$userlink} . . {$widthheight} ({$nbytes})";
+
+ $s .= $this->skin->commentBlock( $description, $wgTitle );
+ $s .= "</li>\n";
+ return $s;
+ }
+
+}
+
+
+?>
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
new file mode 100644
index 00000000..746cf5de
--- /dev/null
+++ b/includes/JobQueue.php
@@ -0,0 +1,267 @@
+<?php
+
+if ( !defined( 'MEDIAWIKI' ) ) {
+ die( "This file is part of MediaWiki, it is not a valid entry point\n" );
+}
+
+abstract class Job {
+ var $command,
+ $title,
+ $params,
+ $id,
+ $removeDuplicates,
+ $error;
+
+ /*-------------------------------------------------------------------------
+ * Static functions
+ *------------------------------------------------------------------------*/
+
+ /**
+ * @deprecated use LinksUpdate::queueRecursiveJobs()
+ */
+ /**
+ * static function queueLinksJobs( $titles ) {}
+ */
+
+ /**
+ * Pop a job off the front of the queue
+ * @static
+ * @return Job or false if there's no jobs
+ */
+ static function pop() {
+ wfProfileIn( __METHOD__ );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ // Get a job from the slave
+ $row = $dbr->selectRow( 'job', '*', '', __METHOD__,
+ array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 )
+ );
+
+ if ( $row === false ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ // Try to delete it from the master
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
+ $affected = $dbw->affectedRows();
+ $dbw->immediateCommit();
+
+ if ( !$affected ) {
+ // Failed, someone else beat us to it
+ // Try getting a random row
+ $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob',
+ 'MAX(job_id) as maxjob' ), '', __METHOD__ );
+ if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) {
+ // No jobs to get
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ // Get the random row
+ $row = $dbw->selectRow( 'job', '*',
+ array( 'job_id' => mt_rand( $row->minjob, $row->maxjob ) ), __METHOD__ );
+ if ( $row === false ) {
+ // Random job gone before we got the chance to select it
+ // Give up
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ // Delete the random row
+ $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
+ $affected = $dbw->affectedRows();
+ $dbw->immediateCommit();
+
+ if ( !$affected ) {
+ // Random job gone before we exclusively deleted it
+ // Give up
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ }
+
+ // If execution got to here, there's a row in $row that has been deleted from the database
+ // by this thread. Hence the concurrent pop was successful.
+ $namespace = $row->job_namespace;
+ $dbkey = $row->job_title;
+ $title = Title::makeTitleSafe( $namespace, $dbkey );
+ $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
+
+ // Remove any duplicates it may have later in the queue
+ $dbw->delete( 'job', $job->insertFields(), __METHOD__ );
+
+ wfProfileOut( __METHOD__ );
+ return $job;
+ }
+
+ /**
+ * Create an object of a subclass
+ */
+ static function factory( $command, $title, $params = false, $id = 0 ) {
+ switch ( $command ) {
+ case 'refreshLinks':
+ return new RefreshLinksJob( $title, $params, $id );
+ case 'htmlCacheUpdate':
+ case 'html_cache_update': # BC
+ return new HTMLCacheUpdateJob( $title, $params['table'], $params['start'], $params['end'], $id );
+ default:
+ throw new MWException( "Invalid job command \"$command\"" );
+ }
+ }
+
+ static function makeBlob( $params ) {
+ if ( $params !== false ) {
+ return serialize( $params );
+ } else {
+ return '';
+ }
+ }
+
+ static function extractBlob( $blob ) {
+ if ( (string)$blob !== '' ) {
+ return unserialize( $blob );
+ } else {
+ return false;
+ }
+ }
+
+ /*-------------------------------------------------------------------------
+ * Non-static functions
+ *------------------------------------------------------------------------*/
+
+ function __construct( $command, $title, $params = false, $id = 0 ) {
+ $this->command = $command;
+ $this->title = $title;
+ $this->params = $params;
+ $this->id = $id;
+
+ // A bit of premature generalisation
+ // Oh well, the whole class is premature generalisation really
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * Insert a single job into the queue.
+ */
+ function insert() {
+ $fields = $this->insertFields();
+
+ $dbw =& wfGetDB( DB_MASTER );
+
+ if ( $this->removeDuplicates ) {
+ $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ );
+ if ( $dbw->numRows( $res ) ) {
+ return;
+ }
+ }
+ $fields['job_id'] = $dbw->nextSequenceValue( 'job_job_id_seq' );
+ $dbw->insert( 'job', $fields, __METHOD__ );
+ }
+
+ protected function insertFields() {
+ return array(
+ 'job_cmd' => $this->command,
+ 'job_namespace' => $this->title->getNamespace(),
+ 'job_title' => $this->title->getDBkey(),
+ 'job_params' => Job::makeBlob( $this->params )
+ );
+ }
+
+ /**
+ * Batch-insert a group of jobs into the queue.
+ * This will be wrapped in a transaction with a forced commit.
+ *
+ * This may add duplicate at insert time, but they will be
+ * removed later on, when the first one is popped.
+ *
+ * @param $jobs array of Job objects
+ */
+ static function batchInsert( $jobs ) {
+ if( count( $jobs ) ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+ foreach( $jobs as $job ) {
+ $rows[] = $job->insertFields();
+ }
+ $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
+ $dbw->commit();
+ }
+ }
+
+ /**
+ * Run the job
+ * @return boolean success
+ */
+ abstract function run();
+
+ function toString() {
+ $paramString = '';
+ if ( $this->params ) {
+ foreach ( $this->params as $key => $value ) {
+ if ( $paramString != '' ) {
+ $paramString .= ' ';
+ }
+ $paramString .= "$key=$value";
+ }
+ }
+
+ if ( is_object( $this->title ) ) {
+ $s = "{$this->command} " . $this->title->getPrefixedDBkey();
+ if ( $paramString !== '' ) {
+ $s .= ' ' . $paramString;
+ }
+ return $s;
+ } else {
+ return "{$this->command} $paramString";
+ }
+ }
+
+ function getLastError() {
+ return $this->error;
+ }
+}
+
+class RefreshLinksJob extends Job {
+ function __construct( $title, $params = '', $id = 0 ) {
+ parent::__construct( 'refreshLinks', $title, $params, $id );
+ }
+
+ /**
+ * Run a refreshLinks job
+ * @return boolean success
+ */
+ function run() {
+ global $wgParser;
+ wfProfileIn( __METHOD__ );
+
+ $linkCache =& LinkCache::singleton();
+ $linkCache->clear();
+
+ if ( is_null( $this->title ) ) {
+ $this->error = "refreshLinks: Invalid title";
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $revision = Revision::newFromTitle( $this->title );
+ if ( !$revision ) {
+ $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfProfileIn( __METHOD__.'-parse' );
+ $options = new ParserOptions;
+ $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
+ wfProfileOut( __METHOD__.'-parse' );
+ wfProfileIn( __METHOD__.'-update' );
+ $update = new LinksUpdate( $this->title, $parserOutput, false );
+ $update->doUpdate();
+ wfProfileOut( __METHOD__.'-update' );
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+}
+
+?>
diff --git a/includes/Licenses.php b/includes/Licenses.php
new file mode 100644
index 00000000..aaa44052
--- /dev/null
+++ b/includes/Licenses.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * A License class for use on Special:Upload
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+class Licenses {
+ /**#@+
+ * @private
+ */
+ /**
+ * @var string
+ */
+ var $msg;
+
+ /**
+ * @var array
+ */
+ var $licenses = array();
+
+ /**
+ * @var string
+ */
+ var $html;
+ /**#@-*/
+
+ /**
+ * Constrictor
+ *
+ * @param $str String: the string to build the licenses member from, will use
+ * wfMsgForContent( 'licenses' ) if null (default: null)
+ */
+ function Licenses( $str = null ) {
+ // PHP sucks, this should be possible in the constructor
+ $this->msg = is_null( $str ) ? wfMsgForContent( 'licenses' ) : $str;
+ $this->html = '';
+
+ $this->makeLicenses();
+ $tmp = $this->getLicenses();
+ $this->makeHtml( $tmp );
+ }
+
+ /**#@+
+ * @private
+ */
+ function makeLicenses() {
+ $levels = array();
+ $lines = explode( "\n", $this->msg );
+
+ foreach ( $lines as $line ) {
+ if ( strpos( $line, '*' ) !== 0 )
+ continue;
+ else {
+ list( $level, $line ) = $this->trimStars( $line );
+
+ if ( strpos( $line, '|' ) !== false ) {
+ $obj = new License( $line );
+ $this->stackItem( $this->licenses, $levels, $obj );
+ } else {
+ if ( $level < count( $levels ) )
+ $levels = array_slice( $levels, 0, $level );
+ if ( $level == count( $levels ) )
+ $levels[$level - 1] = $line;
+ else if ( $level > count( $levels ) )
+ $levels[] = $line;
+ }
+ }
+ }
+ }
+
+ function trimStars( $str ) {
+ $i = $count = 0;
+
+ wfSuppressWarnings();
+ while ($str[$i++] == '*')
+ ++$count;
+ wfRestoreWarnings();
+
+ return array( $count, ltrim( $str, '* ' ) );
+ }
+
+ function stackItem( &$list, $path, $item ) {
+ $position =& $list;
+ if ( $path )
+ foreach( $path as $key )
+ $position =& $position[$key];
+ $position[] = $item;
+ }
+
+ function makeHtml( &$tagset, $depth = 0 ) {
+ foreach ( $tagset as $key => $val )
+ if ( is_array( $val ) ) {
+ $this->html .= $this->outputOption(
+ $this->msg( $key ),
+ array(
+ 'value' => '',
+ 'disabled' => 'disabled',
+ 'style' => 'color: GrayText', // for MSIE
+ ),
+ $depth
+ );
+ $this->makeHtml( $val, $depth + 1 );
+ } else {
+ $this->html .= $this->outputOption(
+ $this->msg( $val->text ),
+ array(
+ 'value' => $val->template,
+ 'title' => '{{' . $val->template . '}}'
+ ),
+ $depth
+ );
+ }
+ }
+
+ function outputOption( $val, $attribs = null, $depth ) {
+ $val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $val;
+ return str_repeat( "\t", $depth ) . wfElement( 'option', $attribs, $val ) . "\n";
+ }
+
+ function msg( $str ) {
+ $out = wfMsg( $str );
+ return wfEmptyMsg( $str, $out ) ? $str : $out;
+ }
+
+ /**#@-*/
+
+ /**
+ * Accessor for $this->licenses
+ *
+ * @return array
+ */
+ function getLicenses() { return $this->licenses; }
+
+ /**
+ * Accessor for $this->html
+ *
+ * @return string
+ */
+ function getHtml() { return $this->html; }
+}
+
+class License {
+ /**
+ * @var string
+ */
+ var $template;
+
+ /**
+ * @var string
+ */
+ var $text;
+
+ /**
+ * Constructor
+ *
+ * @param $str String: license name??
+ */
+ function License( $str ) {
+ list( $text, $template ) = explode( '|', strrev( $str ), 2 );
+
+ $this->template = strrev( $template );
+ $this->text = strrev( $text );
+ }
+}
+?>
diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php
new file mode 100644
index 00000000..e0f0f6fd
--- /dev/null
+++ b/includes/LinkBatch.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * Class representing a list of titles
+ * The execute() method checks them all for existence and adds them to a LinkCache object
+ +
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+class LinkBatch {
+ /**
+ * 2-d array, first index namespace, second index dbkey, value arbitrary
+ */
+ var $data = array();
+
+ function LinkBatch( $arr = array() ) {
+ foreach( $arr as $item ) {
+ $this->addObj( $item );
+ }
+ }
+
+ function addObj( $title ) {
+ if ( is_object( $title ) ) {
+ $this->add( $title->getNamespace(), $title->getDBkey() );
+ } else {
+ wfDebug( "Warning: LinkBatch::addObj got invalid title object\n" );
+ }
+ }
+
+ function add( $ns, $dbkey ) {
+ if ( $ns < 0 ) {
+ return;
+ }
+ if ( !array_key_exists( $ns, $this->data ) ) {
+ $this->data[$ns] = array();
+ }
+
+ $this->data[$ns][$dbkey] = 1;
+ }
+
+ /**
+ * Set the link list to a given 2-d array
+ * First key is the namespace, second is the DB key, value arbitrary
+ */
+ function setArray( $array ) {
+ $this->data = $array;
+ }
+
+ /**
+ * Returns true if no pages have been added, false otherwise.
+ */
+ function isEmpty() {
+ return ($this->getSize() == 0);
+ }
+
+ /**
+ * Returns the size of the batch.
+ */
+ function getSize() {
+ return count( $this->data );
+ }
+
+ /**
+ * Do the query and add the results to the LinkCache object
+ * Return an array mapping PDBK to ID
+ */
+ function execute() {
+ $linkCache =& LinkCache::singleton();
+ $this->executeInto( $linkCache );
+ }
+
+ /**
+ * Do the query and add the results to a given LinkCache object
+ * Return an array mapping PDBK to ID
+ */
+ function executeInto( &$cache ) {
+ $fname = 'LinkBatch::executeInto';
+ wfProfileIn( $fname );
+ // Do query
+ $res = $this->doQuery();
+ if ( !$res ) {
+ wfProfileOut( $fname );
+ return array();
+ }
+
+ // For each returned entry, add it to the list of good links, and remove it from $remaining
+
+ $ids = array();
+ $remaining = $this->data;
+ while ( $row = $res->fetchObject() ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $cache->addGoodLinkObj( $row->page_id, $title );
+ $ids[$title->getPrefixedDBkey()] = $row->page_id;
+ unset( $remaining[$row->page_namespace][$row->page_title] );
+ }
+ $res->free();
+
+ // The remaining links in $data are bad links, register them as such
+ foreach ( $remaining as $ns => $dbkeys ) {
+ foreach ( $dbkeys as $dbkey => $nothing ) {
+ $title = Title::makeTitle( $ns, $dbkey );
+ $cache->addBadLinkObj( $title );
+ $ids[$title->getPrefixedDBkey()] = 0;
+ }
+ }
+ wfProfileOut( $fname );
+ return $ids;
+ }
+
+ /**
+ * Perform the existence test query, return a ResultWrapper with page_id fields
+ */
+ function doQuery() {
+ $fname = 'LinkBatch::doQuery';
+ $namespaces = array();
+
+ if ( $this->isEmpty() ) {
+ return false;
+ }
+ wfProfileIn( $fname );
+
+ // Construct query
+ // This is very similar to Parser::replaceLinkHolders
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $set = $this->constructSet( 'page', $dbr );
+ if ( $set === false ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+ $sql = "SELECT page_id, page_namespace, page_title FROM $page WHERE $set";
+
+ // Do query
+ $res = new ResultWrapper( $dbr, $dbr->query( $sql, $fname ) );
+ wfProfileOut( $fname );
+ return $res;
+ }
+
+ /**
+ * Construct a WHERE clause which will match all the given titles.
+ * Give the appropriate table's field name prefix ('page', 'pl', etc).
+ *
+ * @param $prefix String: ??
+ * @return string
+ * @public
+ */
+ function constructSet( $prefix, &$db ) {
+ $first = true;
+ $firstTitle = true;
+ $sql = '';
+ foreach ( $this->data as $ns => $dbkeys ) {
+ if ( !count( $dbkeys ) ) {
+ continue;
+ }
+
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ' OR ';
+ }
+ $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN (";
+
+ $firstTitle = true;
+ foreach( $dbkeys as $dbkey => $nothing ) {
+ if ( $firstTitle ) {
+ $firstTitle = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= $db->addQuotes( $dbkey );
+ }
+
+ $sql .= '))';
+ }
+ if ( $first && $firstTitle ) {
+ # No titles added
+ return false;
+ } else {
+ return $sql;
+ }
+ }
+}
+
+?>
diff --git a/includes/LinkCache.php b/includes/LinkCache.php
new file mode 100644
index 00000000..451b3f0c
--- /dev/null
+++ b/includes/LinkCache.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * Cache for article titles (prefixed DB keys) and ids linked from one source
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+class LinkCache {
+ // Increment $mClassVer whenever old serialized versions of this class
+ // becomes incompatible with the new version.
+ /* private */ var $mClassVer = 3;
+
+ /* private */ var $mPageLinks;
+ /* private */ var $mGoodLinks, $mBadLinks;
+ /* private */ var $mForUpdate;
+
+ /**
+ * Get an instance of this class
+ */
+ function &singleton() {
+ static $instance;
+ if ( !isset( $instance ) ) {
+ $instance = new LinkCache;
+ }
+ return $instance;
+ }
+
+ function LinkCache() {
+ $this->mForUpdate = false;
+ $this->mPageLinks = array();
+ $this->mGoodLinks = array();
+ $this->mBadLinks = array();
+ }
+
+ /* private */ function getKey( $title ) {
+ global $wgDBname;
+ return $wgDBname.':lc:title:'.$title;
+ }
+
+ /**
+ * General accessor to get/set whether SELECT FOR UPDATE should be used
+ */
+ function forUpdate( $update = NULL ) {
+ return wfSetVar( $this->mForUpdate, $update );
+ }
+
+ function getGoodLinkID( $title ) {
+ if ( array_key_exists( $title, $this->mGoodLinks ) ) {
+ return $this->mGoodLinks[$title];
+ } else {
+ return 0;
+ }
+ }
+
+ function isBadLink( $title ) {
+ return array_key_exists( $title, $this->mBadLinks );
+ }
+
+ function addGoodLinkObj( $id, $title ) {
+ $dbkey = $title->getPrefixedDbKey();
+ $this->mGoodLinks[$dbkey] = $id;
+ $this->mPageLinks[$dbkey] = $title;
+ }
+
+ function addBadLinkObj( $title ) {
+ $dbkey = $title->getPrefixedDbKey();
+ if ( ! $this->isBadLink( $dbkey ) ) {
+ $this->mBadLinks[$dbkey] = 1;
+ $this->mPageLinks[$dbkey] = $title;
+ }
+ }
+
+ function clearBadLink( $title ) {
+ unset( $this->mBadLinks[$title] );
+ $this->clearLink( $title );
+ }
+
+ function clearLink( $title ) {
+ global $wgMemc, $wgLinkCacheMemcached;
+ if( $wgLinkCacheMemcached )
+ $wgMemc->delete( $this->getKey( $title ) );
+ }
+
+ function getPageLinks() { return $this->mPageLinks; }
+ function getGoodLinks() { return $this->mGoodLinks; }
+ function getBadLinks() { return array_keys( $this->mBadLinks ); }
+
+ /**
+ * Add a title to the link cache, return the page_id or zero if non-existent
+ * @param $title String: title to add
+ * @return integer
+ */
+ function addLink( $title ) {
+ $nt = Title::newFromDBkey( $title );
+ if( $nt ) {
+ return $this->addLinkObj( $nt );
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Add a title to the link cache, return the page_id or zero if non-existent
+ * @param $nt Title to add.
+ * @return integer
+ */
+ function addLinkObj( &$nt ) {
+ global $wgMemc, $wgLinkCacheMemcached, $wgAntiLockFlags;
+ $title = $nt->getPrefixedDBkey();
+ if ( $this->isBadLink( $title ) ) { return 0; }
+ $id = $this->getGoodLinkID( $title );
+ if ( 0 != $id ) { return $id; }
+
+ $fname = 'LinkCache::addLinkObj';
+ global $wgProfiling, $wgProfiler;
+ if ( $wgProfiling && isset( $wgProfiler ) ) {
+ $fname .= ' (' . $wgProfiler->getCurrentSection() . ')';
+ }
+
+ wfProfileIn( $fname );
+
+ $ns = $nt->getNamespace();
+ $t = $nt->getDBkey();
+
+ if ( '' == $title ) {
+ wfProfileOut( $fname );
+ return 0;
+ }
+
+ $id = NULL;
+ if( $wgLinkCacheMemcached )
+ $id = $wgMemc->get( $key = $this->getKey( $title ) );
+ if( ! is_integer( $id ) ) {
+ if ( $this->mForUpdate ) {
+ $db =& wfGetDB( DB_MASTER );
+ if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) {
+ $options = array( 'FOR UPDATE' );
+ } else {
+ $options = array();
+ }
+ } else {
+ $db =& wfGetDB( DB_SLAVE );
+ $options = array();
+ }
+
+ $id = $db->selectField( 'page', 'page_id',
+ array( 'page_namespace' => $ns, 'page_title' => $t ),
+ $fname, $options );
+ if ( !$id ) {
+ $id = 0;
+ }
+ if( $wgLinkCacheMemcached )
+ $wgMemc->add( $key, $id, 3600*24 );
+ }
+
+ if( 0 == $id ) {
+ $this->addBadLinkObj( $nt );
+ } else {
+ $this->addGoodLinkObj( $id, $nt );
+ }
+ wfProfileOut( $fname );
+ return $id;
+ }
+
+ /**
+ * Clears cache
+ */
+ function clear() {
+ $this->mPageLinks = array();
+ $this->mGoodLinks = array();
+ $this->mBadLinks = array();
+ }
+}
+?>
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
new file mode 100644
index 00000000..e03b59dd
--- /dev/null
+++ b/includes/LinkFilter.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * Some functions to help implement an external link filter for spam control.
+ *
+ * TODO: implement the filter. Currently these are just some functions to help
+ * maintenance/cleanupSpam.php remove links to a single specified domain. The
+ * next thing is to implement functions for checking a given page against a big
+ * list of domains.
+ *
+ * Another cool thing to do would be a web interface for fast spam removal.
+ */
+class LinkFilter {
+ /**
+ * @static
+ */
+ function matchEntry( $text, $filterEntry ) {
+ $regex = LinkFilter::makeRegex( $filterEntry );
+ return preg_match( $regex, $text );
+ }
+
+ /**
+ * @static
+ */
+ function makeRegex( $filterEntry ) {
+ $regex = '!http://';
+ if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
+ $regex .= '([A-Za-z0-9.-]+\.|)';
+ $filterEntry = substr( $filterEntry, 2 );
+ }
+ $regex .= preg_quote( $filterEntry, '!' ) . '!Si';
+ return $regex;
+ }
+
+ /**
+ * Make a string to go after an SQL LIKE, which will match the specified
+ * string. There are several kinds of filter entry:
+ * *.domain.com - Produces http://com.domain.%, matches domain.com
+ * and www.domain.com
+ * domain.com - Produces http://com.domain./%, matches domain.com
+ * or domain.com/ but not www.domain.com
+ * *.domain.com/x - Produces http://com.domain.%/x%, matches
+ * www.domain.com/xy
+ * domain.com/x - Produces http://com.domain./x%, matches
+ * domain.com/xy but not www.domain.com/xy
+ *
+ * Asterisks in any other location are considered invalid.
+ *
+ * @static
+ */
+ function makeLike( $filterEntry ) {
+ if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
+ $subdomains = true;
+ $filterEntry = substr( $filterEntry, 2 );
+ if ( $filterEntry == '' ) {
+ // We don't want to make a clause that will match everything,
+ // that could be dangerous
+ return false;
+ }
+ } else {
+ $subdomains = false;
+ }
+ // No stray asterisks, that could cause confusion
+ // It's not simple or efficient to handle it properly so we don't
+ // handle it at all.
+ if ( strpos( $filterEntry, '*' ) !== false ) {
+ return false;
+ }
+ $slash = strpos( $filterEntry, '/' );
+ if ( $slash !== false ) {
+ $path = substr( $filterEntry, $slash );
+ $host = substr( $filterEntry, 0, $slash );
+ } else {
+ $path = '/';
+ $host = $filterEntry;
+ }
+ $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
+ if ( substr( $host, -1, 1 ) !== '.' ) {
+ $host .= '.';
+ }
+ $like = "http://$host";
+
+ if ( $subdomains ) {
+ $like .= '%';
+ }
+ if ( !$subdomains || $path !== '/' ) {
+ $like .= $path . '%';
+ }
+ return $like;
+ }
+}
+?>
diff --git a/includes/Linker.php b/includes/Linker.php
new file mode 100644
index 00000000..4a0eafbd
--- /dev/null
+++ b/includes/Linker.php
@@ -0,0 +1,1101 @@
+<?php
+/**
+ * Split off some of the internal bits from Skin.php.
+ * These functions are used for primarily page content:
+ * links, embedded images, table of contents. Links are
+ * also used in the skin.
+ * @package MediaWiki
+ */
+
+/**
+ * For the moment, Skin is a descendent class of Linker.
+ * In the future, it should probably be further split
+ * so that ever other bit of the wiki doesn't have to
+ * go loading up Skin to get at it.
+ *
+ * @package MediaWiki
+ */
+class Linker {
+
+ function Linker() {}
+
+ /**
+ * @deprecated
+ */
+ function postParseLinkColour( $s = NULL ) {
+ return NULL;
+ }
+
+ /** @todo document */
+ function getExternalLinkAttributes( $link, $text, $class='' ) {
+ $link = htmlspecialchars( $link );
+
+ $r = ($class != '') ? " class=\"$class\"" : " class=\"external\"";
+
+ $r .= " title=\"{$link}\"";
+ return $r;
+ }
+
+ function getInterwikiLinkAttributes( $link, $text, $class='' ) {
+ global $wgContLang;
+
+ $same = ($link == $text);
+ $link = urldecode( $link );
+ $link = $wgContLang->checkTitleEncoding( $link );
+ $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link );
+ $link = htmlspecialchars( $link );
+
+ $r = ($class != '') ? " class=\"$class\"" : " class=\"external\"";
+
+ $r .= " title=\"{$link}\"";
+ return $r;
+ }
+
+ /** @todo document */
+ function getInternalLinkAttributes( $link, $text, $broken = false ) {
+ $link = urldecode( $link );
+ $link = str_replace( '_', ' ', $link );
+ $link = htmlspecialchars( $link );
+
+ if( $broken == 'stub' ) {
+ $r = ' class="stub"';
+ } else if ( $broken == 'yes' ) {
+ $r = ' class="new"';
+ } else {
+ $r = '';
+ }
+
+ $r .= " title=\"{$link}\"";
+ return $r;
+ }
+
+ /**
+ * @param $nt Title object.
+ * @param $text String: FIXME
+ * @param $broken Boolean: FIXME, default 'false'.
+ */
+ function getInternalLinkAttributesObj( &$nt, $text, $broken = false ) {
+ if( $broken == 'stub' ) {
+ $r = ' class="stub"';
+ } else if ( $broken == 'yes' ) {
+ $r = ' class="new"';
+ } else {
+ $r = '';
+ }
+
+ $r .= ' title="' . $nt->getEscapedText() . '"';
+ return $r;
+ }
+
+ /**
+ * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeLink( $title, $text = '', $query = '', $trail = '' ) {
+ wfProfileIn( 'Linker::makeLink' );
+ $nt = Title::newFromText( $title );
+ if ($nt) {
+ $result = $this->makeLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" );
+ $result = $text == "" ? $title : $text;
+ }
+
+ wfProfileOut( 'Linker::makeLink' );
+ return $result;
+ }
+
+ /**
+ * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeKnownLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') {
+ $nt = Title::newFromText( $title );
+ if ($nt) {
+ return $this->makeKnownLinkObj( Title::newFromText( $title ), $text, $query, $trail, $prefix , $aprops );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
+ *
+ * @param string $title The text of the title
+ * @param string $text Link text
+ * @param string $query Optional query part
+ * @param string $trail Optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
+ $nt = Title::newFromText( $title );
+ if ($nt) {
+ return $this->makeBrokenLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeStubLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
+ $nt = Title::newFromText( $title );
+ if ($nt) {
+ return $this->makeStubLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * Make a link for a title which may or may not be in the database. If you need to
+ * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
+ * call to this will result in a DB query.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
+ global $wgUser;
+ $fname = 'Linker::makeLinkObj';
+ wfProfileIn( $fname );
+
+ # Fail gracefully
+ if ( ! is_object($nt) ) {
+ # throw new MWException();
+ wfProfileOut( $fname );
+ return "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ }
+
+ $ns = $nt->getNamespace();
+ $dbkey = $nt->getDBkey();
+ if ( $nt->isExternal() ) {
+ $u = $nt->getFullURL();
+ $link = $nt->getPrefixedURL();
+ if ( '' == $text ) { $text = $nt->getPrefixedText(); }
+ $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' );
+
+ $inside = '';
+ if ( '' != $trail ) {
+ if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) {
+ $inside = $m[1];
+ $trail = $m[2];
+ }
+ }
+
+ # Check for anchors, normalize the anchor
+
+ $parts = explode( '#', $u, 2 );
+ if ( count( $parts ) == 2 ) {
+ $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $parts[1] ) ) );
+ $replacearray = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+ $u = $parts[0] . '#' .
+ str_replace( array_keys( $replacearray ),
+ array_values( $replacearray ),
+ $anchor );
+ }
+
+ $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>";
+
+ wfProfileOut( $fname );
+ return $t;
+ } elseif ( $nt->isAlwaysKnown() ) {
+ # Image links, special page links and self-links with fragements are always known.
+ $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
+ } else {
+ wfProfileIn( $fname.'-immediate' );
+ # Work out link colour immediately
+ $aid = $nt->getArticleID() ;
+ if ( 0 == $aid ) {
+ $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
+ } else {
+ $threshold = $wgUser->getOption('stubthreshold') ;
+ if ( $threshold > 0 ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $s = $dbr->selectRow(
+ array( 'page' ),
+ array( 'page_len',
+ 'page_namespace',
+ 'page_is_redirect' ),
+ array( 'page_id' => $aid ), $fname ) ;
+ if ( $s !== false ) {
+ $size = $s->page_len;
+ if ( $s->page_is_redirect OR $s->page_namespace != NS_MAIN ) {
+ $size = $threshold*2 ; # Really big
+ }
+ } else {
+ $size = $threshold*2 ; # Really big
+ }
+ } else {
+ $size = 1 ;
+ }
+ if ( $size < $threshold ) {
+ $retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix );
+ } else {
+ $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
+ }
+ }
+ wfProfileOut( $fname.'-immediate' );
+ }
+ wfProfileOut( $fname );
+ return $retVal;
+ }
+
+ /**
+ * Make a link for a title which definitely exists. This is faster than makeLinkObj because
+ * it doesn't have to do a database query. It's also valid for interwiki titles and special
+ * pages.
+ *
+ * @param $nt Title object of target page
+ * @param $text String: text to replace the title
+ * @param $query String: link target
+ * @param $trail String: text after link
+ * @param $prefix String: text before link text
+ * @param $aprops String: extra attributes to the a-element
+ * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
+ * @return the a-element
+ */
+ function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+
+ $fname = 'Linker::makeKnownLinkObj';
+ wfProfileIn( $fname );
+
+ if ( !is_object( $nt ) ) {
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ $u = $nt->escapeLocalURL( $query );
+ if ( $nt->getFragment() != '' ) {
+ if( $nt->getPrefixedDbkey() == '' ) {
+ $u = '';
+ if ( '' == $text ) {
+ $text = htmlspecialchars( $nt->getFragment() );
+ }
+ }
+ $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace( ' ', '_', $nt->getFragment() ) ) );
+ $replacearray = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+ $u .= '#' . str_replace(array_keys($replacearray),array_values($replacearray),$anchor);
+ }
+ if ( $text == '' ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+ if ( $style == '' ) {
+ $style = $this->getInternalLinkAttributesObj( $nt, $text );
+ }
+
+ if ( $aprops !== '' ) $aprops = ' ' . $aprops;
+
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}";
+ wfProfileOut( $fname );
+ return $r;
+ }
+
+ /**
+ * Make a red link to the edit page of a given title.
+ *
+ * @param $title String: The text of the title
+ * @param $text String: Link text
+ * @param $query String: Optional query part
+ * @param $trail String: Optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ # Fail gracefully
+ if ( ! isset($nt) ) {
+ # throw new MWException();
+ return "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ }
+
+ $fname = 'Linker::makeBrokenLinkObj';
+ wfProfileIn( $fname );
+
+ if ( '' == $query ) {
+ $q = 'action=edit';
+ } else {
+ $q = 'action=edit&'.$query;
+ }
+ $u = $nt->escapeLocalURL( $q );
+
+ if ( '' == $text ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+ $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" );
+
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
+
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /**
+ * Make a brown link to a short article.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ $link = $nt->getPrefixedURL();
+
+ $u = $nt->escapeLocalURL( $query );
+
+ if ( '' == $text ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+ $style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' );
+
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
+ return $s;
+ }
+
+ /**
+ * Generate either a normal exists-style link or a stub link, depending
+ * on the given page size.
+ *
+ * @param $size Integer
+ * @param $nt Title object.
+ * @param $text String
+ * @param $query String
+ * @param $trail String
+ * @param $prefix String
+ * @return string HTML of link
+ */
+ function makeSizeLinkObj( $size, $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ global $wgUser;
+ $threshold = intval( $wgUser->getOption( 'stubthreshold' ) );
+ if( $size < $threshold ) {
+ return $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix );
+ } else {
+ return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
+ }
+ }
+
+ /**
+ * Make appropriate markup for a link to the current article. This is currently rendered
+ * as the bold link text. The calling sequence is the same as the other make*LinkObj functions,
+ * despite $query not being used.
+ */
+ function makeSelfLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ if ( '' == $text ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ return "<strong class=\"selflink\">{$prefix}{$text}{$inside}</strong>{$trail}";
+ }
+
+ /** @todo document */
+ function fnamePart( $url ) {
+ $basename = strrchr( $url, '/' );
+ if ( false === $basename ) {
+ $basename = $url;
+ } else {
+ $basename = substr( $basename, 1 );
+ }
+ return htmlspecialchars( $basename );
+ }
+
+ /** Obsolete alias */
+ function makeImage( $url, $alt = '' ) {
+ return $this->makeExternalImage( $url, $alt );
+ }
+
+ /** @todo document */
+ function makeExternalImage( $url, $alt = '' ) {
+ if ( '' == $alt ) {
+ $alt = $this->fnamePart( $url );
+ }
+ $s = '<img src="'.$url.'" alt="'.$alt.'" />';
+ return $s;
+ }
+
+ /** @todo document */
+ function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false,
+ $thumb = false, $manual_thumb = '' )
+ {
+ global $wgContLang, $wgUser, $wgThumbLimits, $wgGenerateThumbnailOnParse;
+
+ $img = new Image( $nt );
+ if ( !$img->allowInlineDisplay() && $img->exists() ) {
+ return $this->makeKnownLinkObj( $nt );
+ }
+
+ $url = $img->getViewURL();
+ $error = $prefix = $postfix = '';
+
+ wfDebug( "makeImageLinkObj: '$width'x'$height'\n" );
+
+ if ( 'center' == $align )
+ {
+ $prefix = '<div class="center">';
+ $postfix = '</div>';
+ $align = 'none';
+ }
+
+ if ( $thumb || $framed ) {
+
+ # Create a thumbnail. Alignment depends on language
+ # writing direction, # right aligned for left-to-right-
+ # languages ("Western languages"), left-aligned
+ # for right-to-left-languages ("Semitic languages")
+ #
+ # If thumbnail width has not been provided, it is set
+ # to the default user option as specified in Language*.php
+ if ( $align == '' ) {
+ $align = $wgContLang->isRTL() ? 'left' : 'right';
+ }
+
+
+ if ( $width === false ) {
+ $wopt = $wgUser->getOption( 'thumbsize' );
+
+ if( !isset( $wgThumbLimits[$wopt] ) ) {
+ $wopt = User::getDefaultOption( 'thumbsize' );
+ }
+
+ $width = min( $img->getWidth(), $wgThumbLimits[$wopt] );
+ }
+
+ return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $width, $height, $framed, $manual_thumb ).$postfix;
+ }
+
+ if ( $width && $img->exists() ) {
+
+ # Create a resized image, without the additional thumbnail
+ # features
+
+ if ( $height == false )
+ $height = -1;
+ if ( $manual_thumb == '') {
+ $thumb = $img->getThumbnail( $width, $height, $wgGenerateThumbnailOnParse );
+ if ( $thumb ) {
+ // In most cases, $width = $thumb->width or $height = $thumb->height.
+ // If not, we're scaling the image larger than it can be scaled,
+ // so we send to the browser a smaller thumbnail, and let the client do the scaling.
+
+ if ($height != -1 && $width > $thumb->width * $height / $thumb->height) {
+ // $height is the limiting factor, not $width
+ // set $width to the largest it can be, such that the resulting
+ // scaled height is at most $height
+ $width = floor($thumb->width * $height / $thumb->height);
+ }
+ $height = round($thumb->height * $width / $thumb->width);
+
+ wfDebug( "makeImageLinkObj: client-size set to '$width x $height'\n" );
+ $url = $thumb->getUrl();
+ } else {
+ $error = htmlspecialchars( $img->getLastError() );
+ }
+ }
+ } else {
+ $width = $img->width;
+ $height = $img->height;
+ }
+
+ wfDebug( "makeImageLinkObj2: '$width'x'$height'\n" );
+ $u = $nt->escapeLocalURL();
+ if ( $error ) {
+ $s = $error;
+ } elseif ( $url == '' ) {
+ $s = $this->makeBrokenImageLinkObj( $img->getTitle() );
+ //$s .= "<br />{$alt}<br />{$url}<br />\n";
+ } else {
+ $s = '<a href="'.$u.'" class="image" title="'.$alt.'">' .
+ '<img src="'.$url.'" alt="'.$alt.'" ' .
+ ( $width
+ ? ( 'width="'.$width.'" height="'.$height.'" ' )
+ : '' ) .
+ 'longdesc="'.$u.'" /></a>';
+ }
+ if ( '' != $align ) {
+ $s = "<div class=\"float{$align}\"><span>{$s}</span></div>";
+ }
+ return str_replace("\n", ' ',$prefix.$s.$postfix);
+ }
+
+ /**
+ * Make HTML for a thumbnail including image, border and caption
+ * $img is an Image object
+ */
+ function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) {
+ global $wgStylePath, $wgContLang, $wgGenerateThumbnailOnParse;
+ $url = $img->getViewURL();
+ $thumbUrl = '';
+ $error = '';
+
+ $width = $height = 0;
+ if ( $img->exists() ) {
+ $width = $img->getWidth();
+ $height = $img->getHeight();
+ }
+ if ( 0 == $width || 0 == $height ) {
+ $width = $height = 180;
+ }
+ if ( $boxwidth == 0 ) {
+ $boxwidth = 180;
+ }
+ if ( $framed ) {
+ // Use image dimensions, don't scale
+ $boxwidth = $width;
+ $boxheight = $height;
+ $thumbUrl = $url;
+ } else {
+ if ( $boxheight === false )
+ $boxheight = -1;
+ if ( '' == $manual_thumb ) {
+ $thumb = $img->getThumbnail( $boxwidth, $boxheight, $wgGenerateThumbnailOnParse );
+ if ( $thumb ) {
+ $thumbUrl = $thumb->getUrl();
+ $boxwidth = $thumb->width;
+ $boxheight = $thumb->height;
+ } else {
+ $error = $img->getLastError();
+ }
+ }
+ }
+ $oboxwidth = $boxwidth + 2;
+
+ if ( $manual_thumb != '' ) # Use manually specified thumbnail
+ {
+ $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); #new Title ( $manual_thumb ) ;
+ if( $manual_title ) {
+ $manual_img = new Image( $manual_title );
+ $thumbUrl = $manual_img->getViewURL();
+ if ( $manual_img->exists() )
+ {
+ $width = $manual_img->getWidth();
+ $height = $manual_img->getHeight();
+ $boxwidth = $width ;
+ $boxheight = $height ;
+ $oboxwidth = $boxwidth + 2 ;
+ }
+ }
+ }
+
+ $u = $img->getEscapeLocalURL();
+
+ $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) );
+ $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right';
+ $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : '';
+
+ $s = "<div class=\"thumb t{$align}\"><div style=\"width:{$oboxwidth}px;\">";
+ if( $thumbUrl == '' ) {
+ // Couldn't generate thumbnail? Scale the image client-side.
+ $thumbUrl = $url;
+ }
+ if ( $error ) {
+ $s .= htmlspecialchars( $error );
+ $zoomicon = '';
+ } elseif( !$img->exists() ) {
+ $s .= $this->makeBrokenImageLinkObj( $img->getTitle() );
+ $zoomicon = '';
+ } else {
+ $s .= '<a href="'.$u.'" class="internal" title="'.$alt.'">'.
+ '<img src="'.$thumbUrl.'" alt="'.$alt.'" ' .
+ 'width="'.$boxwidth.'" height="'.$boxheight.'" ' .
+ 'longdesc="'.$u.'" /></a>';
+ if ( $framed ) {
+ $zoomicon="";
+ } else {
+ $zoomicon = '<div class="magnify" style="float:'.$magnifyalign.'">'.
+ '<a href="'.$u.'" class="internal" title="'.$more.'">'.
+ '<img src="'.$wgStylePath.'/common/images/magnify-clip.png" ' .
+ 'width="15" height="11" alt="'.$more.'" /></a></div>';
+ }
+ }
+ $s .= ' <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$label."</div></div></div>";
+ return str_replace("\n", ' ', $s);
+ }
+
+ /**
+ * Pass a title object, not a title string
+ */
+ function makeBrokenImageLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ # Fail gracefully
+ if ( ! isset($nt) ) {
+ # throw new MWException();
+ return "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ }
+
+ $fname = 'Linker::makeBrokenImageLinkObj';
+ wfProfileIn( $fname );
+
+ $q = 'wpDestFile=' . urlencode( $nt->getDBkey() );
+ if ( '' != $query ) {
+ $q .= "&$query";
+ }
+ $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $url = $uploadTitle->escapeLocalURL( $q );
+
+ if ( '' == $text ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+ $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ $s = "<a href=\"{$url}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
+
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /** @todo document */
+ function makeMediaLink( $name, /* wtf?! */ $url, $alt = '' ) {
+ $nt = Title::makeTitleSafe( NS_IMAGE, $name );
+ return $this->makeMediaLinkObj( $nt, $alt );
+ }
+
+ /**
+ * Create a direct link to a given uploaded file.
+ *
+ * @param $title Title object.
+ * @param $text String: pre-sanitized HTML
+ * @param $nourl Boolean: Mask absolute URLs, so the parser doesn't
+ * linkify them (it is currently not context-aware)
+ * @return string HTML
+ *
+ * @public
+ * @todo Handle invalid or missing images better.
+ */
+ function makeMediaLinkObj( $title, $text = '' ) {
+ if( is_null( $title ) ) {
+ ### HOTFIX. Instead of breaking, return empty string.
+ return $text;
+ } else {
+ $name = $title->getDBKey();
+ $img = new Image( $title );
+ if( $img->exists() ) {
+ $url = $img->getURL();
+ $class = 'internal';
+ } else {
+ $upload = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) );
+ $class = 'new';
+ }
+ $alt = htmlspecialchars( $title->getText() );
+ if( $text == '' ) {
+ $text = $alt;
+ }
+ $u = htmlspecialchars( $url );
+ return "<a href=\"{$u}\" class=\"$class\" title=\"{$alt}\">{$text}</a>";
+ }
+ }
+
+ /** @todo document */
+ function specialLink( $name, $key = '' ) {
+ global $wgContLang;
+
+ if ( '' == $key ) { $key = strtolower( $name ); }
+ $pn = $wgContLang->ucfirst( $name );
+ return $this->makeKnownLink( $wgContLang->specialPage( $pn ),
+ wfMsg( $key ) );
+ }
+
+ /** @todo document */
+ function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) {
+ $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
+ global $wgNoFollowLinks, $wgNoFollowNsExceptions;
+ if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) {
+ $style .= ' rel="nofollow"';
+ }
+ $url = htmlspecialchars( $url );
+ if( $escape ) {
+ $text = htmlspecialchars( $text );
+ }
+ return '<a href="'.$url.'"'.$style.'>'.$text.'</a>';
+ }
+
+ /**
+ * Make user link (or user contributions for unregistered users)
+ * @param $userId Integer: user id in database.
+ * @param $userText String: user name in database
+ * @return string HTML fragment
+ * @private
+ */
+ function userLink( $userId, $userText ) {
+ $encName = htmlspecialchars( $userText );
+ if( $userId == 0 ) {
+ $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' );
+ return $this->makeKnownLinkObj( $contribsPage,
+ $encName, 'target=' . urlencode( $userText ) );
+ } else {
+ $userPage = Title::makeTitle( NS_USER, $userText );
+ return $this->makeLinkObj( $userPage, $encName );
+ }
+ }
+
+ /**
+ * @param $userId Integer: user id in database.
+ * @param $userText String: user name in database.
+ * @return string HTML fragment with talk and/or block links
+ * @private
+ */
+ function userToolLinks( $userId, $userText ) {
+ global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans;
+ $talkable = !( $wgDisableAnonTalk && 0 == $userId );
+ $blockable = ( $wgSysopUserBans || 0 == $userId );
+
+ $items = array();
+ if( $talkable ) {
+ $items[] = $this->userTalkLink( $userId, $userText );
+ }
+ if( $userId ) {
+ $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' );
+ $items[] = $this->makeKnownLinkObj( $contribsPage,
+ wfMsgHtml( 'contribslink' ), 'target=' . urlencode( $userText ) );
+ }
+ if( $blockable && $wgUser->isAllowed( 'block' ) ) {
+ $items[] = $this->blockLink( $userId, $userText );
+ }
+
+ if( $items ) {
+ return ' (' . implode( ' | ', $items ) . ')';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * @param $userId Integer: user id in database.
+ * @param $userText String: user name in database.
+ * @return string HTML fragment with user talk link
+ * @private
+ */
+ function userTalkLink( $userId, $userText ) {
+ global $wgLang;
+ $talkname = $wgLang->getNsText( NS_TALK ); # use the shorter name
+
+ $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
+ $userTalkLink = $this->makeLinkObj( $userTalkPage, $talkname );
+ return $userTalkLink;
+ }
+
+ /**
+ * @param $userId Integer: userid
+ * @param $userText String: user name in database.
+ * @return string HTML fragment with block link
+ * @private
+ */
+ function blockLink( $userId, $userText ) {
+ $blockPage = Title::makeTitle( NS_SPECIAL, 'Blockip' );
+ $blockLink = $this->makeKnownLinkObj( $blockPage,
+ wfMsgHtml( 'blocklink' ), 'ip=' . urlencode( $userText ) );
+ return $blockLink;
+ }
+
+ /**
+ * Generate a user link if the current user is allowed to view it
+ * @param $rev Revision object.
+ * @return string HTML
+ */
+ function revUserLink( $rev ) {
+ if( $rev->userCan( Revision::DELETED_USER ) ) {
+ $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
+ } else {
+ $link = wfMsgHtml( 'rev-deleted-user' );
+ }
+ if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
+ }
+
+ /**
+ * Generate a user tool link cluster if the current user is allowed to view it
+ * @param $rev Revision object.
+ * @return string HTML
+ */
+ function revUserTools( $rev ) {
+ if( $rev->userCan( Revision::DELETED_USER ) ) {
+ $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
+ ' ' .
+ $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
+ } else {
+ $link = wfMsgHtml( 'rev-deleted-user' );
+ }
+ if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
+ }
+
+ /**
+ * This function is called by all recent changes variants, by the page history,
+ * and by the user contributions list. It is responsible for formatting edit
+ * comments. It escapes any HTML in the comment, but adds some CSS to format
+ * auto-generated comments (from section editing) and formats [[wikilinks]].
+ *
+ * The $title parameter must be a title OBJECT. It is used to generate a
+ * direct link to the section in the autocomment.
+ * @author Erik Moeller <moeller@scireview.de>
+ *
+ * Note: there's not always a title to pass to this function.
+ * Since you can't set a default parameter for a reference, I've turned it
+ * temporarily to a value pass. Should be adjusted further. --brion
+ */
+ function formatComment($comment, $title = NULL) {
+ $fname = 'Linker::formatComment';
+ wfProfileIn( $fname );
+
+ global $wgContLang;
+ $comment = str_replace( "\n", " ", $comment );
+ $comment = htmlspecialchars( $comment );
+
+ # The pattern for autogen comments is / * foo * /, which makes for
+ # some nasty regex.
+ # We look for all comments, match any text before and after the comment,
+ # add a separator where needed and format the comment itself with CSS
+ while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) {
+ $pre=$match[1];
+ $auto=$match[2];
+ $post=$match[3];
+ $link='';
+ if( $title ) {
+ $section = $auto;
+
+ # Generate a valid anchor name from the section title.
+ # Hackish, but should generally work - we strip wiki
+ # syntax, including the magic [[: that is used to
+ # "link rather than show" in case of images and
+ # interlanguage links.
+ $section = str_replace( '[[:', '', $section );
+ $section = str_replace( '[[', '', $section );
+ $section = str_replace( ']]', '', $section );
+ $sectionTitle = wfClone( $title );
+ $sectionTitle->mFragment = $section;
+ $link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) );
+ }
+ $sep='-';
+ $auto=$link.$auto;
+ if($pre) { $auto = $sep.' '.$auto; }
+ if($post) { $auto .= ' '.$sep; }
+ $auto='<span class="autocomment">'.$auto.'</span>';
+ $comment=$pre.$auto.$post;
+ }
+
+ # format regular and media links - all other wiki formatting
+ # is ignored
+ $medians = $wgContLang->getNsText( NS_MEDIA ) . ':';
+ while(preg_match('/\[\[(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) {
+ # Handle link renaming [[foo|text]] will show link as "text"
+ if( "" != $match[3] ) {
+ $text = $match[3];
+ } else {
+ $text = $match[1];
+ }
+ if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
+ # Media link; trail not supported.
+ $linkRegexp = '/\[\[(.*?)\]\]/';
+ $thelink = $this->makeMediaLink( $submatch[1], "", $text );
+ } else {
+ # Other kind of link
+ if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) {
+ $trail = $submatch[1];
+ } else {
+ $trail = "";
+ }
+ $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
+ if ($match[1][0] == ':')
+ $match[1] = substr($match[1], 1);
+ $thelink = $this->makeLink( $match[1], $text, "", $trail );
+ }
+ $comment = preg_replace( $linkRegexp, wfRegexReplacement( $thelink ), $comment, 1 );
+ }
+ wfProfileOut( $fname );
+ return $comment;
+ }
+
+ /**
+ * Wrap a comment in standard punctuation and formatting if
+ * it's non-empty, otherwise return empty string.
+ *
+ * @param $comment String: the comment.
+ * @param $title Title object.
+ *
+ * @return string
+ */
+ function commentBlock( $comment, $title = NULL ) {
+ // '*' used to be the comment inserted by the software way back
+ // in antiquity in case none was provided, here for backwards
+ // compatability, acc. to brion -ævar
+ if( $comment == '' || $comment == '*' ) {
+ return '';
+ } else {
+ $formatted = $this->formatComment( $comment, $title );
+ return " <span class=\"comment\">($formatted)</span>";
+ }
+ }
+
+ /**
+ * Wrap and format the given revision's comment block, if the current
+ * user is allowed to view it.
+ * @param $rev Revision object.
+ * @return string HTML
+ */
+ function revComment( $rev ) {
+ if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
+ $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle() );
+ } else {
+ $block = " <span class=\"comment\">" .
+ wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+ }
+ if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
+ return " <span class=\"history-deleted\">$block</span>";
+ }
+ return $block;
+ }
+
+ /** @todo document */
+ function tocIndent() {
+ return "\n<ul>";
+ }
+
+ /** @todo document */
+ function tocUnindent($level) {
+ return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level>0 ? $level : 0 );
+ }
+
+ /**
+ * parameter level defines if we are on an indentation level
+ */
+ function tocLine( $anchor, $tocline, $tocnumber, $level ) {
+ return "\n<li class=\"toclevel-$level\"><a href=\"#" .
+ $anchor . '"><span class="tocnumber">' .
+ $tocnumber . '</span> <span class="toctext">' .
+ $tocline . '</span></a>';
+ }
+
+ /** @todo document */
+ function tocLineEnd() {
+ return "</li>\n";
+ }
+
+ /** @todo document */
+ function tocList($toc) {
+ global $wgJsMimeType;
+ $title = wfMsgForContent('toc') ;
+ return
+ '<table id="toc" class="toc" summary="' . $title .'"><tr><td>'
+ . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
+ . $toc
+ # no trailing newline, script should not be wrapped in a
+ # paragraph
+ . "</ul>\n</td></tr></table>"
+ . '<script type="' . $wgJsMimeType . '">'
+ . ' if (window.showTocToggle) {'
+ . ' var tocShowText = "' . wfEscapeJsString( wfMsgForContent('showtoc') ) . '";'
+ . ' var tocHideText = "' . wfEscapeJsString( wfMsgForContent('hidetoc') ) . '";'
+ . ' showTocToggle();'
+ . ' } '
+ . "</script>\n";
+ }
+
+ /** @todo document */
+ function editSectionLinkForOther( $title, $section ) {
+ global $wgContLang;
+
+ $title = Title::newFromText( $title );
+ $editurl = '&section='.$section;
+ $url = $this->makeKnownLinkObj( $title, wfMsg('editsection'), 'action=edit'.$editurl );
+
+ if( $wgContLang->isRTL() ) {
+ $farside = 'left';
+ $nearside = 'right';
+ } else {
+ $farside = 'right';
+ $nearside = 'left';
+ }
+ return "<div class=\"editsection\" style=\"float:$farside;margin-$nearside:5px;\">[".$url."]</div>";
+
+ }
+
+ /**
+ * @param $title Title object.
+ * @param $section Integer: section number.
+ * @param $hint Link String: title, or default if omitted or empty
+ */
+ function editSectionLink( $nt, $section, $hint='' ) {
+ global $wgContLang;
+
+ $editurl = '&section='.$section;
+ $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"';
+ $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint );
+
+ if( $wgContLang->isRTL() ) {
+ $farside = 'left';
+ $nearside = 'right';
+ } else {
+ $farside = 'right';
+ $nearside = 'left';
+ }
+ return "<div class=\"editsection\" style=\"float:$farside;margin-$nearside:5px;\">[".$url."]</div>";
+ }
+
+ /**
+ * Split a link trail, return the "inside" portion and the remainder of the trail
+ * as a two-element array
+ *
+ * @static
+ */
+ function splitTrail( $trail ) {
+ static $regex = false;
+ if ( $regex === false ) {
+ global $wgContLang;
+ $regex = $wgContLang->linkTrail();
+ }
+ $inside = '';
+ if ( '' != $trail ) {
+ if ( preg_match( $regex, $trail, $m ) ) {
+ $inside = $m[1];
+ $trail = $m[2];
+ }
+ }
+ return array( $inside, $trail );
+ }
+
+}
+?>
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
new file mode 100644
index 00000000..9e25bf07
--- /dev/null
+++ b/includes/LinksUpdate.php
@@ -0,0 +1,601 @@
+<?php
+/**
+ * See deferred.txt
+ * @package MediaWiki
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class LinksUpdate {
+
+ /**@{{
+ * @private
+ */
+ var $mId, //!< Page ID of the article linked from
+ $mTitle, //!< Title object of the article linked from
+ $mLinks, //!< Map of title strings to IDs for the links in the document
+ $mImages, //!< DB keys of the images used, in the array key only
+ $mTemplates, //!< Map of title strings to IDs for the template references, including broken ones
+ $mExternals, //!< URLs of external links, array key only
+ $mCategories, //!< Map of category names to sort keys
+ $mInterlangs, //!< Map of language codes to titles
+ $mDb, //!< Database connection reference
+ $mOptions, //!< SELECT options to be used (array)
+ $mRecursive; //!< Whether to queue jobs for recursive updates
+ /**@}}*/
+
+ /**
+ * Constructor
+ * Initialize private variables
+ * @param $title Integer: FIXME
+ * @param $parserOutput FIXME
+ * @param $recursive Boolean: FIXME, default 'true'.
+ */
+ function LinksUpdate( $title, $parserOutput, $recursive = true ) {
+ global $wgAntiLockFlags;
+
+ if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
+ $this->mOptions = array();
+ } else {
+ $this->mOptions = array( 'FOR UPDATE' );
+ }
+ $this->mDb =& wfGetDB( DB_MASTER );
+
+ if ( !is_object( $title ) ) {
+ throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
+ "Please see Article::editUpdates() for an invocation example.\n" );
+ }
+ $this->mTitle = $title;
+ $this->mId = $title->getArticleID();
+
+ $this->mLinks = $parserOutput->getLinks();
+ $this->mImages = $parserOutput->getImages();
+ $this->mTemplates = $parserOutput->getTemplates();
+ $this->mExternals = $parserOutput->getExternalLinks();
+ $this->mCategories = $parserOutput->getCategories();
+
+ # Convert the format of the interlanguage links
+ # I didn't want to change it in the ParserOutput, because that array is passed all
+ # the way back to the skin, so either a skin API break would be required, or an
+ # inefficient back-conversion.
+ $ill = $parserOutput->getLanguageLinks();
+ $this->mInterlangs = array();
+ foreach ( $ill as $link ) {
+ list( $key, $title ) = explode( ':', $link, 2 );
+ $this->mInterlangs[$key] = $title;
+ }
+
+ $this->mRecursive = $recursive;
+ }
+
+ /**
+ * Update link tables with outgoing links from an updated article
+ */
+ function doUpdate() {
+ global $wgUseDumbLinkUpdate;
+ if ( $wgUseDumbLinkUpdate ) {
+ $this->doDumbUpdate();
+ } else {
+ $this->doIncrementalUpdate();
+ }
+ }
+
+ function doIncrementalUpdate() {
+ $fname = 'LinksUpdate::doIncrementalUpdate';
+ wfProfileIn( $fname );
+
+ # Page links
+ $existing = $this->getExistingLinks();
+ $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
+ $this->getLinkInsertions( $existing ) );
+
+ # Image links
+ $existing = $this->getExistingImages();
+ $this->incrTableUpdate( 'imagelinks', 'il', $this->getImageDeletions( $existing ),
+ $this->getImageInsertions( $existing ) );
+
+ # Invalidate all image description pages which had links added or removed
+ $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
+ $this->invalidateImageDescriptions( $imageUpdates );
+
+ # External links
+ $existing = $this->getExistingExternals();
+ $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
+ $this->getExternalInsertions( $existing ) );
+
+ # Language links
+ $existing = $this->getExistingInterlangs();
+ $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
+ $this->getInterlangInsertions( $existing ) );
+
+ # Template links
+ $existing = $this->getExistingTemplates();
+ $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
+ $this->getTemplateInsertions( $existing ) );
+
+ # Category links
+ $existing = $this->getExistingCategories();
+ $this->incrTableUpdate( 'categorylinks', 'cl', $this->getCategoryDeletions( $existing ),
+ $this->getCategoryInsertions( $existing ) );
+
+ # Invalidate all categories which were added, deleted or changed (set symmetric difference)
+ $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
+ $this->invalidateCategories( $categoryUpdates );
+
+ # Refresh links of all pages including this page
+ # This will be in a separate transaction
+ if ( $this->mRecursive ) {
+ $this->queueRecursiveJobs();
+ }
+
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Link update which clears the previous entries and inserts new ones
+ * May be slower or faster depending on level of lock contention and write speed of DB
+ * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
+ */
+ function doDumbUpdate() {
+ $fname = 'LinksUpdate::doDumbUpdate';
+ wfProfileIn( $fname );
+
+ # Refresh category pages and image description pages
+ $existing = $this->getExistingCategories();
+ $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
+ $existing = $this->getExistingImages();
+ $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
+
+ $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' );
+ $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' );
+ $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
+ $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
+ $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
+ $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(), 'll_from' );
+
+ # Update the cache of all the category pages and image description pages which were changed
+ $this->invalidateCategories( $categoryUpdates );
+ $this->invalidateImageDescriptions( $imageUpdates );
+
+ # Refresh links of all pages including this page
+ # This will be in a separate transaction
+ if ( $this->mRecursive ) {
+ $this->queueRecursiveJobs();
+ }
+
+ wfProfileOut( $fname );
+ }
+
+ function queueRecursiveJobs() {
+ wfProfileIn( __METHOD__ );
+
+ $batchSize = 100;
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( array( 'templatelinks', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ array(
+ 'page_id=tl_from',
+ 'tl_namespace' => $this->mTitle->getNamespace(),
+ 'tl_title' => $this->mTitle->getDBkey()
+ ), __METHOD__
+ );
+
+ $done = false;
+ while ( !$done ) {
+ $jobs = array();
+ for ( $i = 0; $i < $batchSize; $i++ ) {
+ $row = $dbr->fetchObject( $res );
+ if ( !$row ) {
+ $done = true;
+ break;
+ }
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $jobs[] = Job::factory( 'refreshLinks', $title );
+ }
+ Job::batchInsert( $jobs );
+ }
+ $dbr->freeResult( $res );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Invalidate the cache of a list of pages from a single namespace
+ *
+ * @param integer $namespace
+ * @param array $dbkeys
+ */
+ function invalidatePages( $namespace, $dbkeys ) {
+ $fname = 'LinksUpdate::invalidatePages';
+
+ if ( !count( $dbkeys ) ) {
+ return;
+ }
+
+ /**
+ * Determine which pages need to be updated
+ * This is necessary to prevent the job queue from smashing the DB with
+ * large numbers of concurrent invalidations of the same page
+ */
+ $now = $this->mDb->timestamp();
+ $ids = array();
+ $res = $this->mDb->select( 'page', array( 'page_id' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
+ 'page_touched < ' . $this->mDb->addQuotes( $now )
+ ), $fname
+ );
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $ids[] = $row->page_id;
+ }
+ if ( !count( $ids ) ) {
+ return;
+ }
+
+ /**
+ * Do the update
+ * We still need the page_touched condition, in case the row has changed since
+ * the non-locking select above.
+ */
+ $this->mDb->update( 'page', array( 'page_touched' => $now ),
+ array(
+ 'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
+ 'page_touched < ' . $this->mDb->addQuotes( $now )
+ ), $fname
+ );
+ }
+
+ function invalidateCategories( $cats ) {
+ $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
+ }
+
+ function invalidateImageDescriptions( $images ) {
+ $this->invalidatePages( NS_IMAGE, array_keys( $images ) );
+ }
+
+ function dumbTableUpdate( $table, $insertions, $fromField ) {
+ $fname = 'LinksUpdate::dumbTableUpdate';
+ $this->mDb->delete( $table, array( $fromField => $this->mId ), $fname );
+ if ( count( $insertions ) ) {
+ # The link array was constructed without FOR UPDATE, so there may be collisions
+ # This may cause minor link table inconsistencies, which is better than
+ # crippling the site with lock contention.
+ $this->mDb->insert( $table, $insertions, $fname, array( 'IGNORE' ) );
+ }
+ }
+
+ /**
+ * Make a WHERE clause from a 2-d NS/dbkey array
+ *
+ * @param array $arr 2-d array indexed by namespace and DB key
+ * @param string $prefix Field name prefix, without the underscore
+ */
+ function makeWhereFrom2d( &$arr, $prefix ) {
+ $lb = new LinkBatch;
+ $lb->setArray( $arr );
+ return $lb->constructSet( $prefix, $this->mDb );
+ }
+
+ /**
+ * Update a table by doing a delete query then an insert query
+ * @private
+ */
+ function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
+ $fname = 'LinksUpdate::incrTableUpdate';
+ $where = array( "{$prefix}_from" => $this->mId );
+ if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
+ $clause = $this->makeWhereFrom2d( $deletions, $prefix );
+ if ( $clause ) {
+ $where[] = $clause;
+ } else {
+ $where = false;
+ }
+ } else {
+ if ( $table == 'langlinks' ) {
+ $toField = 'll_lang';
+ } else {
+ $toField = $prefix . '_to';
+ }
+ if ( count( $deletions ) ) {
+ $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
+ } else {
+ $where = false;
+ }
+ }
+ if ( $where ) {
+ $this->mDb->delete( $table, $where, $fname );
+ }
+ if ( count( $insertions ) ) {
+ $this->mDb->insert( $table, $insertions, $fname, 'IGNORE' );
+ }
+ }
+
+
+ /**
+ * Get an array of pagelinks insertions for passing to the DB
+ * Skips the titles specified by the 2-D array $existing
+ * @private
+ */
+ function getLinkInsertions( $existing = array() ) {
+ $arr = array();
+ foreach( $this->mLinks as $ns => $dbkeys ) {
+ # array_diff_key() was introduced in PHP 5.1, there is a compatibility function
+ # in GlobalFunctions.php
+ $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
+ foreach ( $diffs as $dbk => $id ) {
+ $arr[] = array(
+ 'pl_from' => $this->mId,
+ 'pl_namespace' => $ns,
+ 'pl_title' => $dbk
+ );
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Get an array of template insertions. Like getLinkInsertions()
+ * @private
+ */
+ function getTemplateInsertions( $existing = array() ) {
+ $arr = array();
+ foreach( $this->mTemplates as $ns => $dbkeys ) {
+ $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
+ foreach ( $diffs as $dbk => $id ) {
+ $arr[] = array(
+ 'tl_from' => $this->mId,
+ 'tl_namespace' => $ns,
+ 'tl_title' => $dbk
+ );
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Get an array of image insertions
+ * Skips the names specified in $existing
+ * @private
+ */
+ function getImageInsertions( $existing = array() ) {
+ $arr = array();
+ $diffs = array_diff_key( $this->mImages, $existing );
+ foreach( $diffs as $iname => $dummy ) {
+ $arr[] = array(
+ 'il_from' => $this->mId,
+ 'il_to' => $iname
+ );
+ }
+ return $arr;
+ }
+
+ /**
+ * Get an array of externallinks insertions. Skips the names specified in $existing
+ * @private
+ */
+ function getExternalInsertions( $existing = array() ) {
+ $arr = array();
+ $diffs = array_diff_key( $this->mExternals, $existing );
+ foreach( $diffs as $url => $dummy ) {
+ $arr[] = array(
+ 'el_from' => $this->mId,
+ 'el_to' => $url,
+ 'el_index' => wfMakeUrlIndex( $url ),
+ );
+ }
+ return $arr;
+ }
+
+ /**
+ * Get an array of category insertions
+ * @param array $existing Array mapping existing category names to sort keys. If both
+ * match a link in $this, the link will be omitted from the output
+ * @private
+ */
+ function getCategoryInsertions( $existing = array() ) {
+ $diffs = array_diff_assoc( $this->mCategories, $existing );
+ $arr = array();
+ foreach ( $diffs as $name => $sortkey ) {
+ $arr[] = array(
+ 'cl_from' => $this->mId,
+ 'cl_to' => $name,
+ 'cl_sortkey' => $sortkey,
+ 'cl_timestamp' => $this->mDb->timestamp()
+ );
+ }
+ return $arr;
+ }
+
+ /**
+ * Get an array of interlanguage link insertions
+ * @param array $existing Array mapping existing language codes to titles
+ * @private
+ */
+ function getInterlangInsertions( $existing = array() ) {
+ $diffs = array_diff_assoc( $this->mInterlangs, $existing );
+ $arr = array();
+ foreach( $diffs as $lang => $title ) {
+ $arr[] = array(
+ 'll_from' => $this->mId,
+ 'll_lang' => $lang,
+ 'll_title' => $title
+ );
+ }
+ return $arr;
+ }
+
+ /**
+ * Given an array of existing links, returns those links which are not in $this
+ * and thus should be deleted.
+ * @private
+ */
+ function getLinkDeletions( $existing ) {
+ $del = array();
+ foreach ( $existing as $ns => $dbkeys ) {
+ if ( isset( $this->mLinks[$ns] ) ) {
+ $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
+ } else {
+ $del[$ns] = $existing[$ns];
+ }
+ }
+ return $del;
+ }
+
+ /**
+ * Given an array of existing templates, returns those templates which are not in $this
+ * and thus should be deleted.
+ * @private
+ */
+ function getTemplateDeletions( $existing ) {
+ $del = array();
+ foreach ( $existing as $ns => $dbkeys ) {
+ if ( isset( $this->mTemplates[$ns] ) ) {
+ $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
+ } else {
+ $del[$ns] = $existing[$ns];
+ }
+ }
+ return $del;
+ }
+
+ /**
+ * Given an array of existing images, returns those images which are not in $this
+ * and thus should be deleted.
+ * @private
+ */
+ function getImageDeletions( $existing ) {
+ return array_diff_key( $existing, $this->mImages );
+ }
+
+ /**
+ * Given an array of existing external links, returns those links which are not
+ * in $this and thus should be deleted.
+ * @private
+ */
+ function getExternalDeletions( $existing ) {
+ return array_diff_key( $existing, $this->mExternals );
+ }
+
+ /**
+ * Given an array of existing categories, returns those categories which are not in $this
+ * and thus should be deleted.
+ * @private
+ */
+ function getCategoryDeletions( $existing ) {
+ return array_diff_assoc( $existing, $this->mCategories );
+ }
+
+ /**
+ * Given an array of existing interlanguage links, returns those links which are not
+ * in $this and thus should be deleted.
+ * @private
+ */
+ function getInterlangDeletions( $existing ) {
+ return array_diff_assoc( $existing, $this->mInterlangs );
+ }
+
+ /**
+ * Get an array of existing links, as a 2-D array
+ * @private
+ */
+ function getExistingLinks() {
+ $fname = 'LinksUpdate::getExistingLinks';
+ $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
+ array( 'pl_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ if ( !isset( $arr[$row->pl_namespace] ) ) {
+ $arr[$row->pl_namespace] = array();
+ }
+ $arr[$row->pl_namespace][$row->pl_title] = 1;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+ /**
+ * Get an array of existing templates, as a 2-D array
+ * @private
+ */
+ function getExistingTemplates() {
+ $fname = 'LinksUpdate::getExistingTemplates';
+ $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
+ array( 'tl_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ if ( !isset( $arr[$row->tl_namespace] ) ) {
+ $arr[$row->tl_namespace] = array();
+ }
+ $arr[$row->tl_namespace][$row->tl_title] = 1;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+ /**
+ * Get an array of existing images, image names in the keys
+ * @private
+ */
+ function getExistingImages() {
+ $fname = 'LinksUpdate::getExistingImages';
+ $res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
+ array( 'il_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $arr[$row->il_to] = 1;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+ /**
+ * Get an array of existing external links, URLs in the keys
+ * @private
+ */
+ function getExistingExternals() {
+ $fname = 'LinksUpdate::getExistingExternals';
+ $res = $this->mDb->select( 'externallinks', array( 'el_to' ),
+ array( 'el_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $arr[$row->el_to] = 1;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+ /**
+ * Get an array of existing categories, with the name in the key and sort key in the value.
+ * @private
+ */
+ function getExistingCategories() {
+ $fname = 'LinksUpdate::getExistingCategories';
+ $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ),
+ array( 'cl_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $arr[$row->cl_to] = $row->cl_sortkey;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+ /**
+ * Get an array of existing interlanguage links, with the language code in the key and the
+ * title in the value.
+ * @private
+ */
+ function getExistingInterlangs() {
+ $fname = 'LinksUpdate::getExistingInterlangs';
+ $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
+ array( 'll_from' => $this->mId ), $fname, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $arr[$row->ll_lang] = $row->ll_title;
+ }
+ return $arr;
+ }
+}
+?>
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
new file mode 100644
index 00000000..f985a7b4
--- /dev/null
+++ b/includes/LoadBalancer.php
@@ -0,0 +1,666 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Depends on the database object
+ */
+require_once( 'Database.php' );
+
+# Valid database indexes
+# Operation-based indexes
+define( 'DB_SLAVE', -1 ); # Read from the slave (or only server)
+define( 'DB_MASTER', -2 ); # Write to master (or only server)
+define( 'DB_LAST', -3 ); # Whatever database was used last
+
+# Obsolete aliases
+define( 'DB_READ', -1 );
+define( 'DB_WRITE', -2 );
+
+
+# Scale polling time so that under overload conditions, the database server
+# receives a SHOW STATUS query at an average interval of this many microseconds
+define( 'AVG_STATUS_POLL', 2000 );
+
+
+/**
+ * Database load balancing object
+ *
+ * @todo document
+ * @package MediaWiki
+ */
+class LoadBalancer {
+ /* private */ var $mServers, $mConnections, $mLoads, $mGroupLoads;
+ /* private */ var $mFailFunction, $mErrorConnection;
+ /* private */ var $mForce, $mReadIndex, $mLastIndex, $mAllowLagged;
+ /* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout;
+ /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
+
+ function LoadBalancer()
+ {
+ $this->mServers = array();
+ $this->mConnections = array();
+ $this->mFailFunction = false;
+ $this->mReadIndex = -1;
+ $this->mForce = -1;
+ $this->mLastIndex = -1;
+ $this->mErrorConnection = false;
+ $this->mAllowLag = false;
+ }
+
+ function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
+ {
+ $lb = new LoadBalancer;
+ $lb->initialise( $servers, $failFunction, $waitTimeout );
+ return $lb;
+ }
+
+ function initialise( $servers, $failFunction = false, $waitTimeout = 10 )
+ {
+ $this->mServers = $servers;
+ $this->mFailFunction = $failFunction;
+ $this->mReadIndex = -1;
+ $this->mWriteIndex = -1;
+ $this->mForce = -1;
+ $this->mConnections = array();
+ $this->mLastIndex = 1;
+ $this->mLoads = array();
+ $this->mWaitForFile = false;
+ $this->mWaitForPos = false;
+ $this->mWaitTimeout = $waitTimeout;
+ $this->mLaggedSlaveMode = false;
+
+ foreach( $servers as $i => $server ) {
+ $this->mLoads[$i] = $server['load'];
+ if ( isset( $server['groupLoads'] ) ) {
+ foreach ( $server['groupLoads'] as $group => $ratio ) {
+ if ( !isset( $this->mGroupLoads[$group] ) ) {
+ $this->mGroupLoads[$group] = array();
+ }
+ $this->mGroupLoads[$group][$i] = $ratio;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given an array of non-normalised probabilities, this function will select
+ * an element and return the appropriate key
+ */
+ function pickRandom( $weights )
+ {
+ if ( !is_array( $weights ) || count( $weights ) == 0 ) {
+ return false;
+ }
+
+ $sum = array_sum( $weights );
+ if ( $sum == 0 ) {
+ # No loads on any of them
+ # In previous versions, this triggered an unweighted random selection,
+ # but this feature has been removed as of April 2006 to allow for strict
+ # separation of query groups.
+ return false;
+ }
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $sum = 0;
+ foreach ( $weights as $i => $w ) {
+ $sum += $w;
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+ return $i;
+ }
+
+ function getRandomNonLagged( $loads ) {
+ # Unset excessively lagged servers
+ $lags = $this->getLagTimes();
+ foreach ( $lags as $i => $lag ) {
+ if ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) {
+ unset( $loads[$i] );
+ }
+ }
+
+ # Find out if all the slaves with non-zero load are lagged
+ $sum = 0;
+ foreach ( $loads as $load ) {
+ $sum += $load;
+ }
+ if ( $sum == 0 ) {
+ # No appropriate DB servers except maybe the master and some slaves with zero load
+ # Do NOT use the master
+ # Instead, this function will return false, triggering read-only mode,
+ # and a lagged slave will be used instead.
+ return false;
+ }
+
+ if ( count( $loads ) == 0 ) {
+ return false;
+ }
+
+ #wfDebugLog( 'connect', var_export( $loads, true ) );
+
+ # Return a random representative of the remainder
+ return $this->pickRandom( $loads );
+ }
+
+ /**
+ * Get the index of the reader connection, which may be a slave
+ * This takes into account load ratios and lag times. It should
+ * always return a consistent index during a given invocation
+ *
+ * Side effect: opens connections to databases
+ */
+ function getReaderIndex() {
+ global $wgReadOnly, $wgDBClusterTimeout;
+
+ $fname = 'LoadBalancer::getReaderIndex';
+ wfProfileIn( $fname );
+
+ $i = false;
+ if ( $this->mForce >= 0 ) {
+ $i = $this->mForce;
+ } else {
+ if ( $this->mReadIndex >= 0 ) {
+ $i = $this->mReadIndex;
+ } else {
+ # $loads is $this->mLoads except with elements knocked out if they
+ # don't work
+ $loads = $this->mLoads;
+ $done = false;
+ $totalElapsed = 0;
+ do {
+ if ( $wgReadOnly or $this->mAllowLagged ) {
+ $i = $this->pickRandom( $loads );
+ } else {
+ $i = $this->getRandomNonLagged( $loads );
+ if ( $i === false && count( $loads ) != 0 ) {
+ # All slaves lagged. Switch to read-only mode
+ $wgReadOnly = wfMsgNoDB( 'readonly_lag' );
+ $i = $this->pickRandom( $loads );
+ }
+ }
+ $serverIndex = $i;
+ if ( $i !== false ) {
+ wfDebugLog( 'connect', "$fname: Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+ $this->openConnection( $i );
+
+ if ( !$this->isOpen( $i ) ) {
+ wfDebug( "$fname: Failed\n" );
+ unset( $loads[$i] );
+ $sleepTime = 0;
+ } else {
+ $status = $this->mConnections[$i]->getStatus("Thread%");
+ if ( isset( $this->mServers[$i]['max threads'] ) &&
+ $status['Threads_running'] > $this->mServers[$i]['max threads'] )
+ {
+ # Too much load, back off and wait for a while.
+ # The sleep time is scaled by the number of threads connected,
+ # to produce a roughly constant global poll rate.
+ $sleepTime = AVG_STATUS_POLL * $status['Threads_connected'];
+
+ # If we reach the timeout and exit the loop, don't use it
+ $i = false;
+ } else {
+ $done = true;
+ $sleepTime = 0;
+ }
+ }
+ } else {
+ $sleepTime = 500000;
+ }
+ if ( $sleepTime ) {
+ $totalElapsed += $sleepTime;
+ $x = "{$this->mServers[$serverIndex]['host']} [$serverIndex]";
+ wfProfileIn( "$fname-sleep $x" );
+ usleep( $sleepTime );
+ wfProfileOut( "$fname-sleep $x" );
+ }
+ } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $wgDBClusterTimeout );
+
+ if ( $totalElapsed / 1e6 >= $wgDBClusterTimeout ) {
+ $this->mErrorConnection = false;
+ $this->mLastError = 'All servers busy';
+ }
+
+ if ( $i !== false && $this->isOpen( $i ) ) {
+ # Wait for the session master pos for a short time
+ if ( $this->mWaitForFile ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
+ }
+ }
+ if ( $i !== false ) {
+ $this->mReadIndex = $i;
+ }
+ } else {
+ $i = false;
+ }
+ }
+ }
+ wfProfileOut( $fname );
+ return $i;
+ }
+
+ /**
+ * Get a random server to use in a query group
+ */
+ function getGroupIndex( $group ) {
+ if ( isset( $this->mGroupLoads[$group] ) ) {
+ $i = $this->pickRandom( $this->mGroupLoads[$group] );
+ } else {
+ $i = false;
+ }
+ wfDebug( "Query group $group => $i\n" );
+ return $i;
+ }
+
+ /**
+ * Set the master wait position
+ * If a DB_SLAVE connection has been opened already, waits
+ * Otherwise sets a variable telling it to wait if such a connection is opened
+ */
+ function waitFor( $file, $pos ) {
+ $fname = 'LoadBalancer::waitFor';
+ wfProfileIn( $fname );
+
+ wfDebug( "User master pos: $file $pos\n" );
+ $this->mWaitForFile = false;
+ $this->mWaitForPos = false;
+
+ if ( count( $this->mServers ) > 1 ) {
+ $this->mWaitForFile = $file;
+ $this->mWaitForPos = $pos;
+ $i = $this->mReadIndex;
+
+ if ( $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
+ $this->mLaggedSlaveMode = true;
+ }
+ }
+ }
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Wait for a given slave to catch up to the master pos stored in $this
+ */
+ function doWait( $index ) {
+ global $wgMemc;
+
+ $retVal = false;
+
+ # Debugging hacks
+ if ( isset( $this->mServers[$index]['lagged slave'] ) ) {
+ return false;
+ } elseif ( isset( $this->mServers[$index]['fake slave'] ) ) {
+ return true;
+ }
+
+ $key = 'masterpos:' . $index;
+ $memcPos = $wgMemc->get( $key );
+ if ( $memcPos ) {
+ list( $file, $pos ) = explode( ' ', $memcPos );
+ # If the saved position is later than the requested position, return now
+ if ( $file == $this->mWaitForFile && $this->mWaitForPos <= $pos ) {
+ $retVal = true;
+ }
+ }
+
+ if ( !$retVal && $this->isOpen( $index ) ) {
+ $conn =& $this->mConnections[$index];
+ wfDebug( "Waiting for slave #$index to catch up...\n" );
+ $result = $conn->masterPosWait( $this->mWaitForFile, $this->mWaitForPos, $this->mWaitTimeout );
+
+ if ( $result == -1 || is_null( $result ) ) {
+ # Timed out waiting for slave, use master instead
+ wfDebug( "Timed out waiting for slave #$index pos {$this->mWaitForFile} {$this->mWaitForPos}\n" );
+ $retVal = false;
+ } else {
+ $retVal = true;
+ wfDebug( "Done\n" );
+ }
+ }
+ return $retVal;
+ }
+
+ /**
+ * Get a connection by index
+ */
+ function &getConnection( $i, $fail = true, $groups = array() )
+ {
+ global $wgDBtype;
+ $fname = 'LoadBalancer::getConnection';
+ wfProfileIn( $fname );
+
+
+ # Query groups
+ if ( !is_array( $groups ) ) {
+ $groupIndex = $this->getGroupIndex( $groups, $i );
+ if ( $groupIndex !== false ) {
+ $i = $groupIndex;
+ }
+ } else {
+ foreach ( $groups as $group ) {
+ $groupIndex = $this->getGroupIndex( $group, $i );
+ if ( $groupIndex !== false ) {
+ $i = $groupIndex;
+ break;
+ }
+ }
+ }
+
+ # For now, only go through all this for mysql databases
+ if ($wgDBtype != 'mysql') {
+ $i = $this->getWriterIndex();
+ }
+ # Operation-based index
+ elseif ( $i == DB_SLAVE ) {
+ $i = $this->getReaderIndex();
+ } elseif ( $i == DB_MASTER ) {
+ $i = $this->getWriterIndex();
+ } elseif ( $i == DB_LAST ) {
+ # Just use $this->mLastIndex, which should already be set
+ $i = $this->mLastIndex;
+ if ( $i === -1 ) {
+ # Oh dear, not set, best to use the writer for safety
+ wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
+ $i = $this->getWriterIndex();
+ }
+ }
+ # Couldn't find a working server in getReaderIndex()?
+ if ( $i === false ) {
+ $this->reportConnectionError( $this->mErrorConnection );
+ }
+ # Now we have an explicit index into the servers array
+ $this->openConnection( $i, $fail );
+
+ wfProfileOut( $fname );
+ return $this->mConnections[$i];
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array
+ * Returns success
+ * @access private
+ */
+ function openConnection( $i, $fail = false ) {
+ $fname = 'LoadBalancer::openConnection';
+ wfProfileIn( $fname );
+ $success = true;
+
+ if ( !$this->isOpen( $i ) ) {
+ $this->mConnections[$i] = $this->reallyOpenConnection( $this->mServers[$i] );
+ }
+
+ if ( !$this->isOpen( $i ) ) {
+ wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
+ if ( $fail ) {
+ $this->reportConnectionError( $this->mConnections[$i] );
+ }
+ $this->mErrorConnection = $this->mConnections[$i];
+ $this->mConnections[$i] = false;
+ $success = false;
+ }
+ $this->mLastIndex = $i;
+ wfProfileOut( $fname );
+ return $success;
+ }
+
+ /**
+ * Test if the specified index represents an open connection
+ * @access private
+ */
+ function isOpen( $index ) {
+ if( !is_integer( $index ) ) {
+ return false;
+ }
+ if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) &&
+ $this->mConnections[$index]->isOpen() )
+ {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Really opens a connection
+ * @access private
+ */
+ function reallyOpenConnection( &$server ) {
+ if( !is_array( $server ) ) {
+ throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
+ }
+
+ extract( $server );
+ # Get class for this database type
+ $class = 'Database' . ucfirst( $type );
+ if ( !class_exists( $class ) ) {
+ require_once( "$class.php" );
+ }
+
+ # Create object
+ $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+ $db->setLBInfo( $server );
+ return $db;
+ }
+
+ function reportConnectionError( &$conn )
+ {
+ $fname = 'LoadBalancer::reportConnectionError';
+ wfProfileIn( $fname );
+ # Prevent infinite recursion
+
+ static $reporting = false;
+ if ( !$reporting ) {
+ $reporting = true;
+ if ( !is_object( $conn ) ) {
+ // No last connection, probably due to all servers being too busy
+ $conn = new Database;
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ $conn->reportConnectionError( $this->mLastError );
+ } else {
+ // If all servers were busy, mLastError will contain something sensible
+ throw new DBConnectionError( $conn, $this->mLastError );
+ }
+ } else {
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ } else {
+ $conn->failFunction( false );
+ }
+ $server = $conn->getProperty( 'mServer' );
+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+ }
+ $reporting = false;
+ }
+ wfProfileOut( $fname );
+ }
+
+ function getWriterIndex() {
+ return 0;
+ }
+
+ /**
+ * Force subsequent calls to getConnection(DB_SLAVE) to return the
+ * given index. Set to -1 to restore the original load balancing
+ * behaviour. I thought this was a good idea when I originally
+ * wrote this class, but it has never been used.
+ */
+ function force( $i ) {
+ $this->mForce = $i;
+ }
+
+ /**
+ * Returns true if the specified index is a valid server index
+ */
+ function haveIndex( $i ) {
+ return array_key_exists( $i, $this->mServers );
+ }
+
+ /**
+ * Returns true if the specified index is valid and has non-zero load
+ */
+ function isNonZeroLoad( $i ) {
+ return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
+ }
+
+ /**
+ * Get the number of defined servers (not the number of open connections)
+ */
+ function getServerCount() {
+ return count( $this->mServers );
+ }
+
+ /**
+ * Save master pos to the session and to memcached, if the session exists
+ */
+ function saveMasterPos() {
+ global $wgSessionStarted;
+ if ( $wgSessionStarted && count( $this->mServers ) > 1 ) {
+ # If this entire request was served from a slave without opening a connection to the
+ # master (however unlikely that may be), then we can fetch the position from the slave.
+ if ( empty( $this->mConnections[0] ) ) {
+ $conn =& $this->getConnection( DB_SLAVE );
+ list( $file, $pos ) = $conn->getSlavePos();
+ wfDebug( "Saving master pos fetched from slave: $file $pos\n" );
+ } else {
+ $conn =& $this->getConnection( 0 );
+ list( $file, $pos ) = $conn->getMasterPos();
+ wfDebug( "Saving master pos: $file $pos\n" );
+ }
+ if ( $file !== false ) {
+ $_SESSION['master_log_file'] = $file;
+ $_SESSION['master_pos'] = $pos;
+ }
+ }
+ }
+
+ /**
+ * Loads the master pos from the session, waits for it if necessary
+ */
+ function loadMasterPos() {
+ if ( isset( $_SESSION['master_log_file'] ) && isset( $_SESSION['master_pos'] ) ) {
+ $this->waitFor( $_SESSION['master_log_file'], $_SESSION['master_pos'] );
+ }
+ }
+
+ /**
+ * Close all open connections
+ */
+ function closeAll() {
+ foreach( $this->mConnections as $i => $conn ) {
+ if ( $this->isOpen( $i ) ) {
+ // Need to use this syntax because $conn is a copy not a reference
+ $this->mConnections[$i]->close();
+ }
+ }
+ }
+
+ function commitAll() {
+ foreach( $this->mConnections as $i => $conn ) {
+ if ( $this->isOpen( $i ) ) {
+ // Need to use this syntax because $conn is a copy not a reference
+ $this->mConnections[$i]->immediateCommit();
+ }
+ }
+ }
+
+ function waitTimeout( $value = NULL ) {
+ return wfSetVar( $this->mWaitTimeout, $value );
+ }
+
+ function getLaggedSlaveMode() {
+ return $this->mLaggedSlaveMode;
+ }
+
+ /* Disables/enables lag checks */
+ function allowLagged($mode=null) {
+ if ($mode===null)
+ return $this->mAllowLagged;
+ $this->mAllowLagged=$mode;
+ }
+
+ function pingAll() {
+ $success = true;
+ foreach ( $this->mConnections as $i => $conn ) {
+ if ( $this->isOpen( $i ) ) {
+ if ( !$this->mConnections[$i]->ping() ) {
+ $success = false;
+ }
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Get the hostname and lag time of the most-lagged slave
+ * This is useful for maintenance scripts that need to throttle their updates
+ */
+ function getMaxLag() {
+ $maxLag = -1;
+ $host = '';
+ foreach ( $this->mServers as $i => $conn ) {
+ if ( $this->openConnection( $i ) ) {
+ $lag = $this->mConnections[$i]->getLag();
+ if ( $lag > $maxLag ) {
+ $maxLag = $lag;
+ $host = $this->mServers[$i]['host'];
+ }
+ }
+ }
+ return array( $host, $maxLag );
+ }
+
+ /**
+ * Get lag time for each DB
+ * Results are cached for a short time in memcached
+ */
+ function getLagTimes() {
+ global $wgDBname;
+
+ $expiry = 5;
+ $requestRate = 10;
+
+ global $wgMemc;
+ $times = $wgMemc->get( "$wgDBname:lag_times" );
+ if ( $times ) {
+ # Randomly recache with probability rising over $expiry
+ $elapsed = time() - $times['timestamp'];
+ $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+ if ( mt_rand( 0, $chance ) != 0 ) {
+ unset( $times['timestamp'] );
+ return $times;
+ }
+ }
+
+ # Cache key missing or expired
+
+ $times = array();
+ foreach ( $this->mServers as $i => $conn ) {
+ if ($i==0) { # Master
+ $times[$i] = 0;
+ } elseif ( $this->openConnection( $i ) ) {
+ $times[$i] = $this->mConnections[$i]->getLag();
+ }
+ }
+
+ # Add a timestamp key so we know when it was cached
+ $times['timestamp'] = time();
+ $wgMemc->set( "$wgDBname:lag_times", $times, $expiry );
+
+ # But don't give the timestamp to the caller
+ unset($times['timestamp']);
+ return $times;
+ }
+}
+
+?>
diff --git a/includes/LogPage.php b/includes/LogPage.php
new file mode 100644
index 00000000..f588105f
--- /dev/null
+++ b/includes/LogPage.php
@@ -0,0 +1,246 @@
+<?php
+#
+# Copyright (C) 2002, 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
+
+/**
+ * Contain log classes
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Class to simplify the use of log pages.
+ * The logs are now kept in a table which is easier to manage and trim
+ * than ever-growing wiki pages.
+ *
+ * @package MediaWiki
+ */
+class LogPage {
+ /* @access private */
+ var $type, $action, $comment, $params, $target;
+ /* @acess public */
+ var $updateRecentChanges;
+
+ /**
+ * Constructor
+ *
+ * @param string $type One of '', 'block', 'protect', 'rights', 'delete',
+ * 'upload', 'move'
+ * @param bool $rc Whether to update recent changes as well as the logging table
+ */
+ function LogPage( $type, $rc = true ) {
+ $this->type = $type;
+ $this->updateRecentChanges = $rc;
+ }
+
+ function saveContent() {
+ if( wfReadOnly() ) return false;
+
+ global $wgUser;
+ $fname = 'LogPage::saveContent';
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $uid = $wgUser->getID();
+
+ $this->timestamp = $now = wfTimestampNow();
+ $dbw->insert( 'logging',
+ array(
+ 'log_type' => $this->type,
+ 'log_action' => $this->action,
+ 'log_timestamp' => $dbw->timestamp( $now ),
+ 'log_user' => $uid,
+ 'log_namespace' => $this->target->getNamespace(),
+ 'log_title' => $this->target->getDBkey(),
+ 'log_comment' => $this->comment,
+ 'log_params' => $this->params
+ ), $fname
+ );
+
+ # And update recentchanges
+ if ( $this->updateRecentChanges ) {
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Log/' . $this->type );
+ $rcComment = $this->actionText;
+ if( '' != $this->comment ) {
+ if ($rcComment == '')
+ $rcComment = $this->comment;
+ else
+ $rcComment .= ': ' . $this->comment;
+ }
+
+ RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
+ $this->type, $this->action, $this->target, $this->comment, $this->params );
+ }
+ return true;
+ }
+
+ /**
+ * @static
+ */
+ function validTypes() {
+ global $wgLogTypes;
+ return $wgLogTypes;
+ }
+
+ /**
+ * @static
+ */
+ function isLogType( $type ) {
+ return in_array( $type, LogPage::validTypes() );
+ }
+
+ /**
+ * @static
+ */
+ function logName( $type ) {
+ global $wgLogNames;
+
+ if( isset( $wgLogNames[$type] ) ) {
+ return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) );
+ } else {
+ // Bogus log types? Perhaps an extension was removed.
+ return $type;
+ }
+ }
+
+ /**
+ * @fixme: handle missing log types
+ * @static
+ */
+ function logHeader( $type ) {
+ global $wgLogHeaders;
+ return wfMsg( $wgLogHeaders[$type] );
+ }
+
+ /**
+ * @static
+ */
+ function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) {
+ global $wgLang, $wgContLang, $wgLogActions;
+
+ $key = "$type/$action";
+ if( isset( $wgLogActions[$key] ) ) {
+ if( is_null( $title ) ) {
+ $rv=wfMsg( $wgLogActions[$key] );
+ } else {
+ if( $skin ) {
+
+ switch( $type ) {
+ case 'move':
+ $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
+ $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), $params[0] );
+ break;
+ case 'block':
+ if( substr( $title->getText(), 0, 1 ) == '#' ) {
+ $titleLink = $title->getText();
+ } else {
+ $titleLink = $skin->makeLinkObj( $title, $title->getText() );
+ $titleLink .= ' (' . $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions/' . $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')';
+ }
+ break;
+ case 'rights':
+ $text = $wgContLang->ucfirst( $title->getText() );
+ $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
+ break;
+ default:
+ $titleLink = $skin->makeLinkObj( $title );
+ }
+
+ } else {
+ $titleLink = $title->getPrefixedText();
+ }
+ if( $key == 'rights/rights' ) {
+ if ($skin) {
+ $rightsnone = wfMsg( 'rightsnone' );
+ } else {
+ $rightsnone = wfMsgForContent( 'rightsnone' );
+ }
+ if( !isset( $params[0] ) || trim( $params[0] ) == '' )
+ $params[0] = $rightsnone;
+ if( !isset( $params[1] ) || trim( $params[1] ) == '' )
+ $params[1] = $rightsnone;
+ }
+ if( count( $params ) == 0 ) {
+ if ( $skin ) {
+ $rv = wfMsg( $wgLogActions[$key], $titleLink );
+ } else {
+ $rv = wfMsgForContent( $wgLogActions[$key], $titleLink );
+ }
+ } else {
+ array_unshift( $params, $titleLink );
+ if ( $translate && $key == 'block/block' ) {
+ $params[1] = $wgLang->translateBlockExpiry($params[1]);
+ }
+ $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin );
+ }
+ }
+ } else {
+ wfDebug( "LogPage::actionText - unknown action $key\n" );
+ $rv = "$action";
+ }
+ if( $filterWikilinks ) {
+ $rv = str_replace( "[[", "", $rv );
+ $rv = str_replace( "]]", "", $rv );
+ }
+ return $rv;
+ }
+
+ /**
+ * Add a log entry
+ * @param string $action one of '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'move_redir'
+ * @param object &$target A title object.
+ * @param string $comment Description associated
+ * @param array $params Parameters passed later to wfMsg.* functions
+ */
+ function addEntry( $action, &$target, $comment, $params = array() ) {
+ if ( !is_array( $params ) ) {
+ $params = array( $params );
+ }
+
+ $this->action = $action;
+ $this->target =& $target;
+ $this->comment = $comment;
+ $this->params = LogPage::makeParamBlob( $params );
+
+ $this->actionText = LogPage::actionText( $this->type, $action, $target, NULL, $params );
+
+ return $this->saveContent();
+ }
+
+ /**
+ * Create a blob from a parameter array
+ * @static
+ */
+ function makeParamBlob( $params ) {
+ return implode( "\n", $params );
+ }
+
+ /**
+ * Extract a parameter array from a blob
+ * @static
+ */
+ function extractParams( $blob ) {
+ if ( $blob === '' ) {
+ return array();
+ } else {
+ return explode( "\n", $blob );
+ }
+ }
+}
+
+?>
diff --git a/includes/MacBinary.php b/includes/MacBinary.php
new file mode 100644
index 00000000..05c3ce5c
--- /dev/null
+++ b/includes/MacBinary.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * MacBinary signature checker and data fork extractor, for files
+ * uploaded from Internet Explorer for Mac.
+ *
+ * Copyright (C) 2005 Brion Vibber <brion@pobox.com>
+ * Portions based on Convert::BinHex by Eryq et al
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+class MacBinary {
+ function MacBinary( $filename ) {
+ $this->open( $filename );
+ $this->loadHeader();
+ }
+
+ /**
+ * The file must be seekable, such as local filesystem.
+ * Remote URLs probably won't work.
+ *
+ * @param string $filename
+ */
+ function open( $filename ) {
+ $this->valid = false;
+ $this->version = 0;
+ $this->filename = '';
+ $this->dataLength = 0;
+ $this->resourceLength = 0;
+ $this->handle = fopen( $filename, 'rb' );
+ }
+
+ /**
+ * Does this appear to be a valid MacBinary archive?
+ * @return bool
+ */
+ function isValid() {
+ return $this->valid;
+ }
+
+ /**
+ * Get length of data fork
+ * @return int
+ */
+ function dataForkLength() {
+ return $this->dataLength;
+ }
+
+ /**
+ * Copy the data fork to an external file or resource.
+ * @param resource $destination
+ * @return bool
+ */
+ function extractData( $destination ) {
+ if( !$this->isValid() ) {
+ return false;
+ }
+
+ // Data fork appears immediately after header
+ fseek( $this->handle, 128 );
+ return $this->copyBytesTo( $destination, $this->dataLength );
+ }
+
+ /**
+ *
+ */
+ function close() {
+ fclose( $this->handle );
+ }
+
+ // --------------------------------------------------------------
+
+ /**
+ * Check if the given file appears to be MacBinary-encoded,
+ * as Internet Explorer on Mac OS may provide for unknown types.
+ * http://www.lazerware.com/formats/macbinary/macbinary_iii.html
+ * If ok, load header data.
+ *
+ * @return bool
+ * @access private
+ */
+ function loadHeader() {
+ $fname = 'MacBinary::loadHeader';
+
+ fseek( $this->handle, 0 );
+ $head = fread( $this->handle, 128 );
+ $this->hexdump( $head );
+
+ if( strlen( $head ) < 128 ) {
+ wfDebug( "$fname: couldn't read full MacBinary header\n" );
+ return false;
+ }
+
+ if( $head{0} != "\x00" || $head{74} != "\x00" ) {
+ wfDebug( "$fname: header bytes 0 and 74 not null\n" );
+ return false;
+ }
+
+ $signature = substr( $head, 102, 4 );
+ $a = unpack( "ncrc", substr( $head, 124, 2 ) );
+ $storedCRC = $a['crc'];
+ $calculatedCRC = $this->calcCRC( substr( $head, 0, 124 ) );
+ if( $storedCRC == $calculatedCRC ) {
+ if( $signature == 'mBIN' ) {
+ $this->version = 3;
+ } else {
+ $this->version = 2;
+ }
+ } else {
+ $crc = sprintf( "%x != %x", $storedCRC, $calculatedCRC );
+ if( $storedCRC == 0 && $head{82} == "\x00" &&
+ substr( $head, 101, 24 ) == str_repeat( "\x00", 24 ) ) {
+ wfDebug( "$fname: no CRC, looks like MacBinary I\n" );
+ $this->version = 1;
+ } elseif( $signature == 'mBIN' && $storedCRC == 0x185 ) {
+ // Mac IE 5.0 seems to insert this value in the CRC field.
+ // 5.2.3 works correctly; don't know about other versions.
+ wfDebug( "$fname: CRC doesn't match ($crc), looks like Mac IE 5.0\n" );
+ $this->version = 3;
+ } else {
+ wfDebug( "$fname: CRC doesn't match ($crc) and not MacBinary I\n" );
+ return false;
+ }
+ }
+
+ $nameLength = ord( $head{1} );
+ if( $nameLength < 1 || $nameLength > 63 ) {
+ wfDebug( "$fname: invalid filename size $nameLength\n" );
+ return false;
+ }
+ $this->filename = substr( $head, 2, $nameLength );
+
+ $forks = unpack( "Ndata/Nresource", substr( $head, 83, 8 ) );
+ $this->dataLength = $forks['data'];
+ $this->resourceLength = $forks['resource'];
+ $maxForkLength = 0x7fffff;
+
+ if( $this->dataLength < 0 || $this->dataLength > $maxForkLength ) {
+ wfDebug( "$fname: invalid data fork length $this->dataLength\n" );
+ return false;
+ }
+
+ if( $this->resourceLength < 0 || $this->resourceLength > $maxForkLength ) {
+ wfDebug( "$fname: invalid resource fork size $this->resourceLength\n" );
+ return false;
+ }
+
+ wfDebug( "$fname: appears to be MacBinary $this->version, data length $this->dataLength\n" );
+ $this->valid = true;
+ return true;
+ }
+
+ /**
+ * Calculate a 16-bit CRC value as for MacBinary headers.
+ * Adapted from perl5 Convert::BinHex by Eryq,
+ * based on the mcvert utility (Doug Moore, April '87),
+ * with magic array thingy by Jim Van Verth.
+ * http://search.cpan.org/~eryq/Convert-BinHex-1.119/lib/Convert/BinHex.pm
+ *
+ * @param string $data
+ * @param int $seed
+ * @return int
+ * @access private
+ */
+ function calcCRC( $data, $seed = 0 ) {
+ # An array useful for CRC calculations that use 0x1021 as the "seed":
+ $MAGIC = array(
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
+ 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
+ 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
+ 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
+ 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
+ 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
+ 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
+ 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
+ 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
+ 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
+ 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
+ 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
+ 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
+ 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
+ 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
+ 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
+ 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
+ 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
+ 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
+ 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
+ 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
+ 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
+ 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
+ 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
+ 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
+ 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
+ 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
+ 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
+ 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
+ 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
+ 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
+ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
+ );
+ $len = strlen( $data );
+ $crc = $seed;
+ for( $i = 0; $i < $len; $i++ ) {
+ $crc ^= ord( $data{$i} ) << 8;
+ $crc &= 0xFFFF;
+ $crc = ($crc << 8) ^ $MAGIC[$crc >> 8];
+ $crc &= 0xFFFF;
+ }
+ return $crc;
+ }
+
+ /**
+ * @param resource $destination
+ * @param int $bytesToCopy
+ * @return bool
+ * @access private
+ */
+ function copyBytesTo( $destination, $bytesToCopy ) {
+ $bufferSize = 65536;
+ for( $remaining = $bytesToCopy; $remaining > 0; $remaining -= $bufferSize ) {
+ $thisChunkSize = min( $remaining, $bufferSize );
+ $buffer = fread( $this->handle, $thisChunkSize );
+ fwrite( $destination, $buffer );
+ }
+ }
+
+ /**
+ * Hex dump of the header for debugging
+ * @access private
+ */
+ function hexdump( $data ) {
+ global $wgDebugLogFile;
+ if( !$wgDebugLogFile ) return;
+
+ $width = 16;
+ $at = 0;
+ for( $remaining = strlen( $data ); $remaining > 0; $remaining -= $width ) {
+ $line = sprintf( "%04x:", $at );
+ $printable = '';
+ for( $i = 0; $i < $width && $remaining - $i > 0; $i++ ) {
+ $byte = ord( $data{$at++} );
+ $line .= sprintf( " %02x", $byte );
+ $printable .= ($byte >= 32 && $byte <= 126 )
+ ? chr( $byte )
+ : '.';
+ }
+ if( $i < $width ) {
+ $line .= str_repeat( ' ', $width - $i );
+ }
+ wfDebug( "MacBinary: $line $printable\n" );
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
new file mode 100644
index 00000000..c80d2583
--- /dev/null
+++ b/includes/MagicWord.php
@@ -0,0 +1,448 @@
+<?php
+/**
+ * File for magic words
+ * @package MediaWiki
+ * @subpackage Parser
+ */
+
+/**
+ * private
+ */
+$wgMagicFound = false;
+
+/** Actual keyword to be used is set in Language.php */
+
+$magicWords = array(
+ 'MAG_REDIRECT',
+ 'MAG_NOTOC',
+ 'MAG_START',
+ 'MAG_CURRENTMONTH',
+ 'MAG_CURRENTMONTHNAME',
+ 'MAG_CURRENTMONTHNAMEGEN',
+ 'MAG_CURRENTMONTHABBREV',
+ 'MAG_CURRENTDAY',
+ 'MAG_CURRENTDAY2',
+ 'MAG_CURRENTDAYNAME',
+ 'MAG_CURRENTYEAR',
+ 'MAG_CURRENTTIME',
+ 'MAG_NUMBEROFARTICLES',
+ 'MAG_SUBST',
+ 'MAG_MSG',
+ 'MAG_MSGNW',
+ 'MAG_NOEDITSECTION',
+ 'MAG_END',
+ 'MAG_IMG_THUMBNAIL',
+ 'MAG_IMG_RIGHT',
+ 'MAG_IMG_LEFT',
+ 'MAG_IMG_NONE',
+ 'MAG_IMG_WIDTH',
+ 'MAG_IMG_CENTER',
+ 'MAG_INT',
+ 'MAG_FORCETOC',
+ 'MAG_SITENAME',
+ 'MAG_NS',
+ 'MAG_LOCALURL',
+ 'MAG_LOCALURLE',
+ 'MAG_SERVER',
+ 'MAG_IMG_FRAMED',
+ 'MAG_PAGENAME',
+ 'MAG_PAGENAMEE',
+ 'MAG_NAMESPACE',
+ 'MAG_NAMESPACEE',
+ 'MAG_TOC',
+ 'MAG_GRAMMAR',
+ 'MAG_NOTITLECONVERT',
+ 'MAG_NOCONTENTCONVERT',
+ 'MAG_CURRENTWEEK',
+ 'MAG_CURRENTDOW',
+ 'MAG_REVISIONID',
+ 'MAG_SCRIPTPATH',
+ 'MAG_SERVERNAME',
+ 'MAG_NUMBEROFFILES',
+ 'MAG_IMG_MANUALTHUMB',
+ 'MAG_PLURAL',
+ 'MAG_FULLURL',
+ 'MAG_FULLURLE',
+ 'MAG_LCFIRST',
+ 'MAG_UCFIRST',
+ 'MAG_LC',
+ 'MAG_UC',
+ 'MAG_FULLPAGENAME',
+ 'MAG_FULLPAGENAMEE',
+ 'MAG_RAW',
+ 'MAG_SUBPAGENAME',
+ 'MAG_SUBPAGENAMEE',
+ 'MAG_DISPLAYTITLE',
+ 'MAG_TALKSPACE',
+ 'MAG_TALKSPACEE',
+ 'MAG_SUBJECTSPACE',
+ 'MAG_SUBJECTSPACEE',
+ 'MAG_TALKPAGENAME',
+ 'MAG_TALKPAGENAMEE',
+ 'MAG_SUBJECTPAGENAME',
+ 'MAG_SUBJECTPAGENAMEE',
+ 'MAG_NUMBEROFUSERS',
+ 'MAG_RAWSUFFIX',
+ 'MAG_NEWSECTIONLINK',
+ 'MAG_NUMBEROFPAGES',
+ 'MAG_CURRENTVERSION',
+ 'MAG_BASEPAGENAME',
+ 'MAG_BASEPAGENAMEE',
+ 'MAG_URLENCODE',
+ 'MAG_CURRENTTIMESTAMP',
+ 'MAG_DIRECTIONMARK',
+ 'MAG_LANGUAGE',
+ 'MAG_CONTENTLANGUAGE',
+ 'MAG_PAGESINNAMESPACE',
+ 'MAG_NOGALLERY',
+ 'MAG_NUMBEROFADMINS',
+ 'MAG_FORMATNUM',
+);
+if ( ! defined( 'MEDIAWIKI_INSTALL' ) )
+ wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) );
+
+for ( $i = 0; $i < count( $magicWords ); ++$i )
+ define( $magicWords[$i], $i );
+
+$wgVariableIDs = array(
+ MAG_CURRENTMONTH,
+ MAG_CURRENTMONTHNAME,
+ MAG_CURRENTMONTHNAMEGEN,
+ MAG_CURRENTMONTHABBREV,
+ MAG_CURRENTDAY,
+ MAG_CURRENTDAY2,
+ MAG_CURRENTDAYNAME,
+ MAG_CURRENTYEAR,
+ MAG_CURRENTTIME,
+ MAG_NUMBEROFARTICLES,
+ MAG_NUMBEROFFILES,
+ MAG_SITENAME,
+ MAG_SERVER,
+ MAG_SERVERNAME,
+ MAG_SCRIPTPATH,
+ MAG_PAGENAME,
+ MAG_PAGENAMEE,
+ MAG_FULLPAGENAME,
+ MAG_FULLPAGENAMEE,
+ MAG_NAMESPACE,
+ MAG_NAMESPACEE,
+ MAG_CURRENTWEEK,
+ MAG_CURRENTDOW,
+ MAG_REVISIONID,
+ MAG_SUBPAGENAME,
+ MAG_SUBPAGENAMEE,
+ MAG_DISPLAYTITLE,
+ MAG_TALKSPACE,
+ MAG_TALKSPACEE,
+ MAG_SUBJECTSPACE,
+ MAG_SUBJECTSPACEE,
+ MAG_TALKPAGENAME,
+ MAG_TALKPAGENAMEE,
+ MAG_SUBJECTPAGENAME,
+ MAG_SUBJECTPAGENAMEE,
+ MAG_NUMBEROFUSERS,
+ MAG_RAWSUFFIX,
+ MAG_NEWSECTIONLINK,
+ MAG_NUMBEROFPAGES,
+ MAG_CURRENTVERSION,
+ MAG_BASEPAGENAME,
+ MAG_BASEPAGENAMEE,
+ MAG_URLENCODE,
+ MAG_CURRENTTIMESTAMP,
+ MAG_DIRECTIONMARK,
+ MAG_LANGUAGE,
+ MAG_CONTENTLANGUAGE,
+ MAG_PAGESINNAMESPACE,
+ MAG_NUMBEROFADMINS,
+);
+if ( ! defined( 'MEDIAWIKI_INSTALL' ) )
+ wfRunHooks( 'MagicWordwgVariableIDs', array( &$wgVariableIDs ) );
+
+/**
+ * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc.
+ * Usage:
+ * if (MagicWord::get( MAG_REDIRECT )->match( $text ) )
+ *
+ * Possible future improvements:
+ * * Simultaneous searching for a number of magic words
+ * * $wgMagicWords in shared memory
+ *
+ * Please avoid reading the data out of one of these objects and then writing
+ * special case code. If possible, add another match()-like function here.
+ *
+ * @package MediaWiki
+ */
+class MagicWord {
+ /**#@+
+ * @private
+ */
+ var $mId, $mSynonyms, $mCaseSensitive, $mRegex;
+ var $mRegexStart, $mBaseRegex, $mVariableRegex;
+ var $mModified;
+ /**#@-*/
+
+ function MagicWord($id = 0, $syn = '', $cs = false) {
+ $this->mId = $id;
+ $this->mSynonyms = (array)$syn;
+ $this->mCaseSensitive = $cs;
+ $this->mRegex = '';
+ $this->mRegexStart = '';
+ $this->mVariableRegex = '';
+ $this->mVariableStartToEndRegex = '';
+ $this->mModified = false;
+ }
+
+ /**
+ * Factory: creates an object representing an ID
+ * @static
+ */
+ function &get( $id ) {
+ global $wgMagicWords;
+
+ if ( !is_array( $wgMagicWords ) ) {
+ throw new MWException( "Incorrect initialisation order, \$wgMagicWords does not exist\n" );
+ }
+ if (!array_key_exists( $id, $wgMagicWords ) ) {
+ $mw = new MagicWord();
+ $mw->load( $id );
+ $wgMagicWords[$id] = $mw;
+ }
+ return $wgMagicWords[$id];
+ }
+
+ # Initialises this object with an ID
+ function load( $id ) {
+ global $wgContLang;
+ $this->mId = $id;
+ $wgContLang->getMagic( $this );
+ }
+
+ /**
+ * Preliminary initialisation
+ * @private
+ */
+ function initRegex() {
+ #$variableClass = Title::legalChars();
+ # This was used for matching "$1" variables, but different uses of the feature will have
+ # different restrictions, which should be checked *after* the MagicWord has been matched,
+ # not here. - IMSoP
+
+ $escSyn = array();
+ foreach ( $this->mSynonyms as $synonym )
+ // In case a magic word contains /, like that's going to happen;)
+ $escSyn[] = preg_quote( $synonym, '/' );
+ $this->mBaseRegex = implode( '|', $escSyn );
+
+ $case = $this->mCaseSensitive ? '' : 'i';
+ $this->mRegex = "/{$this->mBaseRegex}/{$case}";
+ $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}";
+ $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex );
+ $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)",
+ "/^(?:{$this->mBaseRegex})$/{$case}" );
+ }
+
+ /**
+ * Gets a regex representing matching the word
+ */
+ function getRegex() {
+ if ($this->mRegex == '' ) {
+ $this->initRegex();
+ }
+ return $this->mRegex;
+ }
+
+ /**
+ * Gets the regexp case modifier to use, i.e. i or nothing, to be used if
+ * one is using MagicWord::getBaseRegex(), otherwise it'll be included in
+ * the complete expression
+ */
+ function getRegexCase() {
+ if ( $this->mRegex === '' )
+ $this->initRegex();
+
+ return $this->mCaseSensitive ? '' : 'i';
+ }
+
+ /**
+ * Gets a regex matching the word, if it is at the string start
+ */
+ function getRegexStart() {
+ if ($this->mRegex == '' ) {
+ $this->initRegex();
+ }
+ return $this->mRegexStart;
+ }
+
+ /**
+ * regex without the slashes and what not
+ */
+ function getBaseRegex() {
+ if ($this->mRegex == '') {
+ $this->initRegex();
+ }
+ return $this->mBaseRegex;
+ }
+
+ /**
+ * Returns true if the text contains the word
+ * @return bool
+ */
+ function match( $text ) {
+ return preg_match( $this->getRegex(), $text );
+ }
+
+ /**
+ * Returns true if the text starts with the word
+ * @return bool
+ */
+ function matchStart( $text ) {
+ return preg_match( $this->getRegexStart(), $text );
+ }
+
+ /**
+ * Returns NULL if there's no match, the value of $1 otherwise
+ * The return code is the matched string, if there's no variable
+ * part in the regex and the matched variable part ($1) if there
+ * is one.
+ */
+ function matchVariableStartToEnd( $text ) {
+ $matches = array();
+ $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches );
+ if ( $matchcount == 0 ) {
+ return NULL;
+ } elseif ( count($matches) == 1 ) {
+ return $matches[0];
+ } else {
+ # multiple matched parts (variable match); some will be empty because of
+ # synonyms. The variable will be the second non-empty one so remove any
+ # blank elements and re-sort the indices.
+ $matches = array_values(array_filter($matches));
+ return $matches[1];
+ }
+ }
+
+
+ /**
+ * Returns true if the text matches the word, and alters the
+ * input string, removing all instances of the word
+ */
+ function matchAndRemove( &$text ) {
+ global $wgMagicFound;
+ $wgMagicFound = false;
+ $text = preg_replace_callback( $this->getRegex(), 'pregRemoveAndRecord', $text );
+ return $wgMagicFound;
+ }
+
+ function matchStartAndRemove( &$text ) {
+ global $wgMagicFound;
+ $wgMagicFound = false;
+ $text = preg_replace_callback( $this->getRegexStart(), 'pregRemoveAndRecord', $text );
+ return $wgMagicFound;
+ }
+
+
+ /**
+ * Replaces the word with something else
+ */
+ function replace( $replacement, $subject, $limit=-1 ) {
+ $res = preg_replace( $this->getRegex(), wfRegexReplacement( $replacement ), $subject, $limit );
+ $this->mModified = !($res === $subject);
+ return $res;
+ }
+
+ /**
+ * Variable handling: {{SUBST:xxx}} style words
+ * Calls back a function to determine what to replace xxx with
+ * Input word must contain $1
+ */
+ function substituteCallback( $text, $callback ) {
+ $res = preg_replace_callback( $this->getVariableRegex(), $callback, $text );
+ $this->mModified = !($res === $text);
+ return $res;
+ }
+
+ /**
+ * Matches the word, where $1 is a wildcard
+ */
+ function getVariableRegex() {
+ if ( $this->mVariableRegex == '' ) {
+ $this->initRegex();
+ }
+ return $this->mVariableRegex;
+ }
+
+ /**
+ * Matches the entire string, where $1 is a wildcard
+ */
+ function getVariableStartToEndRegex() {
+ if ( $this->mVariableStartToEndRegex == '' ) {
+ $this->initRegex();
+ }
+ return $this->mVariableStartToEndRegex;
+ }
+
+ /**
+ * Accesses the synonym list directly
+ */
+ function getSynonym( $i ) {
+ return $this->mSynonyms[$i];
+ }
+
+ function getSynonyms() {
+ return $this->mSynonyms;
+ }
+
+ /**
+ * Returns true if the last call to replace() or substituteCallback()
+ * returned a modified text, otherwise false.
+ */
+ function getWasModified(){
+ return $this->mModified;
+ }
+
+ /**
+ * $magicarr is an associative array of (magic word ID => replacement)
+ * This method uses the php feature to do several replacements at the same time,
+ * thereby gaining some efficiency. The result is placed in the out variable
+ * $result. The return value is true if something was replaced.
+ * @static
+ **/
+ function replaceMultiple( $magicarr, $subject, &$result ){
+ $search = array();
+ $replace = array();
+ foreach( $magicarr as $id => $replacement ){
+ $mw = MagicWord::get( $id );
+ $search[] = $mw->getRegex();
+ $replace[] = $replacement;
+ }
+
+ $result = preg_replace( $search, $replace, $subject );
+ return !($result === $subject);
+ }
+
+ /**
+ * Adds all the synonyms of this MagicWord to an array, to allow quick
+ * lookup in a list of magic words
+ */
+ function addToArray( &$array, $value ) {
+ foreach ( $this->mSynonyms as $syn ) {
+ $array[$syn] = $value;
+ }
+ }
+
+ function isCaseSensitive() {
+ return $this->mCaseSensitive;
+ }
+}
+
+/**
+ * Used in matchAndRemove()
+ * @private
+ **/
+function pregRemoveAndRecord( $match ) {
+ global $wgMagicFound;
+ $wgMagicFound = true;
+ return '';
+}
+
+?>
diff --git a/includes/Math.php b/includes/Math.php
new file mode 100644
index 00000000..f9d6a605
--- /dev/null
+++ b/includes/Math.php
@@ -0,0 +1,269 @@
+<?php
+/**
+ * Contain everything related to <math> </math> parsing
+ * @package MediaWiki
+ */
+
+/**
+ * Takes LaTeX fragments, sends them to a helper program (texvc) for rendering
+ * to rasterized PNG and HTML and MathML approximations. An appropriate
+ * rendering form is picked and returned.
+ *
+ * by Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004)
+ *
+ * @package MediaWiki
+ */
+class MathRenderer {
+ var $mode = MW_MATH_MODERN;
+ var $tex = '';
+ var $inputhash = '';
+ var $hash = '';
+ var $html = '';
+ var $mathml = '';
+ var $conservativeness = 0;
+
+ function MathRenderer( $tex ) {
+ $this->tex = $tex;
+ }
+
+ function setOutputMode( $mode ) {
+ $this->mode = $mode;
+ }
+
+ function render() {
+ global $wgTmpDirectory, $wgInputEncoding;
+ global $wgTexvc;
+ $fname = 'MathRenderer::render';
+
+ if( $this->mode == MW_MATH_SOURCE ) {
+ # No need to render or parse anything more!
+ return ('$ '.htmlspecialchars( $this->tex ).' $');
+ }
+
+ if( !$this->_recall() ) {
+ # Ensure that the temp and output directories are available before continuing...
+ if( !file_exists( $wgTmpDirectory ) ) {
+ if( !@mkdir( $wgTmpDirectory ) ) {
+ return $this->_error( 'math_bad_tmpdir' );
+ }
+ } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) {
+ return $this->_error( 'math_bad_tmpdir' );
+ }
+
+ if( function_exists( 'is_executable' ) && !is_executable( $wgTexvc ) ) {
+ return $this->_error( 'math_notexvc' );
+ }
+ $cmd = $wgTexvc . ' ' .
+ escapeshellarg( $wgTmpDirectory ).' '.
+ escapeshellarg( $wgTmpDirectory ).' '.
+ escapeshellarg( $this->tex ).' '.
+ escapeshellarg( $wgInputEncoding );
+
+ if ( wfIsWindows() ) {
+ # Invoke it within cygwin sh, because texvc expects sh features in its default shell
+ $cmd = 'sh -c ' . wfEscapeShellArg( $cmd );
+ }
+
+ wfDebug( "TeX: $cmd\n" );
+ $contents = `$cmd`;
+ wfDebug( "TeX output:\n $contents\n---\n" );
+
+ if (strlen($contents) == 0) {
+ return $this->_error( 'math_unknown_error' );
+ }
+
+ $retval = substr ($contents, 0, 1);
+ $errmsg = '';
+ if (($retval == 'C') || ($retval == 'M') || ($retval == 'L')) {
+ if ($retval == 'C')
+ $this->conservativeness = 2;
+ else if ($retval == 'M')
+ $this->conservativeness = 1;
+ else
+ $this->conservativeness = 0;
+ $outdata = substr ($contents, 33);
+
+ $i = strpos($outdata, "\000");
+
+ $this->html = substr($outdata, 0, $i);
+ $this->mathml = substr($outdata, $i+1);
+ } else if (($retval == 'c') || ($retval == 'm') || ($retval == 'l')) {
+ $this->html = substr ($contents, 33);
+ if ($retval == 'c')
+ $this->conservativeness = 2;
+ else if ($retval == 'm')
+ $this->conservativeness = 1;
+ else
+ $this->conservativeness = 0;
+ $this->mathml = NULL;
+ } else if ($retval == 'X') {
+ $this->html = NULL;
+ $this->mathml = substr ($contents, 33);
+ $this->conservativeness = 0;
+ } else if ($retval == '+') {
+ $this->html = NULL;
+ $this->mathml = NULL;
+ $this->conservativeness = 0;
+ } else {
+ $errbit = htmlspecialchars( substr($contents, 1) );
+ switch( $retval ) {
+ case 'E': $errmsg = $this->_error( 'math_lexing_error', $errbit );
+ case 'S': $errmsg = $this->_error( 'math_syntax_error', $errbit );
+ case 'F': $errmsg = $this->_error( 'math_unknown_function', $errbit );
+ default: $errmsg = $this->_error( 'math_unknown_error', $errbit );
+ }
+ }
+
+ if ( !$errmsg ) {
+ $this->hash = substr ($contents, 1, 32);
+ }
+
+ $res = wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) );
+
+ if ( $errmsg ) {
+ return $errmsg;
+ }
+
+ if (!preg_match("/^[a-f0-9]{32}$/", $this->hash)) {
+ return $this->_error( 'math_unknown_error' );
+ }
+
+ if( !file_exists( "$wgTmpDirectory/{$this->hash}.png" ) ) {
+ return $this->_error( 'math_image_error' );
+ }
+
+ $hashpath = $this->_getHashPath();
+ if( !file_exists( $hashpath ) ) {
+ if( !@wfMkdirParents( $hashpath, 0755 ) ) {
+ return $this->_error( 'math_bad_output' );
+ }
+ } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) {
+ return $this->_error( 'math_bad_output' );
+ }
+
+ if( !rename( "$wgTmpDirectory/{$this->hash}.png", "$hashpath/{$this->hash}.png" ) ) {
+ return $this->_error( 'math_output_error' );
+ }
+
+ # Now save it back to the DB:
+ if ( !wfReadOnly() ) {
+ $outmd5_sql = pack('H32', $this->hash);
+
+ $md5_sql = pack('H32', $this->md5); # Binary packed, not hex
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->replace( 'math', array( 'math_inputhash' ),
+ array(
+ 'math_inputhash' => $md5_sql,
+ 'math_outputhash' => $outmd5_sql,
+ 'math_html_conservativeness' => $this->conservativeness,
+ 'math_html' => $this->html,
+ 'math_mathml' => $this->mathml,
+ ), $fname, array( 'IGNORE' )
+ );
+ }
+
+ }
+
+ return $this->_doRender();
+ }
+
+ function _error( $msg, $append = '' ) {
+ $mf = htmlspecialchars( wfMsg( 'math_failure' ) );
+ $errmsg = htmlspecialchars( wfMsg( $msg ) );
+ $source = htmlspecialchars( str_replace( "\n", ' ', $this->tex ) );
+ return "<strong class='error'>$mf ($errmsg$append): $source</strong>\n";
+ }
+
+ function _recall() {
+ global $wgMathDirectory;
+ $fname = 'MathRenderer::_recall';
+
+ $this->md5 = md5( $this->tex );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $rpage = $dbr->selectRow( 'math',
+ array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ),
+ array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex
+ $fname
+ );
+
+ if( $rpage !== false ) {
+ # Tailing 0x20s can get dropped by the database, add it back on if necessary:
+ $xhash = unpack( 'H32md5', $rpage->math_outputhash . " " );
+ $this->hash = $xhash ['md5'];
+
+ $this->conservativeness = $rpage->math_html_conservativeness;
+ $this->html = $rpage->math_html;
+ $this->mathml = $rpage->math_mathml;
+
+ if( file_exists( $this->_getHashPath() . "/{$this->hash}.png" ) ) {
+ return true;
+ }
+
+ if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) {
+ $hashpath = $this->_getHashPath();
+
+ if( !file_exists( $hashpath ) ) {
+ if( !@wfMkdirParents( $hashpath, 0755 ) ) {
+ return false;
+ }
+ } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) {
+ return false;
+ }
+ if ( function_exists( "link" ) ) {
+ return link ( $wgMathDirectory . "/{$this->hash}.png",
+ $hashpath . "/{$this->hash}.png" );
+ } else {
+ return rename ( $wgMathDirectory . "/{$this->hash}.png",
+ $hashpath . "/{$this->hash}.png" );
+ }
+ }
+
+ }
+
+ # Missing from the database and/or the render cache
+ return false;
+ }
+
+ /**
+ * Select among PNG, HTML, or MathML output depending on
+ */
+ function _doRender() {
+ if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) {
+ return "<math xmlns='http://www.w3.org/1998/Math/MathML'>{$this->mathml}</math>";
+ }
+ if (($this->mode == MW_MATH_PNG) || ($this->html == '') ||
+ (($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) ||
+ (($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) {
+ return $this->_linkToMathImage();
+ } else {
+ return '<span class="texhtml">'.$this->html.'</span>';
+ }
+ }
+
+ function _linkToMathImage() {
+ global $wgMathPath;
+ $url = htmlspecialchars( "$wgMathPath/" . substr($this->hash, 0, 1)
+ .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1)
+ . "/{$this->hash}.png" );
+ $alt = trim(str_replace("\n", ' ', htmlspecialchars( $this->tex )));
+ return "<img class='tex' src=\"$url\" alt=\"$alt\" />";
+ }
+
+ function _getHashPath() {
+ global $wgMathDirectory;
+ $path = $wgMathDirectory .'/'. substr($this->hash, 0, 1)
+ .'/'. substr($this->hash, 1, 1)
+ .'/'. substr($this->hash, 2, 1);
+ wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" );
+ return $path;
+ }
+
+ function renderMath( $tex ) {
+ global $wgUser;
+ $math = new MathRenderer( $tex );
+ $math->setOutputMode( $wgUser->getOption('math'));
+ return $math->render();
+ }
+}
+?>
diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php
new file mode 100644
index 00000000..af49109c
--- /dev/null
+++ b/includes/MemcachedSessions.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * This file gets included if $wgSessionsInMemcache is set in the config.
+ * It redirects session handling functions to store their data in memcached
+ * instead of the local filesystem. Depending on circumstances, it may also
+ * be necessary to change the cookie settings to work across hostnames.
+ * See: http://www.php.net/manual/en/function.session-set-save-handler.php
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * @todo document
+ */
+function memsess_key( $id ) {
+ global $wgDBname;
+ return "$wgDBname:session:$id";
+}
+
+/**
+ * @todo document
+ */
+function memsess_open( $save_path, $session_name ) {
+ # NOP, $wgMemc should be set up already
+ return true;
+}
+
+/**
+ * @todo document
+ */
+function memsess_close() {
+ # NOP
+ return true;
+}
+
+/**
+ * @todo document
+ */
+function memsess_read( $id ) {
+ global $wgMemc;
+ $data = $wgMemc->get( memsess_key( $id ) );
+ if( ! $data ) return '';
+ return $data;
+}
+
+/**
+ * @todo document
+ */
+function memsess_write( $id, $data ) {
+ global $wgMemc;
+ $wgMemc->set( memsess_key( $id ), $data, 3600 );
+ return true;
+}
+
+/**
+ * @todo document
+ */
+function memsess_destroy( $id ) {
+ global $wgMemc;
+ $wgMemc->delete( memsess_key( $id ) );
+ return true;
+}
+
+/**
+ * @todo document
+ */
+function memsess_gc( $maxlifetime ) {
+ # NOP: Memcached performs garbage collection.
+ return true;
+}
+
+session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' );
+
+?>
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
new file mode 100644
index 00000000..c8b7124c
--- /dev/null
+++ b/includes/MessageCache.php
@@ -0,0 +1,581 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+
+/**
+ *
+ */
+define( 'MSG_LOAD_TIMEOUT', 60);
+define( 'MSG_LOCK_TIMEOUT', 10);
+define( 'MSG_WAIT_TIMEOUT', 10);
+
+/**
+ * Message cache
+ * Performs various useful MediaWiki namespace-related functions
+ *
+ * @package MediaWiki
+ */
+class MessageCache {
+ var $mCache, $mUseCache, $mDisable, $mExpiry;
+ var $mMemcKey, $mKeys, $mParserOptions, $mParser;
+ var $mExtensionMessages = array();
+ var $mInitialised = false;
+ var $mDeferred = true;
+
+ function __construct( &$memCached, $useDB, $expiry, $memcPrefix) {
+ wfProfileIn( __METHOD__ );
+
+ $this->mUseCache = !is_null( $memCached );
+ $this->mMemc = &$memCached;
+ $this->mDisable = !$useDB;
+ $this->mExpiry = $expiry;
+ $this->mDisableTransform = false;
+ $this->mMemcKey = $memcPrefix.':messages';
+ $this->mKeys = false; # initialised on demand
+ $this->mInitialised = true;
+
+ wfProfileIn( __METHOD__.'-parseropt' );
+ $this->mParserOptions = new ParserOptions( $u=NULL );
+ wfProfileOut( __METHOD__.'-parseropt' );
+ $this->mParser = null;
+
+ # When we first get asked for a message,
+ # then we'll fill up the cache. If we
+ # can return a cache hit, this saves
+ # some extra milliseconds
+ $this->mDeferred = true;
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Try to load the cache from a local file
+ */
+ function loadFromLocal( $hash ) {
+ global $wgLocalMessageCache, $wgDBname;
+
+ $this->mCache = false;
+ if ( $wgLocalMessageCache === false ) {
+ return;
+ }
+
+ $filename = "$wgLocalMessageCache/messages-$wgDBname";
+
+ wfSuppressWarnings();
+ $file = fopen( $filename, 'r' );
+ wfRestoreWarnings();
+ if ( !$file ) {
+ return;
+ }
+
+ // Check to see if the file has the hash specified
+ $localHash = fread( $file, 32 );
+ if ( $hash == $localHash ) {
+ // All good, get the rest of it
+ $serialized = fread( $file, 1000000 );
+ $this->mCache = unserialize( $serialized );
+ }
+ fclose( $file );
+ }
+
+ /**
+ * Save the cache to a local file
+ */
+ function saveToLocal( $serialized, $hash ) {
+ global $wgLocalMessageCache, $wgDBname;
+
+ if ( $wgLocalMessageCache === false ) {
+ return;
+ }
+
+ $filename = "$wgLocalMessageCache/messages-$wgDBname";
+ $oldUmask = umask( 0 );
+ wfMkdirParents( $wgLocalMessageCache, 0777 );
+ umask( $oldUmask );
+
+ $file = fopen( $filename, 'w' );
+ if ( !$file ) {
+ wfDebug( "Unable to open local cache file for writing\n" );
+ return;
+ }
+
+ fwrite( $file, $hash . $serialized );
+ fclose( $file );
+ @chmod( $filename, 0666 );
+ }
+
+ function loadFromScript( $hash ) {
+ global $wgLocalMessageCache, $wgDBname;
+ if ( $wgLocalMessageCache === false ) {
+ return;
+ }
+
+ $filename = "$wgLocalMessageCache/messages-$wgDBname";
+
+ wfSuppressWarnings();
+ $file = fopen( $filename, 'r' );
+ wfRestoreWarnings();
+ if ( !$file ) {
+ return;
+ }
+ $localHash=substr(fread($file,40),8);
+ fclose($file);
+ if ($hash!=$localHash) {
+ return;
+ }
+ require("$wgLocalMessageCache/messages-$wgDBname");
+ }
+
+ function saveToScript($array, $hash) {
+ global $wgLocalMessageCache, $wgDBname;
+ if ( $wgLocalMessageCache === false ) {
+ return;
+ }
+
+ $filename = "$wgLocalMessageCache/messages-$wgDBname";
+ $oldUmask = umask( 0 );
+ wfMkdirParents( $wgLocalMessageCache, 0777 );
+ umask( $oldUmask );
+ $file = fopen( $filename.'.tmp', 'w');
+ fwrite($file,"<?php\n//$hash\n\n \$this->mCache = array(");
+
+ foreach ($array as $key => $message) {
+ fwrite($file, "'". $this->escapeForScript($key).
+ "' => '" . $this->escapeForScript($message).
+ "',\n");
+ }
+ fwrite($file,");\n?>");
+ fclose($file);
+ rename($filename.'.tmp',$filename);
+ }
+
+ function escapeForScript($string) {
+ $string = str_replace( '\\', '\\\\', $string );
+ $string = str_replace( '\'', '\\\'', $string );
+ return $string;
+ }
+
+ /**
+ * Loads messages either from memcached or the database, if not disabled
+ * On error, quietly switches to a fallback mode
+ * Returns false for a reportable error, true otherwise
+ */
+ function load() {
+ global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
+
+ if ( $this->mDisable ) {
+ static $shownDisabled = false;
+ if ( !$shownDisabled ) {
+ wfDebug( "MessageCache::load(): disabled\n" );
+ $shownDisabled = true;
+ }
+ return true;
+ }
+ $fname = 'MessageCache::load';
+ wfProfileIn( $fname );
+ $success = true;
+
+ if ( $this->mUseCache ) {
+ $this->mCache = false;
+
+ # Try local cache
+ wfProfileIn( $fname.'-fromlocal' );
+ $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" );
+ if ( $hash ) {
+ if ($wgLocalMessageCacheSerialized) {
+ $this->loadFromLocal( $hash );
+ } else {
+ $this->loadFromScript( $hash );
+ }
+ }
+ wfProfileOut( $fname.'-fromlocal' );
+
+ # Try memcached
+ if ( !$this->mCache ) {
+ wfProfileIn( $fname.'-fromcache' );
+ $this->mCache = $this->mMemc->get( $this->mMemcKey );
+
+ # Save to local cache
+ if ( $wgLocalMessageCache !== false ) {
+ $serialized = serialize( $this->mCache );
+ if ( !$hash ) {
+ $hash = md5( $serialized );
+ $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
+ }
+ if ($wgLocalMessageCacheSerialized) {
+ $this->saveToLocal( $serialized,$hash );
+ } else {
+ $this->saveToScript( $this->mCache, $hash );
+ }
+ }
+ wfProfileOut( $fname.'-fromcache' );
+ }
+
+
+ # If there's nothing in memcached, load all the messages from the database
+ if ( !$this->mCache ) {
+ wfDebug( "MessageCache::load(): loading all messages\n" );
+ $this->lock();
+ # Other threads don't need to load the messages if another thread is doing it.
+ $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT );
+ if ( $success ) {
+ wfProfileIn( $fname.'-load' );
+ $this->loadFromDB();
+ wfProfileOut( $fname.'-load' );
+
+ # Save in memcached
+ # Keep trying if it fails, this is kind of important
+ wfProfileIn( $fname.'-save' );
+ for ($i=0; $i<20 &&
+ !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
+ $i++ ) {
+ usleep(mt_rand(500000,1500000));
+ }
+
+ # Save to local cache
+ if ( $wgLocalMessageCache !== false ) {
+ $serialized = serialize( $this->mCache );
+ $hash = md5( $serialized );
+ $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
+ if ($wgLocalMessageCacheSerialized) {
+ $this->saveToLocal( $serialized,$hash );
+ } else {
+ $this->saveToScript( $this->mCache, $hash );
+ }
+ }
+
+ wfProfileOut( $fname.'-save' );
+ if ( $i == 20 ) {
+ $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 );
+ wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
+ }
+ }
+ $this->unlock();
+ }
+
+ if ( !is_array( $this->mCache ) ) {
+ wfDebug( "MessageCache::load(): individual message mode\n" );
+ # If it is 'loading' or 'error', switch to individual message mode, otherwise disable
+ # Causing too much DB load, disabling -- TS
+ $this->mDisable = true;
+ /*
+ if ( $this->mCache == "loading" ) {
+ $this->mUseCache = false;
+ } elseif ( $this->mCache == "error" ) {
+ $this->mUseCache = false;
+ $success = false;
+ } else {
+ $this->mDisable = true;
+ $success = false;
+ }*/
+ $this->mCache = false;
+ }
+ }
+ wfProfileOut( $fname );
+ $this->mDeferred = false;
+ return $success;
+ }
+
+ /**
+ * Loads all or main part of cacheable messages from the database
+ */
+ function loadFromDB() {
+ global $wgAllMessagesEn, $wgLang;
+
+ $fname = 'MessageCache::loadFromDB';
+ $dbr =& wfGetDB( DB_SLAVE );
+ if ( !$dbr ) {
+ throw new MWException( 'Invalid database object' );
+ }
+ $conditions = array( 'page_is_redirect' => 0,
+ 'page_namespace' => NS_MEDIAWIKI);
+ $res = $dbr->select( array( 'page', 'revision', 'text' ),
+ array( 'page_title', 'old_text', 'old_flags' ),
+ 'page_is_redirect=0 AND page_namespace='.NS_MEDIAWIKI.' AND page_latest=rev_id AND rev_text_id=old_id',
+ $fname
+ );
+
+ $this->mCache = array();
+ for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) {
+ $this->mCache[$row->page_title] = Revision::getRevisionText( $row );
+ }
+
+ # Negative caching
+ # Go through the language array and the extension array and make a note of
+ # any keys missing from the cache
+ foreach ( $wgAllMessagesEn as $key => $value ) {
+ $uckey = $wgLang->ucfirst( $key );
+ if ( !array_key_exists( $uckey, $this->mCache ) ) {
+ $this->mCache[$uckey] = false;
+ }
+ }
+
+ # Make sure all extension messages are available
+ wfLoadAllExtensions();
+
+ # Add them to the cache
+ foreach ( $this->mExtensionMessages as $key => $value ) {
+ $uckey = $wgLang->ucfirst( $key );
+ if ( !array_key_exists( $uckey, $this->mCache ) &&
+ ( isset( $this->mExtensionMessages[$key][$wgLang->getCode()] ) || isset( $this->mExtensionMessages[$key]['en'] ) ) ) {
+ $this->mCache[$uckey] = false;
+ }
+ }
+
+ $dbr->freeResult( $res );
+ }
+
+ /**
+ * Not really needed anymore
+ */
+ function getKeys() {
+ global $wgAllMessagesEn, $wgContLang;
+ if ( !$this->mKeys ) {
+ $this->mKeys = array();
+ foreach ( $wgAllMessagesEn as $key => $value ) {
+ $title = $wgContLang->ucfirst( $key );
+ array_push( $this->mKeys, $title );
+ }
+ }
+ return $this->mKeys;
+ }
+
+ /**
+ * @deprecated
+ */
+ function isCacheable( $key ) {
+ return true;
+ }
+
+ function replace( $title, $text ) {
+ global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc, $wgDBname;
+
+ $this->lock();
+ $this->load();
+ $parserMemc->delete("$wgDBname:sidebar");
+ if ( is_array( $this->mCache ) ) {
+ $this->mCache[$title] = $text;
+ $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
+
+ # Save to local cache
+ if ( $wgLocalMessageCache !== false ) {
+ $serialized = serialize( $this->mCache );
+ $hash = md5( $serialized );
+ $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
+ if ($wgLocalMessageCacheSerialized) {
+ $this->saveToLocal( $serialized,$hash );
+ } else {
+ $this->saveToScript( $this->mCache, $hash );
+ }
+ }
+
+
+ }
+ $this->unlock();
+ }
+
+ /**
+ * Returns success
+ * Represents a write lock on the messages key
+ */
+ function lock() {
+ if ( !$this->mUseCache ) {
+ return true;
+ }
+
+ $lockKey = $this->mMemcKey . 'lock';
+ for ($i=0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) {
+ sleep(1);
+ }
+
+ return $i >= MSG_WAIT_TIMEOUT;
+ }
+
+ function unlock() {
+ if ( !$this->mUseCache ) {
+ return;
+ }
+
+ $lockKey = $this->mMemcKey . 'lock';
+ $this->mMemc->delete( $lockKey );
+ }
+
+ function get( $key, $useDB, $forcontent=true, $isfullkey = false ) {
+ global $wgContLanguageCode;
+ if( $forcontent ) {
+ global $wgContLang;
+ $lang =& $wgContLang;
+ $langcode = $wgContLanguageCode;
+ } else {
+ global $wgLang, $wgLanguageCode;
+ $lang =& $wgLang;
+ $langcode = $wgLanguageCode;
+ }
+ # If uninitialised, someone is trying to call this halfway through Setup.php
+ if( !$this->mInitialised ) {
+ return '&lt;' . htmlspecialchars($key) . '&gt;';
+ }
+ # If cache initialization was deferred, start it now.
+ if( $this->mDeferred && !$this->mDisable && $useDB ) {
+ $this->load();
+ }
+
+ $message = false;
+ if( !$this->mDisable && $useDB ) {
+ $title = $lang->ucfirst( $key );
+ if(!$isfullkey && ($langcode != $wgContLanguageCode) ) {
+ $title .= '/' . $langcode;
+ }
+ $message = $this->getFromCache( $title );
+ }
+ # Try the extension array
+ if( $message === false && array_key_exists( $key, $this->mExtensionMessages ) ) {
+ if ( isset( $this->mExtensionMessages[$key][$langcode] ) ) {
+ $message = $this->mExtensionMessages[$key][$langcode];
+ } elseif ( isset( $this->mExtensionMessages[$key]['en'] ) ) {
+ $message = $this->mExtensionMessages[$key]['en'];
+ }
+ }
+
+ # Try the array in the language object
+ if( $message === false ) {
+ wfSuppressWarnings();
+ $message = $lang->getMessage( $key );
+ wfRestoreWarnings();
+ if ( is_null( $message ) ) {
+ $message = false;
+ }
+ }
+
+ # Try the English array
+ if( $message === false && $langcode != 'en' ) {
+ wfSuppressWarnings();
+ $message = Language::getMessage( $key );
+ wfRestoreWarnings();
+ if ( is_null( $message ) ) {
+ $message = false;
+ }
+ }
+
+ # Is this a custom message? Try the default language in the db...
+ if( ($message === false || $message === '-' ) &&
+ !$this->mDisable && $useDB &&
+ !$isfullkey && ($langcode != $wgContLanguageCode) ) {
+ $message = $this->getFromCache( $lang->ucfirst( $key ) );
+ }
+
+ # Final fallback
+ if( $message === false ) {
+ return '&lt;' . htmlspecialchars($key) . '&gt;';
+ }
+
+ # Replace brace tags
+ $message = $this->transform( $message );
+ return $message;
+ }
+
+ function getFromCache( $title ) {
+ $message = false;
+
+ # Try the cache
+ if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) {
+ return $this->mCache[$title];
+ }
+
+ # Try individual message cache
+ if ( $this->mUseCache ) {
+ $message = $this->mMemc->get( $this->mMemcKey . ':' . $title );
+ if ( $message == '###NONEXISTENT###' ) {
+ return false;
+ } elseif( !is_null( $message ) ) {
+ $this->mCache[$title] = $message;
+ return $message;
+ } else {
+ $message = false;
+ }
+ }
+
+ # Call message Hooks, in case they are defined
+ wfRunHooks('MessagesPreLoad',array($title,&$message));
+
+ # If it wasn't in the cache, load each message from the DB individually
+ $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
+ if( $revision ) {
+ $message = $revision->getText();
+ if ($this->mUseCache) {
+ $this->mCache[$title]=$message;
+ /* individual messages may be often
+ recached until proper purge code exists
+ */
+ $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 );
+ }
+ } else {
+ # Negative caching
+ # Use some special text instead of false, because false gets converted to '' somewhere
+ $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry );
+ }
+
+ return $message;
+ }
+
+ function transform( $message ) {
+ global $wgParser;
+ if ( !$this->mParser && isset( $wgParser ) ) {
+ # Do some initialisation so that we don't have to do it twice
+ $wgParser->firstCallInit();
+ # Clone it and store it
+ $this->mParser = clone $wgParser;
+ }
+ if ( !$this->mDisableTransform && $this->mParser ) {
+ if( strpos( $message, '{{' ) !== false ) {
+ $message = $this->mParser->transformMsg( $message, $this->mParserOptions );
+ }
+ }
+ return $message;
+ }
+
+ function disable() { $this->mDisable = true; }
+ function enable() { $this->mDisable = false; }
+ function disableTransform() { $this->mDisableTransform = true; }
+ function enableTransform() { $this->mDisableTransform = false; }
+ function setTransform( $x ) { $this->mDisableTransform = $x; }
+ function getTransform() { return $this->mDisableTransform; }
+
+ /**
+ * Add a message to the cache
+ *
+ * @param mixed $key
+ * @param mixed $value
+ * @param string $lang The messages language, English by default
+ */
+ function addMessage( $key, $value, $lang = 'en' ) {
+ $this->mExtensionMessages[$key][$lang] = $value;
+ }
+
+ /**
+ * Add an associative array of message to the cache
+ *
+ * @param array $messages An associative array of key => values to be added
+ * @param string $lang The messages language, English by default
+ */
+ function addMessages( $messages, $lang = 'en' ) {
+ wfProfileIn( __METHOD__ );
+ foreach ( $messages as $key => $value ) {
+ $this->addMessage( $key, $value, $lang );
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Clear all stored messages. Mainly used after a mass rebuild.
+ */
+ function clear() {
+ if( $this->mUseCache ) {
+ $this->mMemc->delete( $this->mMemcKey );
+ }
+ }
+}
+?>
diff --git a/includes/Metadata.php b/includes/Metadata.php
new file mode 100644
index 00000000..af40ab21
--- /dev/null
+++ b/includes/Metadata.php
@@ -0,0 +1,362 @@
+<?php
+/**
+ * Metadata.php -- provides DublinCore and CreativeCommons metadata
+ * 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 Prodromou <evan@wikitravel.org>
+ * @package MediaWiki
+ */
+
+/**
+ *
+ */
+define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1");
+
+function wfDublinCoreRdf($article) {
+
+ $url = dcReallyFullUrl($article->mTitle);
+
+ if (rdfSetup()) {
+ dcPrologue($url);
+ dcBasics($article);
+ dcEpilogue();
+ }
+}
+
+function wfCreativeCommonsRdf($article) {
+
+ if (rdfSetup()) {
+ global $wgRightsUrl;
+
+ $url = dcReallyFullUrl($article->mTitle);
+
+ ccPrologue();
+ ccSubPrologue('Work', $url);
+ dcBasics($article);
+ if (isset($wgRightsUrl)) {
+ $url = htmlspecialchars( $wgRightsUrl );
+ print " <cc:license rdf:resource=\"$url\" />\n";
+ }
+
+ ccSubEpilogue('Work');
+
+ if (isset($wgRightsUrl)) {
+ $terms = ccGetTerms($wgRightsUrl);
+ if ($terms) {
+ ccSubPrologue('License', $wgRightsUrl);
+ ccLicense($terms);
+ ccSubEpilogue('License');
+ }
+ }
+ }
+
+ ccEpilogue();
+}
+
+/**
+ * @private
+ */
+function rdfSetup() {
+ global $wgOut, $_SERVER;
+
+ $rdftype = wfNegotiateType(wfAcceptToPrefs($_SERVER['HTTP_ACCEPT']), wfAcceptToPrefs(RDF_TYPE_PREFS));
+
+ if (!$rdftype) {
+ wfHttpError(406, "Not Acceptable", wfMsg("notacceptable"));
+ return false;
+ } else {
+ $wgOut->disable();
+ header( "Content-type: {$rdftype}" );
+ $wgOut->sendCacheControl();
+ return true;
+ }
+}
+
+/**
+ * @private
+ */
+function dcPrologue($url) {
+ global $wgOutputEncoding;
+
+ $url = htmlspecialchars( $url );
+ print "<" . "?xml version=\"1.0\" encoding=\"{$wgOutputEncoding}\" ?" . ">
+
+ <!DOCTYPE rdf:RDF PUBLIC \"-//DUBLIN CORE//DCMES DTD 2002/07/31//EN\" \"http://dublincore.org/documents/2002/07/31/dcmes-xml/dcmes-xml-dtd.dtd\">
+
+ <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
+ xmlns:dc=\"http://purl.org/dc/elements/1.1/\">
+ <rdf:Description rdf:about=\"$url\">
+ ";
+}
+
+/**
+ * @private
+ */
+function dcEpilogue() {
+ print "
+ </rdf:Description>
+ </rdf:RDF>
+ ";
+}
+
+/**
+ * @private
+ */
+function dcBasics($article) {
+ global $wgContLanguageCode, $wgSitename;
+
+ dcElement('title', $article->mTitle->getText());
+ dcPageOrString('publisher', wfMsg('aboutpage'), $wgSitename);
+ dcElement('language', $wgContLanguageCode);
+ dcElement('type', 'Text');
+ dcElement('format', 'text/html');
+ dcElement('identifier', dcReallyFullUrl($article->mTitle));
+ dcElement('date', dcDate($article->getTimestamp()));
+
+ $last_editor = $article->getUser();
+
+ if ($last_editor == 0) {
+ dcPerson('creator', 0);
+ } else {
+ dcPerson('creator', $last_editor, $article->getUserText(),
+ User::whoIsReal($last_editor));
+ }
+
+ $contributors = $article->getContributors();
+
+ foreach ($contributors as $user_parts) {
+ dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]);
+ }
+
+ dcRights($article);
+}
+
+/**
+ * @private
+ */
+function ccPrologue() {
+ global $wgOutputEncoding;
+
+ echo "<" . "?xml version='1.0' encoding='{$wgOutputEncoding}' ?" . ">
+
+ <rdf:RDF xmlns:cc=\"http://web.resource.org/cc/\"
+ xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
+ xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">
+ ";
+}
+
+/**
+ * @private
+ */
+function ccSubPrologue($type, $url) {
+ $url = htmlspecialchars( $url );
+ echo " <cc:{$type} rdf:about=\"{$url}\">\n";
+}
+
+/**
+ * @private
+ */
+function ccSubEpilogue($type) {
+ echo " </cc:{$type}>\n";
+}
+
+/**
+ * @private
+ */
+function ccLicense($terms) {
+
+ foreach ($terms as $term) {
+ switch ($term) {
+ case 're':
+ ccTerm('permits', 'Reproduction'); break;
+ case 'di':
+ ccTerm('permits', 'Distribution'); break;
+ case 'de':
+ ccTerm('permits', 'DerivativeWorks'); break;
+ case 'nc':
+ ccTerm('prohibits', 'CommercialUse'); break;
+ case 'no':
+ ccTerm('requires', 'Notice'); break;
+ case 'by':
+ ccTerm('requires', 'Attribution'); break;
+ case 'sa':
+ ccTerm('requires', 'ShareAlike'); break;
+ case 'sc':
+ ccTerm('requires', 'SourceCode'); break;
+ }
+ }
+}
+
+/**
+ * @private
+ */
+function ccTerm($term, $name) {
+ print " <cc:{$term} rdf:resource=\"http://web.resource.org/cc/{$name}\" />\n";
+}
+
+/**
+ * @private
+ */
+function ccEpilogue() {
+ echo "</rdf:RDF>\n";
+}
+
+/**
+ * @private
+ */
+function dcElement($name, $value) {
+ $value = htmlspecialchars( $value );
+ print " <dc:{$name}>{$value}</dc:{$name}>\n";
+}
+
+/**
+ * @private
+ */
+function dcDate($timestamp) {
+ return substr($timestamp, 0, 4) . '-'
+ . substr($timestamp, 4, 2) . '-'
+ . substr($timestamp, 6, 2);
+}
+
+/**
+ * @private
+ */
+function dcReallyFullUrl($title) {
+ return $title->getFullURL();
+}
+
+/**
+ * @private
+ */
+function dcPageOrString($name, $page, $str) {
+ $nt = Title::newFromText($page);
+
+ if (!$nt || $nt->getArticleID() == 0) {
+ dcElement($name, $str);
+ } else {
+ dcPage($name, $nt);
+ }
+}
+
+/**
+ * @private
+ */
+function dcPage($name, $title) {
+ dcUrl($name, dcReallyFullUrl($title));
+}
+
+/**
+ * @private
+ */
+function dcUrl($name, $url) {
+ $url = htmlspecialchars( $url );
+ print " <dc:{$name} rdf:resource=\"{$url}\" />\n";
+}
+
+/**
+ * @private
+ */
+function dcPerson($name, $id, $user_name='', $user_real_name='') {
+ global $wgContLang;
+
+ if ($id == 0) {
+ dcElement($name, wfMsg('anonymous'));
+ } else if ( !empty($user_real_name) ) {
+ dcElement($name, $user_real_name);
+ } else {
+ # XXX: This shouldn't happen.
+ if( empty( $user_name ) ) {
+ $user_name = User::whoIs($id);
+ }
+ dcPageOrString($name, $wgContLang->getNsText(NS_USER) . ':' . $user_name, wfMsg('siteuser', $user_name));
+ }
+}
+
+/**
+ * Takes an arg, for future enhancement with different rights for
+ * different pages.
+ * @private
+ */
+function dcRights($article) {
+
+ global $wgRightsPage, $wgRightsUrl, $wgRightsText;
+
+ if (isset($wgRightsPage) &&
+ ($nt = Title::newFromText($wgRightsPage))
+ && ($nt->getArticleID() != 0)) {
+ dcPage('rights', $nt);
+ } else if (isset($wgRightsUrl)) {
+ dcUrl('rights', $wgRightsUrl);
+ } else if (isset($wgRightsText)) {
+ dcElement('rights', $wgRightsText);
+ }
+}
+
+/**
+ * @private
+ */
+function ccGetTerms($url) {
+ global $wgLicenseTerms;
+
+ if (isset($wgLicenseTerms)) {
+ return $wgLicenseTerms;
+ } else {
+ $known = getKnownLicenses();
+ return $known[$url];
+ }
+}
+
+/**
+ * @private
+ */
+function getKnownLicenses() {
+
+ $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc',
+ 'by-nc-sa', 'by-sa');
+ $ccVersions = array('1.0', '2.0');
+ $knownLicenses = array();
+
+ foreach ($ccVersions as $version) {
+ foreach ($ccLicenses as $license) {
+ if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) {
+ # 2.0 dropped the non-attribs licenses
+ continue;
+ }
+ $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/";
+ $knownLicenses[$lurl] = explode('-', $license);
+ $knownLicenses[$lurl][] = 're';
+ $knownLicenses[$lurl][] = 'di';
+ $knownLicenses[$lurl][] = 'no';
+ if (!in_array('nd', $knownLicenses[$lurl])) {
+ $knownLicenses[$lurl][] = 'de';
+ }
+ }
+ }
+
+ /* Handle the GPL and LGPL, too. */
+
+ $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
+ $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
+ $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
+
+ return $knownLicenses;
+}
+
+?>
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
new file mode 100644
index 00000000..30861ba3
--- /dev/null
+++ b/includes/MimeMagic.php
@@ -0,0 +1,695 @@
+<?php
+/** Module defining helper functions for detecting and dealing with mime types.
+ *
+ * @package MediaWiki
+ */
+
+ /** Defines a set of well known mime types
+ * This is used as a fallback to mime.types files.
+ * An extensive list of well known mime types is provided by
+ * the file mime.types in the includes directory.
+ */
+define('MM_WELL_KNOWN_MIME_TYPES',<<<END_STRING
+application/ogg ogg ogm
+application/pdf pdf
+application/x-javascript js
+application/x-shockwave-flash swf
+audio/midi mid midi kar
+audio/mpeg mpga mpa mp2 mp3
+audio/x-aiff aif aiff aifc
+audio/x-wav wav
+audio/ogg ogg
+image/x-bmp bmp
+image/gif gif
+image/jpeg jpeg jpg jpe
+image/png png
+image/svg+xml svg
+image/tiff tiff tif
+image/vnd.djvu djvu
+text/plain txt
+text/html html htm
+video/ogg ogm ogg
+video/mpeg mpg mpeg
+END_STRING
+);
+
+ /** Defines a set of well known mime info entries
+ * This is used as a fallback to mime.info files.
+ * An extensive list of well known mime types is provided by
+ * the file mime.info in the includes directory.
+ */
+define('MM_WELL_KNOWN_MIME_INFO', <<<END_STRING
+application/pdf [OFFICE]
+text/javascript application/x-javascript [EXECUTABLE]
+application/x-shockwave-flash [MULTIMEDIA]
+audio/midi [AUDIO]
+audio/x-aiff [AUDIO]
+audio/x-wav [AUDIO]
+audio/mp3 audio/mpeg [AUDIO]
+application/ogg audio/ogg video/ogg [MULTIMEDIA]
+image/x-bmp image/bmp [BITMAP]
+image/gif [BITMAP]
+image/jpeg [BITMAP]
+image/png [BITMAP]
+image/svg image/svg+xml [DRAWING]
+image/tiff [BITMAP]
+image/vnd.djvu [BITMAP]
+text/plain [TEXT]
+text/html [TEXT]
+video/ogg [VIDEO]
+video/mpeg [VIDEO]
+unknown/unknown application/octet-stream application/x-empty [UNKNOWN]
+END_STRING
+);
+
+#note: because this file is possibly included by a function,
+#we need to access the global scope explicitely!
+global $wgLoadFileinfoExtension;
+
+if ($wgLoadFileinfoExtension) {
+ if(!extension_loaded('fileinfo')) dl('fileinfo.' . PHP_SHLIB_SUFFIX);
+}
+
+/** Implements functions related to mime types such as detection and mapping to
+* file extension,
+*
+* Instances of this class are stateles, there only needs to be one global instance
+* of MimeMagic. Please use wfGetMimeMagic to get that instance.
+* @package MediaWiki
+*/
+class MimeMagic {
+
+ /**
+ * Mapping of media types to arrays of mime types.
+ * This is used by findMediaType and getMediaType, respectively
+ */
+ var $mMediaTypes= NULL;
+
+ /** Map of mime type aliases
+ */
+ var $mMimeTypeAliases= NULL;
+
+ /** map of mime types to file extensions (as a space seprarated list)
+ */
+ var $mMimeToExt= NULL;
+
+ /** map of file extensions types to mime types (as a space seprarated list)
+ */
+ var $mExtToMime= NULL;
+
+ /** Initializes the MimeMagic object. This is called by wfGetMimeMagic when instantiation
+ * the global MimeMagic singleton object.
+ *
+ * This constructor parses the mime.types and mime.info files and build internal mappings.
+ */
+ function MimeMagic() {
+ /*
+ * --- load mime.types ---
+ */
+
+ global $wgMimeTypeFile;
+
+ $types= MM_WELL_KNOWN_MIME_TYPES;
+
+ if ($wgMimeTypeFile) {
+ if (is_file($wgMimeTypeFile) and is_readable($wgMimeTypeFile)) {
+ wfDebug("MimeMagic::MimeMagic: loading mime types from $wgMimeTypeFile\n");
+
+ $types.= "\n";
+ $types.= file_get_contents($wgMimeTypeFile);
+ }
+ else wfDebug("MimeMagic::MimeMagic: can't load mime types from $wgMimeTypeFile\n");
+ }
+ else wfDebug("MimeMagic::MimeMagic: no mime types file defined, using build-ins only.\n");
+
+ $types= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$types);
+ $types= str_replace("\t"," ",$types);
+
+ $this->mMimeToExt= array();
+ $this->mToMime= array();
+
+ $lines= explode("\n",$types);
+ foreach ($lines as $s) {
+ $s= trim($s);
+ if (empty($s)) continue;
+ if (strpos($s,'#')===0) continue;
+
+ $s= strtolower($s);
+ $i= strpos($s,' ');
+
+ if ($i===false) continue;
+
+ #print "processing MIME line $s<br>";
+
+ $mime= substr($s,0,$i);
+ $ext= trim(substr($s,$i+1));
+
+ if (empty($ext)) continue;
+
+ if (@$this->mMimeToExt[$mime]) $this->mMimeToExt[$mime] .= ' '.$ext;
+ else $this->mMimeToExt[$mime]= $ext;
+
+ $extensions= explode(' ',$ext);
+
+ foreach ($extensions as $e) {
+ $e= trim($e);
+ if (empty($e)) continue;
+
+ if (@$this->mExtToMime[$e]) $this->mExtToMime[$e] .= ' '.$mime;
+ else $this->mExtToMime[$e]= $mime;
+ }
+ }
+
+ /*
+ * --- load mime.info ---
+ */
+
+ global $wgMimeInfoFile;
+
+ $info= MM_WELL_KNOWN_MIME_INFO;
+
+ if ($wgMimeInfoFile) {
+ if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) {
+ wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n");
+
+ $info.= "\n";
+ $info.= file_get_contents($wgMimeInfoFile);
+ }
+ else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n");
+ }
+ else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n");
+
+ $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info);
+ $info= str_replace("\t"," ",$info);
+
+ $this->mMimeTypeAliases= array();
+ $this->mMediaTypes= array();
+
+ $lines= explode("\n",$info);
+ foreach ($lines as $s) {
+ $s= trim($s);
+ if (empty($s)) continue;
+ if (strpos($s,'#')===0) continue;
+
+ $s= strtolower($s);
+ $i= strpos($s,' ');
+
+ if ($i===false) continue;
+
+ #print "processing MIME INFO line $s<br>";
+
+ $match= array();
+ if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) {
+ $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s);
+ $mtype= trim(strtoupper($match[1]));
+ }
+ else $mtype= MEDIATYPE_UNKNOWN;
+
+ $m= explode(' ',$s);
+
+ if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array();
+
+ foreach ($m as $mime) {
+ $mime= trim($mime);
+ if (empty($mime)) continue;
+
+ $this->mMediaTypes[$mtype][]= $mime;
+ }
+
+ if (sizeof($m)>1) {
+ $main= $m[0];
+ for ($i=1; $i<sizeof($m); $i+= 1) {
+ $mime= $m[$i];
+ $this->mMimeTypeAliases[$mime]= $main;
+ }
+ }
+ }
+
+ }
+
+ /** returns a list of file extensions for a given mime type
+ * as a space separated string.
+ */
+ function getExtensionsForType($mime) {
+ $mime= strtolower($mime);
+
+ $r= @$this->mMimeToExt[$mime];
+
+ if (@!$r and isset($this->mMimeTypeAliases[$mime])) {
+ $mime= $this->mMimeTypeAliases[$mime];
+ $r= @$this->mMimeToExt[$mime];
+ }
+
+ return $r;
+ }
+
+ /** returns a list of mime types for a given file extension
+ * as a space separated string.
+ */
+ function getTypesForExtension($ext) {
+ $ext= strtolower($ext);
+
+ $r= @$this->mExtToMime[$ext];
+ return $r;
+ }
+
+ /** returns a single mime type for a given file extension.
+ * This is always the first type from the list returned by getTypesForExtension($ext).
+ */
+ function guessTypesForExtension($ext) {
+ $m= $this->getTypesForExtension( $ext );
+ if( is_null($m) ) return NULL;
+
+ $m= trim( $m );
+ $m= preg_replace('/\s.*$/','',$m);
+
+ return $m;
+ }
+
+
+ /** tests if the extension matches the given mime type.
+ * returns true if a match was found, NULL if the mime type is unknown,
+ * and false if the mime type is known but no matches where found.
+ */
+ function isMatchingExtension($extension,$mime) {
+ $ext= $this->getExtensionsForType($mime);
+
+ if (!$ext) {
+ return NULL; //unknown
+ }
+
+ $ext= explode(' ',$ext);
+
+ $extension= strtolower($extension);
+ if (in_array($extension,$ext)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /** returns true if the mime type is known to represent
+ * an image format supported by the PHP GD library.
+ */
+ function isPHPImageType( $mime ) {
+ #as defined by imagegetsize and image_type_to_mime
+ static $types = array(
+ 'image/gif', 'image/jpeg', 'image/png',
+ 'image/x-bmp', 'image/xbm', 'image/tiff',
+ 'image/jp2', 'image/jpeg2000', 'image/iff',
+ 'image/xbm', 'image/x-xbitmap',
+ 'image/vnd.wap.wbmp', 'image/vnd.xiff',
+ 'image/x-photoshop',
+ 'application/x-shockwave-flash',
+ );
+
+ return in_array( $mime, $types );
+ }
+
+ /**
+ * Returns true if the extension represents a type which can
+ * be reliably detected from its content. Use this to determine
+ * whether strict content checks should be applied to reject
+ * invalid uploads; if we can't identify the type we won't
+ * be able to say if it's invalid.
+ *
+ * @todo Be more accurate when using fancy mime detector plugins;
+ * right now this is the bare minimum getimagesize() list.
+ * @return bool
+ */
+ function isRecognizableExtension( $extension ) {
+ static $types = array(
+ 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd',
+ 'bmp', 'tiff', 'tif', 'jpc', 'jp2',
+ 'jpx', 'jb2', 'swc', 'iff', 'wbmp',
+ 'xbm', 'djvu'
+ );
+ return in_array( strtolower( $extension ), $types );
+ }
+
+
+ /** mime type detection. This uses detectMimeType to detect the mim type of the file,
+ * but applies additional checks to determine some well known file formats that may be missed
+ * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG).
+ *
+ * @param string $file The file to check
+ * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default.
+ *
+ * @return string the mime type of $file
+ */
+ function guessMimeType( $file, $useExt=true ) {
+ $fname = 'MimeMagic::guessMimeType';
+ $mime= $this->detectMimeType($file,$useExt);
+
+ // Read a chunk of the file
+ $f = fopen( $file, "rt" );
+ if( !$f ) return "unknown/unknown";
+ $head = fread( $f, 1024 );
+ fclose( $f );
+
+ $sub4 = substr( $head, 0, 4 );
+ if ( $sub4 == "\x01\x00\x09\x00" || $sub4 == "\xd7\xcd\xc6\x9a" ) {
+ // WMF kill kill kill
+ // Note that WMF may have a bare header, no magic number.
+ // The former of the above two checks is theoretically prone to false positives
+ $mime = "application/x-msmetafile";
+ }
+
+ if (strpos($mime,"text/")===0 || $mime==="application/xml") {
+
+ $xml_type= NULL;
+ $script_type= NULL;
+
+ /*
+ * look for XML formats (XHTML and SVG)
+ */
+ if ($mime==="text/sgml" ||
+ $mime==="text/plain" ||
+ $mime==="text/html" ||
+ $mime==="text/xml" ||
+ $mime==="application/xml") {
+
+ if (substr($head,0,5)=="<?xml") $xml_type= "ASCII";
+ elseif (substr($head,0,8)=="\xef\xbb\xbf<?xml") $xml_type= "UTF-8";
+ elseif (substr($head,0,10)=="\xfe\xff\x00<\x00?\x00x\x00m\x00l") $xml_type= "UTF-16BE";
+ elseif (substr($head,0,10)=="\xff\xfe<\x00?\x00x\x00m\x00l\x00") $xml_type= "UTF-16LE";
+
+ if ($xml_type) {
+ if ($xml_type!=="UTF-8" && $xml_type!=="ASCII") $head= iconv($xml_type,"ASCII//IGNORE",$head);
+
+ $match= array();
+ $doctype= "";
+ $tag= "";
+
+ if (preg_match('%<!DOCTYPE\s+[\w-]+\s+PUBLIC\s+["'."'".'"](.*?)["'."'".'"].*>%sim',$head,$match)) $doctype= $match[1];
+ if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1];
+
+ #print "<br>ANALYSING $file ($mime): doctype= $doctype; tag= $tag<br>";
+
+ if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg";
+ elseif ($tag==="svg") $mime= "image/svg";
+ elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html";
+ elseif ($tag==="html") $mime= "text/html";
+
+ $test_more= false;
+ }
+ }
+
+ /*
+ * look for shell scripts
+ */
+ if (!$xml_type) {
+ $script_type= NULL;
+
+ #detect by shebang
+ if (substr($head,0,2)=="#!") $script_type= "ASCII";
+ elseif (substr($head,0,5)=="\xef\xbb\xbf#!") $script_type= "UTF-8";
+ elseif (substr($head,0,7)=="\xfe\xff\x00#\x00!") $script_type= "UTF-16BE";
+ elseif (substr($head,0,7)=="\xff\xfe#\x00!") $script_type= "UTF-16LE";
+
+ if ($script_type) {
+ if ($script_type!=="UTF-8" && $script_type!=="ASCII") $head= iconv($script_type,"ASCII//IGNORE",$head);
+
+ $match= array();
+ $prog= "";
+
+ if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) $script= $match[2];
+
+ $mime= "application/x-$prog";
+ }
+ }
+
+ /*
+ * look for PHP
+ */
+ if( !$xml_type && !$script_type ) {
+
+ if( ( strpos( $head, '<?php' ) !== false ) ||
+ ( strpos( $head, '<? ' ) !== false ) ||
+ ( strpos( $head, "<?\n" ) !== false ) ||
+ ( strpos( $head, "<?\t" ) !== false ) ||
+ ( strpos( $head, "<?=" ) !== false ) ||
+
+ ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00 " ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00\n" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
+
+ $mime= "application/x-php";
+ }
+ }
+
+ }
+
+ if (isset($this->mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime];
+
+ wfDebug("$fname: final mime type of $file: $mime\n");
+ return $mime;
+ }
+
+ /** Internal mime type detection, please use guessMimeType() for application code instead.
+ * Detection is done using an external program, if $wgMimeDetectorCommand is set.
+ * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available.
+ * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension.
+ * If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image.
+ * If no mime type can be determined, this function returns "unknown/unknown".
+ *
+ * @param string $file The file to check
+ * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default.
+ *
+ * @return string the mime type of $file
+ * @access private
+ */
+ function detectMimeType( $file, $useExt=true ) {
+ $fname = 'MimeMagic::detectMimeType';
+
+ global $wgMimeDetectorCommand;
+
+ $m= NULL;
+ if ($wgMimeDetectorCommand) {
+ $fn= wfEscapeShellArg($file);
+ $m= `$wgMimeDetectorCommand $fn`;
+ }
+ else if (function_exists("finfo_open") && function_exists("finfo_file")) {
+
+ # This required the fileinfo extension by PECL,
+ # see http://pecl.php.net/package/fileinfo
+ # This must be compiled into PHP
+ #
+ # finfo is the official replacement for the deprecated
+ # mime_content_type function, see below.
+ #
+ # If you may need to load the fileinfo extension at runtime, set
+ # $wgLoadFileinfoExtension in LocalSettings.php
+
+ $mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */
+
+ if ($mime_magic_resource) {
+ $m= finfo_file($mime_magic_resource, $file);
+
+ finfo_close($mime_magic_resource);
+ }
+ else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n");
+ }
+ else if (function_exists("mime_content_type")) {
+
+ # NOTE: this function is available since PHP 4.3.0, but only if
+ # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic.
+ #
+ # On Winodws, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP;
+ # sometimes, this may even be needed under linus/unix.
+ #
+ # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above.
+ # see http://www.php.net/manual/en/ref.mime-magic.php for details.
+
+ $m= mime_content_type($file);
+ }
+ else wfDebug("$fname: no magic mime detector found!\n");
+
+ if ($m) {
+ #normalize
+ $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc
+ $m= trim($m);
+ $m= strtolower($m);
+
+ if (strpos($m,'unknown')!==false) $m= NULL;
+ else {
+ wfDebug("$fname: magic mime type of $file: $m\n");
+ return $m;
+ }
+ }
+
+ #if still not known, use getimagesize to find out the type of image
+ #TODO: skip things that do not have a well-known image extension? Would that be safe?
+ wfSuppressWarnings();
+ $gis = getimagesize( $file );
+ wfRestoreWarnings();
+
+ $notAnImage= false;
+
+ if ($gis && is_array($gis) && $gis[2]) {
+ switch ($gis[2]) {
+ case IMAGETYPE_GIF: $m= "image/gif"; break;
+ case IMAGETYPE_JPEG: $m= "image/jpeg"; break;
+ case IMAGETYPE_PNG: $m= "image/png"; break;
+ case IMAGETYPE_SWF: $m= "application/x-shockwave-flash"; break;
+ case IMAGETYPE_PSD: $m= "application/photoshop"; break;
+ case IMAGETYPE_BMP: $m= "image/bmp"; break;
+ case IMAGETYPE_TIFF_II: $m= "image/tiff"; break;
+ case IMAGETYPE_TIFF_MM: $m= "image/tiff"; break;
+ case IMAGETYPE_JPC: $m= "image"; break;
+ case IMAGETYPE_JP2: $m= "image/jpeg2000"; break;
+ case IMAGETYPE_JPX: $m= "image/jpeg2000"; break;
+ case IMAGETYPE_JB2: $m= "image"; break;
+ case IMAGETYPE_SWC: $m= "application/x-shockwave-flash"; break;
+ case IMAGETYPE_IFF: $m= "image/vnd.xiff"; break;
+ case IMAGETYPE_WBMP: $m= "image/vnd.wap.wbmp"; break;
+ case IMAGETYPE_XBM: $m= "image/x-xbitmap"; break;
+ }
+
+ if ($m) {
+ wfDebug("$fname: image mime type of $file: $m\n");
+ return $m;
+ }
+ else $notAnImage= true;
+ } else {
+ // Also test DjVu
+ $deja = new DjVuImage( $file );
+ if( $deja->isValid() ) {
+ wfDebug("$fname: detected $file as image/vnd.djvu\n");
+ return 'image/vnd.djvu';
+ }
+ }
+
+ #if desired, look at extension as a fallback.
+ if ($useExt) {
+ $i = strrpos( $file, '.' );
+ $e= strtolower( $i ? substr( $file, $i + 1 ) : '' );
+
+ $m= $this->guessTypesForExtension($e);
+
+ #TODO: if $notAnImage is set, do not trust the file extension if
+ # the results is one of the image types that should have been recognized
+ # by getimagesize
+
+ if ($m) {
+ wfDebug("$fname: extension mime type of $file: $m\n");
+ return $m;
+ }
+ }
+
+ #unknown type
+ wfDebug("$fname: failed to guess mime type for $file!\n");
+ return "unknown/unknown";
+ }
+
+ /**
+ * Determine the media type code for a file, using its mime type, name and possibly
+ * its contents.
+ *
+ * This function relies on the findMediaType(), mapping extensions and mime
+ * types to media types.
+ *
+ * @todo analyse file if need be
+ * @todo look at multiple extension, separately and together.
+ *
+ * @param string $path full path to the image file, in case we have to look at the contents
+ * (if null, only the mime type is used to determine the media type code).
+ * @param string $mime mime type. If null it will be guessed using guessMimeType.
+ *
+ * @return (int?string?) a value to be used with the MEDIATYPE_xxx constants.
+ */
+ function getMediaType($path=NULL,$mime=NULL) {
+ if( !$mime && !$path ) return MEDIATYPE_UNKNOWN;
+
+ #if mime type is unknown, guess it
+ if( !$mime ) $mime= $this->guessMimeType($path,false);
+
+ #special code for ogg - detect if it's video (theora),
+ #else label it as sound.
+ if( $mime=="application/ogg" && file_exists($path) ) {
+
+ // Read a chunk of the file
+ $f = fopen( $path, "rt" );
+ if( !$f ) return MEDIATYPE_UNKNOWN;
+ $head = fread( $f, 256 );
+ fclose( $f );
+
+ $head= strtolower( $head );
+
+ #This is an UGLY HACK, file should be parsed correctly
+ if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO;
+ elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO;
+ elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO;
+ elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO;
+ else return MEDIATYPE_MULTIMEDIA;
+ }
+
+ #check for entry for full mime type
+ if( $mime ) {
+ $type= $this->findMediaType($mime);
+ if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ }
+
+ #check for entry for file extension
+ $e= NULL;
+ if( $path ) {
+ $i = strrpos( $path, '.' );
+ $e= strtolower( $i ? substr( $path, $i + 1 ) : '' );
+
+ #TODO: look at multi-extension if this fails, parse from full path
+
+ $type= $this->findMediaType('.'.$e);
+ if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ }
+
+ #check major mime type
+ if( $mime ) {
+ $i= strpos($mime,'/');
+ if( $i !== false ) {
+ $major= substr($mime,0,$i);
+ $type= $this->findMediaType($major);
+ if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ }
+ }
+
+ if( !$type ) $type= MEDIATYPE_UNKNOWN;
+
+ return $type;
+ }
+
+ /** returns a media code matching the given mime type or file extension.
+ * File extensions are represented by a string starting with a dot (.) to
+ * distinguish them from mime types.
+ *
+ * This funktion relies on the mapping defined by $this->mMediaTypes
+ * @access private
+ */
+ function findMediaType($extMime) {
+
+ if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types
+ $m= $this->getTypesForExtension(substr($extMime,1));
+ if (!$m) return MEDIATYPE_UNKNOWN;
+
+ $m= explode(' ',$m);
+ }
+ else { #normalize mime type
+ if (isset($this->mMimeTypeAliases[$extMime])) {
+ $extMime= $this->mMimeTypeAliases[$extMime];
+ }
+
+ $m= array($extMime);
+ }
+
+ foreach ($m as $mime) {
+ foreach ($this->mMediaTypes as $type => $codes) {
+ if (in_array($mime,$codes,true)) return $type;
+ }
+ }
+
+ return MEDIATYPE_UNKNOWN;
+ }
+}
+
+?>
diff --git a/includes/Namespace.php b/includes/Namespace.php
new file mode 100644
index 00000000..ab7511d0
--- /dev/null
+++ b/includes/Namespace.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Provide things related to namespaces
+ * @package MediaWiki
+ */
+
+/**
+ * Definitions of the NS_ constants are in Defines.php
+ * @private
+ */
+$wgCanonicalNamespaceNames = array(
+ NS_MEDIA => 'Media',
+ NS_SPECIAL => 'Special',
+ NS_TALK => 'Talk',
+ NS_USER => 'User',
+ NS_USER_TALK => 'User_talk',
+ NS_PROJECT => 'Project',
+ NS_PROJECT_TALK => 'Project_talk',
+ NS_IMAGE => 'Image',
+ NS_IMAGE_TALK => 'Image_talk',
+ NS_MEDIAWIKI => 'MediaWiki',
+ NS_MEDIAWIKI_TALK => 'MediaWiki_talk',
+ NS_TEMPLATE => 'Template',
+ NS_TEMPLATE_TALK => 'Template_talk',
+ NS_HELP => 'Help',
+ NS_HELP_TALK => 'Help_talk',
+ NS_CATEGORY => 'Category',
+ NS_CATEGORY_TALK => 'Category_talk',
+);
+
+if( is_array( $wgExtraNamespaces ) ) {
+ $wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces;
+}
+
+/**
+ * This is a utility class with only static functions
+ * for dealing with namespaces that encodes all the
+ * "magic" behaviors of them based on index. The textual
+ * names of the namespaces are handled by Language.php.
+ *
+ * These are synonyms for the names given in the language file
+ * Users and translators should not change them
+ *
+ * @package MediaWiki
+ */
+class Namespace {
+
+ /**
+ * Check if the given namespace might be moved
+ * @return bool
+ */
+ function isMovable( $index ) {
+ return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY );
+ }
+
+ /**
+ * Check if the given namespace is not a talk page
+ * @return bool
+ */
+ function isMain( $index ) {
+ return ! Namespace::isTalk( $index );
+ }
+
+ /**
+ * Check if the give namespace is a talk page
+ * @return bool
+ */
+ function isTalk( $index ) {
+ return ($index > NS_MAIN) // Special namespaces are negative
+ && ($index % 2); // Talk namespaces are odd-numbered
+ }
+
+ /**
+ * Get the talk namespace corresponding to the given index
+ */
+ function getTalk( $index ) {
+ if ( Namespace::isTalk( $index ) ) {
+ return $index;
+ } else {
+ # FIXME
+ return $index + 1;
+ }
+ }
+
+ function getSubject( $index ) {
+ if ( Namespace::isTalk( $index ) ) {
+ return $index - 1;
+ } else {
+ return $index;
+ }
+ }
+
+ /**
+ * Returns the canonical (English Wikipedia) name for a given index
+ */
+ function getCanonicalName( $index ) {
+ global $wgCanonicalNamespaceNames;
+ return $wgCanonicalNamespaceNames[$index];
+ }
+
+ /**
+ * Returns the index for a given canonical name, or NULL
+ * The input *must* be converted to lower case first
+ */
+ function getCanonicalIndex( $name ) {
+ global $wgCanonicalNamespaceNames;
+ static $xNamespaces = false;
+ if ( $xNamespaces === false ) {
+ $xNamespaces = array();
+ foreach ( $wgCanonicalNamespaceNames as $i => $text ) {
+ $xNamespaces[strtolower($text)] = $i;
+ }
+ }
+ if ( array_key_exists( $name, $xNamespaces ) ) {
+ return $xNamespaces[$name];
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Can this namespace ever have a talk namespace?
+ * @param $index Namespace index
+ */
+ function canTalk( $index ) {
+ return( $index >= NS_MAIN );
+ }
+}
+?>
diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php
new file mode 100644
index 00000000..fe7417d2
--- /dev/null
+++ b/includes/ObjectCache.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+
+/**
+ * FakeMemCachedClient imitates the API of memcached-client v. 0.1.2.
+ * It acts as a memcached server with no RAM, that is, all objects are
+ * cleared the moment they are set. All set operations succeed and all
+ * get operations return null.
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+class FakeMemCachedClient {
+ function add ($key, $val, $exp = 0) { return true; }
+ function decr ($key, $amt=1) { return null; }
+ function delete ($key, $time = 0) { return false; }
+ function disconnect_all () { }
+ function enable_compress ($enable) { }
+ function forget_dead_hosts () { }
+ function get ($key) { return null; }
+ function get_multi ($keys) { return array_pad(array(), count($keys), null); }
+ function incr ($key, $amt=1) { return null; }
+ function replace ($key, $value, $exp=0) { return false; }
+ function run_command ($sock, $cmd) { return null; }
+ function set ($key, $value, $exp=0){ return true; }
+ function set_compress_threshold ($thresh){ }
+ function set_debug ($dbg) { }
+ function set_servers ($list) { }
+}
+
+global $wgCaches;
+$wgCaches = array();
+
+/** @todo document */
+function &wfGetCache( $inputType ) {
+ global $wgCaches, $wgMemCachedServers, $wgMemCachedDebug, $wgMemCachedPersistent;
+ $cache = false;
+
+ if ( $inputType == CACHE_ANYTHING ) {
+ reset( $wgCaches );
+ $type = key( $wgCaches );
+ if ( $type === false || $type === CACHE_NONE ) {
+ $type = CACHE_DB;
+ }
+ } else {
+ $type = $inputType;
+ }
+
+ if ( $type == CACHE_MEMCACHED ) {
+ if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ){
+ require_once( 'memcached-client.php' );
+
+ if (!class_exists("MemcachedClientforWiki")) {
+ class MemCachedClientforWiki extends memcached {
+ function _debugprint( $text ) {
+ wfDebug( "memcached: $text\n" );
+ }
+ }
+ }
+
+ $wgCaches[CACHE_DB] = new MemCachedClientforWiki(
+ array('persistant' => $wgMemCachedPersistent, 'compress_threshold' => 1500 ) );
+ $cache =& $wgCaches[CACHE_DB];
+ $cache->set_servers( $wgMemCachedServers );
+ $cache->set_debug( $wgMemCachedDebug );
+ }
+ } elseif ( $type == CACHE_ACCEL ) {
+ if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) {
+ if ( function_exists( 'eaccelerator_get' ) ) {
+ require_once( 'BagOStuff.php' );
+ $wgCaches[CACHE_ACCEL] = new eAccelBagOStuff;
+ } elseif ( function_exists( 'apc_fetch') ) {
+ require_once( 'BagOStuff.php' );
+ $wgCaches[CACHE_ACCEL] = new APCBagOStuff;
+ } elseif ( function_exists( 'mmcache_get' ) ) {
+ require_once( 'BagOStuff.php' );
+ $wgCaches[CACHE_ACCEL] = new TurckBagOStuff;
+ } else {
+ $wgCaches[CACHE_ACCEL] = false;
+ }
+ }
+ if ( $wgCaches[CACHE_ACCEL] !== false ) {
+ $cache =& $wgCaches[CACHE_ACCEL];
+ }
+ }
+
+ if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) {
+ if ( !array_key_exists( CACHE_DB, $wgCaches ) ) {
+ require_once( 'BagOStuff.php' );
+ $wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache');
+ }
+ $cache =& $wgCaches[CACHE_DB];
+ }
+
+ if ( $cache === false ) {
+ if ( !array_key_exists( CACHE_NONE, $wgCaches ) ) {
+ $wgCaches[CACHE_NONE] = new FakeMemCachedClient;
+ }
+ $cache =& $wgCaches[CACHE_NONE];
+ }
+
+ return $cache;
+}
+
+function &wfGetMainCache() {
+ global $wgMainCacheType;
+ $ret =& wfGetCache( $wgMainCacheType );
+ return $ret;
+}
+
+function &wfGetMessageCacheStorage() {
+ global $wgMessageCacheType;
+ $ret =& wfGetCache( $wgMessageCacheType );
+ return $ret;
+}
+
+function &wfGetParserCacheStorage() {
+ global $wgParserCacheType;
+ $ret =& wfGetCache( $wgParserCacheType );
+ return $ret;
+}
+
+?>
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
new file mode 100644
index 00000000..31a0781a
--- /dev/null
+++ b/includes/OutputPage.php
@@ -0,0 +1,1078 @@
+<?php
+if ( ! defined( 'MEDIAWIKI' ) )
+ die( 1 );
+/**
+ * @package MediaWiki
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class OutputPage {
+ var $mMetatags, $mKeywords;
+ var $mLinktags, $mPagetitle, $mBodytext, $mDebugtext;
+ var $mHTMLtitle, $mRobotpolicy, $mIsarticle, $mPrintable;
+ var $mSubtitle, $mRedirect, $mStatusCode;
+ var $mLastModified, $mETag, $mCategoryLinks;
+ var $mScripts, $mLinkColours, $mPageLinkTitle;
+
+ var $mSuppressQuickbar;
+ var $mOnloadHandler;
+ var $mDoNothing;
+ var $mContainsOldMagic, $mContainsNewMagic;
+ var $mIsArticleRelated;
+ var $mParserOptions;
+ var $mShowFeedLinks = false;
+ var $mEnableClientCache = true;
+ var $mArticleBodyOnly = false;
+
+ var $mNewSectionLink = false;
+ var $mNoGallery = false;
+
+ /**
+ * Constructor
+ * Initialise private variables
+ */
+ function OutputPage() {
+ $this->mMetatags = $this->mKeywords = $this->mLinktags = array();
+ $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
+ $this->mRedirect = $this->mLastModified =
+ $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy =
+ $this->mOnloadHandler = $this->mPageLinkTitle = '';
+ $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true;
+ $this->mSuppressQuickbar = $this->mPrintable = false;
+ $this->mLanguageLinks = array();
+ $this->mCategoryLinks = array();
+ $this->mDoNothing = false;
+ $this->mContainsOldMagic = $this->mContainsNewMagic = 0;
+ $this->mParserOptions = ParserOptions::newFromUser( $temp = NULL );
+ $this->mSquidMaxage = 0;
+ $this->mScripts = '';
+ $this->mETag = false;
+ $this->mRevisionId = null;
+ $this->mNewSectionLink = false;
+ }
+
+ function redirect( $url, $responsecode = '302' ) {
+ # Strip newlines as a paranoia check for header injection in PHP<5.1.2
+ $this->mRedirect = str_replace( "\n", '', $url );
+ $this->mRedirectCode = $responsecode;
+ }
+
+ function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; }
+
+ # To add an http-equiv meta tag, precede the name with "http:"
+ function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
+ function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
+ function addScript( $script ) { $this->mScripts .= $script; }
+ function getScript() { return $this->mScripts; }
+
+ function setETag($tag) { $this->mETag = $tag; }
+ function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; }
+ function getArticleBodyOnly($only) { return $this->mArticleBodyOnly; }
+
+ function addLink( $linkarr ) {
+ # $linkarr should be an associative array of attributes. We'll escape on output.
+ array_push( $this->mLinktags, $linkarr );
+ }
+
+ function addMetadataLink( $linkarr ) {
+ # note: buggy CC software only reads first "meta" link
+ static $haveMeta = false;
+ $linkarr['rel'] = ($haveMeta) ? 'alternate meta' : 'meta';
+ $this->addLink( $linkarr );
+ $haveMeta = true;
+ }
+
+ /**
+ * checkLastModified tells the client to use the client-cached page if
+ * possible. If sucessful, the OutputPage is disabled so that
+ * any future call to OutputPage->output() have no effect. The method
+ * returns true iff cache-ok headers was sent.
+ */
+ function checkLastModified ( $timestamp ) {
+ global $wgCachePages, $wgCacheEpoch, $wgUser;
+ $fname = 'OutputPage::checkLastModified';
+
+ if ( !$timestamp || $timestamp == '19700101000000' ) {
+ wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" );
+ return;
+ }
+ if( !$wgCachePages ) {
+ wfDebug( "$fname: CACHE DISABLED\n", false );
+ return;
+ }
+ if( $wgUser->getOption( 'nocache' ) ) {
+ wfDebug( "$fname: USER DISABLED CACHE\n", false );
+ return;
+ }
+
+ $timestamp=wfTimestamp(TS_MW,$timestamp);
+ $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) );
+
+ if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+ # IE sends sizes after the date like this:
+ # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
+ # this breaks strtotime().
+ $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
+ $modsinceTime = strtotime( $modsince );
+ $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
+ wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false );
+ wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false );
+ if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) {
+ # Make sure you're in a place you can leave when you call us!
+ header( "HTTP/1.0 304 Not Modified" );
+ $this->mLastModified = $lastmod;
+ $this->sendCacheControl();
+ wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+ $this->disable();
+ @ob_end_clean(); // Don't output compressed blob
+ return true;
+ } else {
+ wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+ $this->mLastModified = $lastmod;
+ }
+ } else {
+ wfDebug( "$fname: client did not send If-Modified-Since header\n", false );
+ $this->mLastModified = $lastmod;
+ }
+ }
+
+ function getPageTitleActionText () {
+ global $action;
+ switch($action) {
+ case 'edit':
+ case 'delete':
+ case 'protect':
+ case 'unprotect':
+ case 'watch':
+ case 'unwatch':
+ // Display title is already customized
+ return '';
+ case 'history':
+ return wfMsg('history_short');
+ case 'submit':
+ // FIXME: bug 2735; not correct for special pages etc
+ return wfMsg('preview');
+ case 'info':
+ return wfMsg('info_short');
+ default:
+ return '';
+ }
+ }
+
+ function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; }
+ function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; }
+ function setPageTitle( $name ) {
+ global $action, $wgContLang;
+ $name = $wgContLang->convert($name, true);
+ $this->mPagetitle = $name;
+ if(!empty($action)) {
+ $taction = $this->getPageTitleActionText();
+ if( !empty( $taction ) ) {
+ $name .= ' - '.$taction;
+ }
+ }
+
+ $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) );
+ }
+ function getHTMLTitle() { return $this->mHTMLtitle; }
+ function getPageTitle() { return $this->mPagetitle; }
+ function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514
+ function getSubtitle() { return $this->mSubtitle; }
+ function isArticle() { return $this->mIsarticle; }
+ function setPrintable() { $this->mPrintable = true; }
+ function isPrintable() { return $this->mPrintable; }
+ function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; }
+ function isSyndicated() { return $this->mShowFeedLinks; }
+ function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; }
+ function getOnloadHandler() { return $this->mOnloadHandler; }
+ function disable() { $this->mDoNothing = true; }
+
+ function setArticleRelated( $v ) {
+ $this->mIsArticleRelated = $v;
+ if ( !$v ) {
+ $this->mIsarticle = false;
+ }
+ }
+ function setArticleFlag( $v ) {
+ $this->mIsarticle = $v;
+ if ( $v ) {
+ $this->mIsArticleRelated = $v;
+ }
+ }
+
+ function isArticleRelated() { return $this->mIsArticleRelated; }
+
+ function getLanguageLinks() { return $this->mLanguageLinks; }
+ function addLanguageLinks($newLinkArray) {
+ $this->mLanguageLinks += $newLinkArray;
+ }
+ function setLanguageLinks($newLinkArray) {
+ $this->mLanguageLinks = $newLinkArray;
+ }
+
+ function getCategoryLinks() {
+ return $this->mCategoryLinks;
+ }
+
+ /**
+ * Add an array of categories, with names in the keys
+ */
+ function addCategoryLinks($categories) {
+ global $wgUser, $wgContLang;
+
+ if ( !is_array( $categories ) ) {
+ return;
+ }
+ # Add the links to the link cache in a batch
+ $arr = array( NS_CATEGORY => $categories );
+ $lb = new LinkBatch;
+ $lb->setArray( $arr );
+ $lb->execute();
+
+ $sk =& $wgUser->getSkin();
+ foreach ( $categories as $category => $arbitrary ) {
+ $title = Title::makeTitleSafe( NS_CATEGORY, $category );
+ $text = $wgContLang->convertHtml( $title->getText() );
+ $this->mCategoryLinks[] = $sk->makeLinkObj( $title, $text );
+ }
+ }
+
+ function setCategoryLinks($categories) {
+ $this->mCategoryLinks = array();
+ $this->addCategoryLinks($categories);
+ }
+
+ function suppressQuickbar() { $this->mSuppressQuickbar = true; }
+ function isQuickbarSuppressed() { return $this->mSuppressQuickbar; }
+
+ function addHTML( $text ) { $this->mBodytext .= $text; }
+ function clearHTML() { $this->mBodytext = ''; }
+ function getHTML() { return $this->mBodytext; }
+ function debug( $text ) { $this->mDebugtext .= $text; }
+
+ /* @deprecated */
+ function setParserOptions( $options ) {
+ return $this->ParserOptions( $options );
+ }
+
+ function ParserOptions( $options = null ) {
+ return wfSetVar( $this->mParserOptions, $options );
+ }
+
+ /**
+ * Set the revision ID which will be seen by the wiki text parser
+ * for things such as embedded {{REVISIONID}} variable use.
+ * @param mixed $revid an integer, or NULL
+ * @return mixed previous value
+ */
+ function setRevisionId( $revid ) {
+ $val = is_null( $revid ) ? null : intval( $revid );
+ return wfSetVar( $this->mRevisionId, $val );
+ }
+
+ /**
+ * Convert wikitext to HTML and add it to the buffer
+ * Default assumes that the current page title will
+ * be used.
+ */
+ function addWikiText( $text, $linestart = true ) {
+ global $wgTitle;
+ $this->addWikiTextTitle($text, $wgTitle, $linestart);
+ }
+
+ function addWikiTextWithTitle($text, &$title, $linestart = true) {
+ $this->addWikiTextTitle($text, $title, $linestart);
+ }
+
+ function addWikiTextTitle($text, &$title, $linestart) {
+ global $wgParser;
+ $parserOutput = $wgParser->parse( $text, $title, $this->mParserOptions,
+ $linestart, true, $this->mRevisionId );
+ $this->addParserOutput( $parserOutput );
+ }
+
+ function addParserOutputNoText( &$parserOutput ) {
+ $this->mLanguageLinks += $parserOutput->getLanguageLinks();
+ $this->addCategoryLinks( $parserOutput->getCategories() );
+ $this->mNewSectionLink = $parserOutput->getNewSection();
+ $this->addKeywords( $parserOutput );
+ if ( $parserOutput->getCacheTime() == -1 ) {
+ $this->enableClientCache( false );
+ }
+ if ( $parserOutput->mHTMLtitle != "" ) {
+ $this->mPagetitle = $parserOutput->mHTMLtitle ;
+ $this->mSubtitle .= $parserOutput->mSubtitle ;
+ }
+ }
+
+ function addParserOutput( &$parserOutput ) {
+ $this->addParserOutputNoText( $parserOutput );
+ $this->addHTML( $parserOutput->getText() );
+ }
+
+ /**
+ * Add wikitext to the buffer, assuming that this is the primary text for a page view
+ * Saves the text into the parser cache if possible
+ */
+ function addPrimaryWikiText( $text, $article, $cache = true ) {
+ global $wgParser, $wgUser;
+
+ $this->mParserOptions->setTidy(true);
+ $parserOutput = $wgParser->parse( $text, $article->mTitle,
+ $this->mParserOptions, true, true, $this->mRevisionId );
+ $this->mParserOptions->setTidy(false);
+ if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) {
+ $parserCache =& ParserCache::singleton();
+ $parserCache->save( $parserOutput, $article, $wgUser );
+ }
+
+ $this->addParserOutputNoText( $parserOutput );
+ $text = $parserOutput->getText();
+ $this->mNoGallery = $parserOutput->getNoGallery();
+ wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) );
+ $parserOutput->setText( $text );
+ $this->addHTML( $parserOutput->getText() );
+ }
+
+ /**
+ * For anything that isn't primary text or interface message
+ */
+ function addSecondaryWikiText( $text, $linestart = true ) {
+ global $wgTitle;
+ $this->mParserOptions->setTidy(true);
+ $this->addWikiTextTitle($text, $wgTitle, $linestart);
+ $this->mParserOptions->setTidy(false);
+ }
+
+
+ /**
+ * Add the output of a QuickTemplate to the output buffer
+ * @param QuickTemplate $template
+ */
+ function addTemplate( &$template ) {
+ ob_start();
+ $template->execute();
+ $this->addHTML( ob_get_contents() );
+ ob_end_clean();
+ }
+
+ /**
+ * Parse wikitext and return the HTML.
+ */
+ function parse( $text, $linestart = true, $interface = false ) {
+ global $wgParser, $wgTitle;
+ if ( $interface) { $this->mParserOptions->setInterfaceMessage(true); }
+ $parserOutput = $wgParser->parse( $text, $wgTitle, $this->mParserOptions,
+ $linestart, true, $this->mRevisionId );
+ if ( $interface) { $this->mParserOptions->setInterfaceMessage(false); }
+ return $parserOutput->getText();
+ }
+
+ /**
+ * @param $article
+ * @param $user
+ *
+ * @return bool
+ */
+ function tryParserCache( &$article, $user ) {
+ $parserCache =& ParserCache::singleton();
+ $parserOutput = $parserCache->get( $article, $user );
+ if ( $parserOutput !== false ) {
+ $this->mLanguageLinks += $parserOutput->getLanguageLinks();
+ $this->addCategoryLinks( $parserOutput->getCategories() );
+ $this->addKeywords( $parserOutput );
+ $this->mNewSectionLink = $parserOutput->getNewSection();
+ $this->mNoGallery = $parserOutput->getNoGallery();
+ $text = $parserOutput->getText();
+ wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) );
+ $this->addHTML( $text );
+ $t = $parserOutput->getTitleText();
+ if( !empty( $t ) ) {
+ $this->setPageTitle( $t );
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Set the maximum cache time on the Squid in seconds
+ * @param $maxage
+ */
+ function setSquidMaxage( $maxage ) {
+ $this->mSquidMaxage = $maxage;
+ }
+
+ /**
+ * Use enableClientCache(false) to force it to send nocache headers
+ * @param $state
+ */
+ function enableClientCache( $state ) {
+ return wfSetVar( $this->mEnableClientCache, $state );
+ }
+
+ function uncacheableBecauseRequestvars() {
+ global $wgRequest;
+ return $wgRequest->getText('useskin', false) === false
+ && $wgRequest->getText('uselang', false) === false;
+ }
+
+ function sendCacheControl() {
+ global $wgUseSquid, $wgUseESI, $wgSquidMaxage;
+ $fname = 'OutputPage::sendCacheControl';
+
+ if ($this->mETag)
+ header("ETag: $this->mETag");
+
+ # don't serve compressed data to clients who can't handle it
+ # maintain different caches for logged-in users and non-logged in ones
+ header( 'Vary: Accept-Encoding, Cookie' );
+ if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) {
+ if( $wgUseSquid && ! isset( $_COOKIE[ini_get( 'session.name') ] ) &&
+ ! $this->isPrintable() && $this->mSquidMaxage != 0 )
+ {
+ if ( $wgUseESI ) {
+ # We'll purge the proxy cache explicitly, but require end user agents
+ # to revalidate against the proxy on each visit.
+ # Surrogate-Control controls our Squid, Cache-Control downstream caches
+ wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false );
+ # start with a shorter timeout for initial testing
+ # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
+ header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"');
+ header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
+ } else {
+ # We'll purge the proxy cache for anons explicitly, but require end user agents
+ # to revalidate against the proxy on each visit.
+ # IMPORTANT! The Squid needs to replace the Cache-Control header with
+ # Cache-Control: s-maxage=0, must-revalidate, max-age=0
+ wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false );
+ # start with a shorter timeout for initial testing
+ # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
+ header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' );
+ }
+ } else {
+ # We do want clients to cache if they can, but they *must* check for updates
+ # on revisiting the page.
+ wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false );
+ header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ header( "Cache-Control: private, must-revalidate, max-age=0" );
+ }
+ if($this->mLastModified) header( "Last-modified: {$this->mLastModified}" );
+ } else {
+ wfDebug( "$fname: no caching **\n", false );
+
+ # In general, the absence of a last modified header should be enough to prevent
+ # the client from using its cache. We send a few other things just to make sure.
+ header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+ header( 'Pragma: no-cache' );
+ }
+ }
+
+ /**
+ * Finally, all the text has been munged and accumulated into
+ * the object, let's actually output it:
+ */
+ function output() {
+ global $wgUser, $wgOutputEncoding;
+ global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
+ global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgScriptPath, $wgServer;
+
+ if( $this->mDoNothing ){
+ return;
+ }
+ $fname = 'OutputPage::output';
+ wfProfileIn( $fname );
+ $sk = $wgUser->getSkin();
+
+ if ( $wgUseAjax ) {
+ $this->addScript( "<script type=\"{$wgJsMimeType}\">
+ var wgScriptPath=\"{$wgScriptPath}\";
+ var wgServer=\"{$wgServer}\";
+ </script>" );
+ $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js\"></script>\n" );
+ }
+
+ if ( '' != $this->mRedirect ) {
+ if( substr( $this->mRedirect, 0, 4 ) != 'http' ) {
+ # Standards require redirect URLs to be absolute
+ global $wgServer;
+ $this->mRedirect = $wgServer . $this->mRedirect;
+ }
+ if( $this->mRedirectCode == '301') {
+ if( !$wgDebugRedirects ) {
+ header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently");
+ }
+ $this->mLastModified = wfTimestamp( TS_RFC2822 );
+ }
+
+ $this->sendCacheControl();
+
+ if( $wgDebugRedirects ) {
+ $url = htmlspecialchars( $this->mRedirect );
+ print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
+ print "<p>Location: <a href=\"$url\">$url</a></p>\n";
+ print "</body>\n</html>\n";
+ } else {
+ header( 'Location: '.$this->mRedirect );
+ }
+ wfProfileOut( $fname );
+ return;
+ }
+ elseif ( $this->mStatusCode )
+ {
+ $statusMessage = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Request Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 507 => 'Insufficient Storage'
+ );
+
+ if ( $statusMessage[$this->mStatusCode] )
+ header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] );
+ }
+
+ # Buffer output; final headers may depend on later processing
+ ob_start();
+
+ # Disable temporary placeholders, so that the skin produces HTML
+ $sk->postParseLinkColour( false );
+
+ header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" );
+ header( 'Content-language: '.$wgContLanguageCode );
+
+ if ($this->mArticleBodyOnly) {
+ $this->out($this->mBodytext);
+ } else {
+ wfProfileIn( 'Output-skin' );
+ $sk->outputPage( $this );
+ wfProfileOut( 'Output-skin' );
+ }
+
+ $this->sendCacheControl();
+ ob_end_flush();
+ wfProfileOut( $fname );
+ }
+
+ function out( $ins ) {
+ global $wgInputEncoding, $wgOutputEncoding, $wgContLang;
+ if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) {
+ $outs = $ins;
+ } else {
+ $outs = $wgContLang->iconv( $wgInputEncoding, $wgOutputEncoding, $ins );
+ if ( false === $outs ) { $outs = $ins; }
+ }
+ print $outs;
+ }
+
+ function setEncodings() {
+ global $wgInputEncoding, $wgOutputEncoding;
+ global $wgUser, $wgContLang;
+
+ $wgInputEncoding = strtolower( $wgInputEncoding );
+
+ if( $wgUser->getOption( 'altencoding' ) ) {
+ $wgContLang->setAltEncoding();
+ return;
+ }
+
+ if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) {
+ $wgOutputEncoding = strtolower( $wgOutputEncoding );
+ return;
+ }
+ $wgOutputEncoding = $wgInputEncoding;
+ }
+
+ /**
+ * Returns a HTML comment with the elapsed time since request.
+ * This method has no side effects.
+ * Use wfReportTime() instead.
+ * @return string
+ * @deprecated
+ */
+ function reportTime() {
+ $time = wfReportTime();
+ return $time;
+ }
+
+ /**
+ * Produce a "user is blocked" page
+ */
+ function blockedPage( $return = true ) {
+ global $wgUser, $wgContLang, $wgTitle;
+
+ $this->setPageTitle( wfMsg( 'blockedtitle' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+
+ $id = $wgUser->blockedBy();
+ $reason = $wgUser->blockedFor();
+ $ip = wfGetIP();
+
+ if ( is_numeric( $id ) ) {
+ $name = User::whoIs( $id );
+ } else {
+ $name = $id;
+ }
+ $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
+
+ $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name ) );
+
+ # Don't auto-return to special pages
+ if( $return ) {
+ $return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL;
+ $this->returnToMain( false, $return );
+ }
+ }
+
+ /**
+ * Note: these arguments are keys into wfMsg(), not text!
+ */
+ function showErrorPage( $title, $msg ) {
+ global $wgTitle;
+
+ $this->mDebugtext .= 'Original title: ' .
+ $wgTitle->getPrefixedText() . "\n";
+ $this->setPageTitle( wfMsg( $title ) );
+ $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->enableClientCache( false );
+ $this->mRedirect = '';
+
+ $this->mBodytext = '';
+ $this->addWikiText( wfMsg( $msg ) );
+ $this->returnToMain( false );
+ }
+
+ /** @obsolete */
+ function errorpage( $title, $msg ) {
+ throw new ErrorPageError( $title, $msg );
+ }
+
+ /**
+ * Display an error page indicating that a given version of MediaWiki is
+ * required to use it
+ *
+ * @param mixed $version The version of MediaWiki needed to use the page
+ */
+ function versionRequired( $version ) {
+ $this->setPageTitle( wfMsg( 'versionrequired', $version ) );
+ $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->mBodytext = '';
+
+ $this->addWikiText( wfMsg( 'versionrequiredtext', $version ) );
+ $this->returnToMain();
+ }
+
+ /**
+ * Display an error page noting that a given permission bit is required.
+ * This should generally replace the sysopRequired, developerRequired etc.
+ * @param string $permission key required
+ */
+ function permissionRequired( $permission ) {
+ global $wgUser;
+
+ $this->setPageTitle( wfMsg( 'badaccess' ) );
+ $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->mBodytext = '';
+
+ $sk = $wgUser->getSkin();
+ $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ) );
+ $this->addHTML( wfMsgHtml( 'badaccesstext', $ap, $permission ) );
+ $this->returnToMain();
+ }
+
+ /**
+ * @deprecated
+ */
+ function sysopRequired() {
+ global $wgUser;
+
+ $this->setPageTitle( wfMsg( 'sysoptitle' ) );
+ $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->mBodytext = '';
+
+ $sk = $wgUser->getSkin();
+ $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' );
+ $this->addHTML( wfMsgHtml( 'sysoptext', $ap ) );
+ $this->returnToMain();
+ }
+
+ /**
+ * @deprecated
+ */
+ function developerRequired() {
+ global $wgUser;
+
+ $this->setPageTitle( wfMsg( 'developertitle' ) );
+ $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->mBodytext = '';
+
+ $sk = $wgUser->getSkin();
+ $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' );
+ $this->addHTML( wfMsgHtml( 'developertext', $ap ) );
+ $this->returnToMain();
+ }
+
+ /**
+ * Produce the stock "please login to use the wiki" page
+ */
+ function loginToUse() {
+ global $wgUser, $wgTitle, $wgContLang;
+ $skin = $wgUser->getSkin();
+
+ $this->setPageTitle( wfMsg( 'loginreqtitle' ) );
+ $this->setHtmlTitle( wfMsg( 'errorpagetitle' ) );
+ $this->setRobotPolicy( 'noindex,nofollow' );
+ $this->setArticleFlag( false );
+
+ $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() );
+ $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) );
+ $this->addHtml( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" );
+
+ $this->returnToMain();
+ }
+
+ /** @obsolete */
+ function databaseError( $fname, $sql, $error, $errno ) {
+ throw new MWException( "OutputPage::databaseError is obsolete\n" );
+ }
+
+ function readOnlyPage( $source = null, $protected = false ) {
+ global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle;
+
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+
+ if( $protected ) {
+ $skin = $wgUser->getSkin();
+ $this->setPageTitle( wfMsg( 'viewsource' ) );
+ $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) );
+
+ # Determine if protection is due to the page being a system message
+ # and show an appropriate explanation
+ if( $wgTitle->getNamespace() == NS_MEDIAWIKI && !$wgUser->isAllowed( 'editinterface' ) ) {
+ $this->addWikiText( wfMsg( 'protectedinterface' ) );
+ } else {
+ $this->addWikiText( wfMsg( 'protectedtext' ) );
+ }
+ } else {
+ $this->setPageTitle( wfMsg( 'readonly' ) );
+ if ( $wgReadOnly ) {
+ $reason = $wgReadOnly;
+ } else {
+ $reason = file_get_contents( $wgReadOnlyFile );
+ }
+ $this->addWikiText( wfMsg( 'readonlytext', $reason ) );
+ }
+
+ if( is_string( $source ) ) {
+ if( strcmp( $source, '' ) == 0 ) {
+ global $wgTitle;
+ if ( $wgTitle->getNamespace() == NS_MEDIAWIKI ) {
+ $source = wfMsgWeirdKey ( $wgTitle->getText() );
+ } else {
+ $source = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' );
+ }
+ }
+ $rows = $wgUser->getIntOption( 'rows' );
+ $cols = $wgUser->getIntOption( 'cols' );
+
+ $text = "\n<textarea name='wpTextbox1' id='wpTextbox1' cols='$cols' rows='$rows' readonly='readonly'>" .
+ htmlspecialchars( $source ) . "\n</textarea>";
+ $this->addHTML( $text );
+ }
+
+ $this->returnToMain( false );
+ }
+
+ /** @obsolete */
+ function fatalError( $message ) {
+ throw new FatalError( $message );
+ }
+
+ /** @obsolete */
+ function unexpectedValueError( $name, $val ) {
+ throw new FatalError( wfMsg( 'unexpected', $name, $val ) );
+ }
+
+ /** @obsolete */
+ function fileCopyError( $old, $new ) {
+ throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) );
+ }
+
+ /** @obsolete */
+ function fileRenameError( $old, $new ) {
+ throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) );
+ }
+
+ /** @obsolete */
+ function fileDeleteError( $name ) {
+ throw new FatalError( wfMsg( 'filedeleteerror', $name ) );
+ }
+
+ /** @obsolete */
+ function fileNotFoundError( $name ) {
+ throw new FatalError( wfMsg( 'filenotfound', $name ) );
+ }
+
+ function showFatalError( $message ) {
+ $this->setPageTitle( wfMsg( "internalerror" ) );
+ $this->setRobotpolicy( "noindex,nofollow" );
+ $this->setArticleRelated( false );
+ $this->enableClientCache( false );
+ $this->mRedirect = '';
+ $this->mBodytext = $message;
+ }
+
+ function showUnexpectedValueError( $name, $val ) {
+ $this->showFatalError( wfMsg( 'unexpected', $name, $val ) );
+ }
+
+ function showFileCopyError( $old, $new ) {
+ $this->showFatalError( wfMsg( 'filecopyerror', $old, $new ) );
+ }
+
+ function showFileRenameError( $old, $new ) {
+ $this->showFatalError( wfMsg( 'filerenameerror', $old, $new ) );
+ }
+
+ function showFileDeleteError( $name ) {
+ $this->showFatalError( wfMsg( 'filedeleteerror', $name ) );
+ }
+
+ function showFileNotFoundError( $name ) {
+ $this->showFatalError( wfMsg( 'filenotfound', $name ) );
+ }
+
+ /**
+ * return from error messages or notes
+ * @param $auto automatically redirect the user after 10 seconds
+ * @param $returnto page title to return to. Default is Main Page.
+ */
+ function returnToMain( $auto = true, $returnto = NULL ) {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if ( $returnto == NULL ) {
+ $returnto = $wgRequest->getText( 'returnto' );
+ }
+
+ if ( '' === $returnto ) {
+ $returnto = wfMsgForContent( 'mainpage' );
+ }
+
+ if ( is_object( $returnto ) ) {
+ $titleObj = $returnto;
+ } else {
+ $titleObj = Title::newFromText( $returnto );
+ }
+ if ( !is_object( $titleObj ) ) {
+ $titleObj = Title::newMainPage();
+ }
+
+ $sk = $wgUser->getSkin();
+ $link = $sk->makeLinkObj( $titleObj, '' );
+
+ $r = wfMsg( 'returnto', $link );
+ if ( $auto ) {
+ $wgOut->addMeta( 'http:Refresh', '10;url=' . $titleObj->escapeFullURL() );
+ }
+ $wgOut->addHTML( "\n<p>$r</p>\n" );
+ }
+
+ /**
+ * This function takes the title (first item of mGoodLinks), categories, existing and broken links for the page
+ * and uses the first 10 of them for META keywords
+ */
+ function addKeywords( &$parserOutput ) {
+ global $wgTitle;
+ $this->addKeyword( $wgTitle->getPrefixedText() );
+ $count = 1;
+ $links2d =& $parserOutput->getLinks();
+ if ( !is_array( $links2d ) ) {
+ return;
+ }
+ foreach ( $links2d as $ns => $dbkeys ) {
+ foreach( $dbkeys as $dbkey => $id ) {
+ $this->addKeyword( $dbkey );
+ if ( ++$count > 10 ) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ /**
+ * @access private
+ * @return string
+ */
+ function headElement() {
+ global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType;
+ global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle;
+
+ if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) {
+ $ret = "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n";
+ } else {
+ $ret = '';
+ }
+
+ $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
+
+ if ( '' == $this->getHTMLTitle() ) {
+ $this->setHTMLTitle( wfMsg( 'pagetitle', $this->getPageTitle() ));
+ }
+
+ $rtl = $wgContLang->isRTL() ? " dir='RTL'" : '';
+ $ret .= "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n";
+ $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n";
+ array_push( $this->mMetatags, array( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ) );
+
+ $ret .= $this->getHeadLinks();
+ global $wgStylePath;
+ if( $this->isPrintable() ) {
+ $media = '';
+ } else {
+ $media = "media='print'";
+ }
+ $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css" );
+ $ret .= "<link rel='stylesheet' type='text/css' $media href='$printsheet' />\n";
+
+ $sk = $wgUser->getSkin();
+ $ret .= $sk->getHeadScripts();
+ $ret .= $this->mScripts;
+ $ret .= $sk->getUserStyles();
+
+ if ($wgUseTrackbacks && $this->isArticleRelated())
+ $ret .= $wgTitle->trackbackRDF();
+
+ $ret .= "</head>\n";
+ return $ret;
+ }
+
+ function getHeadLinks() {
+ global $wgRequest;
+ $ret = '';
+ foreach ( $this->mMetatags as $tag ) {
+ if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
+ $a = 'http-equiv';
+ $tag[0] = substr( $tag[0], 5 );
+ } else {
+ $a = 'name';
+ }
+ $ret .= "<meta $a=\"{$tag[0]}\" content=\"{$tag[1]}\" />\n";
+ }
+
+ $p = $this->mRobotpolicy;
+ if( $p !== '' && $p != 'index,follow' ) {
+ // http://www.robotstxt.org/wc/meta-user.html
+ // Only show if it's different from the default robots policy
+ $ret .= "<meta name=\"robots\" content=\"$p\" />\n";
+ }
+
+ if ( count( $this->mKeywords ) > 0 ) {
+ $strip = array(
+ "/<.*?>/" => '',
+ "/_/" => ' '
+ );
+ $ret .= "<meta name=\"keywords\" content=\"" .
+ htmlspecialchars(preg_replace(array_keys($strip), array_values($strip),implode( ",", $this->mKeywords ))) . "\" />\n";
+ }
+ foreach ( $this->mLinktags as $tag ) {
+ $ret .= '<link';
+ foreach( $tag as $attr => $val ) {
+ $ret .= " $attr=\"" . htmlspecialchars( $val ) . "\"";
+ }
+ $ret .= " />\n";
+ }
+ if( $this->isSyndicated() ) {
+ # FIXME: centralize the mime-type and name information in Feed.php
+ $link = $wgRequest->escapeAppendQuery( 'feed=rss' );
+ $ret .= "<link rel='alternate' type='application/rss+xml' title='RSS 2.0' href='$link' />\n";
+ $link = $wgRequest->escapeAppendQuery( 'feed=atom' );
+ $ret .= "<link rel='alternate' type='application/atom+xml' title='Atom 0.3' href='$link' />\n";
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Turn off regular page output and return an error reponse
+ * for when rate limiting has triggered.
+ * @todo i18n
+ * @access public
+ */
+ function rateLimited() {
+ global $wgOut;
+ $wgOut->disable();
+ wfHttpError( 500, 'Internal Server Error',
+ 'Sorry, the server has encountered an internal error. ' .
+ 'Please wait a moment and hit "refresh" to submit the request again.' );
+ }
+
+ /**
+ * Show an "add new section" link?
+ *
+ * @return bool True if the parser output instructs us to add one
+ */
+ function showNewSectionLink() {
+ return $this->mNewSectionLink;
+ }
+
+}
+?>
diff --git a/includes/PageHistory.php b/includes/PageHistory.php
new file mode 100644
index 00000000..de006285
--- /dev/null
+++ b/includes/PageHistory.php
@@ -0,0 +1,685 @@
+<?php
+/**
+ * Page history
+ *
+ * Split off from Article.php and Skin.php, 2003-12-22
+ * @package MediaWiki
+ */
+
+/**
+ * This class handles printing the history page for an article. In order to
+ * be efficient, it uses timestamps rather than offsets for paging, to avoid
+ * costly LIMIT,offset queries.
+ *
+ * Construct it by passing in an Article, and call $h->history() to print the
+ * history.
+ *
+ * @package MediaWiki
+ */
+
+class PageHistory {
+ const DIR_PREV = 0;
+ const DIR_NEXT = 1;
+
+ var $mArticle, $mTitle, $mSkin;
+ var $lastdate;
+ var $linesonpage;
+ var $mNotificationTimestamp;
+ var $mLatestId = null;
+
+ /**
+ * Construct a new PageHistory.
+ *
+ * @param Article $article
+ * @returns nothing
+ */
+ function PageHistory($article) {
+ global $wgUser;
+
+ $this->mArticle =& $article;
+ $this->mTitle =& $article->mTitle;
+ $this->mNotificationTimestamp = NULL;
+ $this->mSkin = $wgUser->getSkin();
+
+ $this->defaultLimit = 50;
+ }
+
+ /**
+ * Print the history page for an article.
+ *
+ * @returns nothing
+ */
+ function history() {
+ global $wgOut, $wgRequest, $wgTitle;
+
+ /*
+ * Allow client caching.
+ */
+
+ if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) )
+ /* Client cache fresh and headers sent, nothing more to do. */
+ return;
+
+ $fname = 'PageHistory::history';
+ wfProfileIn( $fname );
+
+ /*
+ * Setup page variables.
+ */
+ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ $wgOut->setArticleFlag( false );
+ $wgOut->setArticleRelated( true );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setSyndicated( true );
+
+ $logPage = Title::makeTitle( NS_SPECIAL, 'Log' );
+ $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() );
+
+ $subtitle = wfMsgHtml( 'revhistory' ) . '<br />' . $logLink;
+ $wgOut->setSubtitle( $subtitle );
+
+ $feedType = $wgRequest->getVal( 'feed' );
+ if( $feedType ) {
+ wfProfileOut( $fname );
+ return $this->feed( $feedType );
+ }
+
+ /*
+ * Fail if article doesn't exist.
+ */
+ if( !$this->mTitle->exists() ) {
+ $wgOut->addWikiText( wfMsg( 'nohistory' ) );
+ wfProfileOut( $fname );
+ return;
+ }
+
+ $dbr =& wfGetDB(DB_SLAVE);
+
+ /*
+ * Extract limit, the number of revisions to show, and
+ * offset, the timestamp to begin at, from the URL.
+ */
+ $limit = $wgRequest->getInt('limit', $this->defaultLimit);
+ if ( $limit <= 0 ) {
+ $limit = $this->defaultLimit;
+ } elseif ( $limit > 50000 ) {
+ # Arbitrary maximum
+ # Any more than this and we'll probably get an out of memory error
+ $limit = 50000;
+ }
+
+ $offset = $wgRequest->getText('offset');
+
+ /* Offset must be an integral. */
+ if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset))
+ $offset = 0;
+# $offset = $dbr->timestamp($offset);
+ $dboffset = $offset === 0 ? 0 : $dbr->timestamp($offset);
+ /*
+ * "go=last" means to jump to the last history page.
+ */
+ if (($gowhere = $wgRequest->getText("go")) !== NULL) {
+ $gourl = null;
+ switch ($gowhere) {
+ case "first":
+ if (($lastid = $this->getLastOffsetForPaging($this->mTitle->getArticleID(), $limit)) === NULL)
+ break;
+ $gourl = $wgTitle->getLocalURL("action=history&limit={$limit}&offset=".
+ wfTimestamp(TS_MW, $lastid));
+ break;
+ }
+
+ if (!is_null($gourl)) {
+ $wgOut->redirect($gourl);
+ return;
+ }
+ }
+
+ /*
+ * Fetch revisions.
+ *
+ * If the user clicked "previous", we retrieve the revisions backwards,
+ * then reverse them. This is to avoid needing to know the timestamp of
+ * previous revisions when generating the URL.
+ */
+ $direction = $this->getDirection();
+ $revisions = $this->fetchRevisions($limit, $dboffset, $direction);
+ $navbar = $this->makeNavbar($revisions, $offset, $limit, $direction);
+
+ /*
+ * We fetch one more revision than needed to get the timestamp of the
+ * one after this page (and to know if it exists).
+ *
+ * linesonpage stores the actual number of lines.
+ */
+ if (count($revisions) < $limit + 1)
+ $this->linesonpage = count($revisions);
+ else
+ $this->linesonpage = count($revisions) - 1;
+
+ /* Un-reverse revisions */
+ if ($direction == PageHistory::DIR_PREV)
+ $revisions = array_reverse($revisions);
+
+ /*
+ * Print the top navbar.
+ */
+ $s = $navbar;
+ $s .= $this->beginHistoryList();
+ $counter = 1;
+
+ /*
+ * Print each revision, excluding the one-past-the-end, if any.
+ */
+ foreach (array_slice($revisions, 0, $limit) as $i => $line) {
+ $latest = !$i && $offset == 0;
+ $firstInList = !$i;
+ $next = isset( $revisions[$i + 1] ) ? $revisions[$i + 1 ] : null;
+ $s .= $this->historyLine($line, $next, $counter, $this->getNotificationTimestamp(), $latest, $firstInList);
+ $counter++;
+ }
+
+ /*
+ * End navbar.
+ */
+ $s .= $this->endHistoryList();
+ $s .= $navbar;
+
+ $wgOut->addHTML( $s );
+ wfProfileOut( $fname );
+ }
+
+ /** @todo document */
+ function beginHistoryList() {
+ global $wgTitle;
+ $this->lastdate = '';
+ $s = wfMsgExt( 'histlegend', array( 'parse') );
+ $s .= '<form action="' . $wgTitle->escapeLocalURL( '-' ) . '" method="get">';
+ $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey());
+
+ // The following line is SUPPOSED to have double-quotes around the
+ // $prefixedkey variable, because htmlspecialchars() doesn't escape
+ // single-quotes.
+ //
+ // On at least two occasions people have changed it to single-quotes,
+ // which creates invalid HTML and incorrect display of the resulting
+ // link.
+ //
+ // Please do not break this a third time. Thank you for your kind
+ // consideration and cooperation.
+ //
+ $s .= "<input type='hidden' name='title' value=\"{$prefixedkey}\" />\n";
+
+ $s .= $this->submitButton();
+ $s .= '<ul id="pagehistory">' . "\n";
+ return $s;
+ }
+
+ /** @todo document */
+ function endHistoryList() {
+ $s = '</ul>';
+ $s .= $this->submitButton( array( 'id' => 'historysubmit' ) );
+ $s .= '</form>';
+ return $s;
+ }
+
+ /** @todo document */
+ function submitButton( $bits = array() ) {
+ return ( $this->linesonpage > 0 )
+ ? wfElement( 'input', array_merge( $bits,
+ array(
+ 'class' => 'historysubmit',
+ 'type' => 'submit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ 'value' => wfMsg( 'compareselectedversions' ),
+ ) ) )
+ : '';
+ }
+
+ /** @todo document */
+ function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
+ global $wgUser;
+ $rev = new Revision( $row );
+ $rev->setTitle( $this->mTitle );
+
+ $s = '<li>';
+ $curlink = $this->curLink( $rev, $latest );
+ $lastlink = $this->lastLink( $rev, $next, $counter );
+ $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
+ $link = $this->revLink( $rev );
+
+ $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() )
+ . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() );
+
+ $s .= "($curlink) ($lastlink) $arbitrary";
+
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $revdel = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' );
+ if( $firstInList ) {
+ // We don't currently handle well changing the top revision's settings
+ $del = wfMsgHtml( 'rev-delundel' );
+ } else {
+ $del = $this->mSkin->makeKnownLinkObj( $revdel,
+ wfMsg( 'rev-delundel' ),
+ 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
+ '&oldid=' . urlencode( $rev->getId() ) );
+ }
+ $s .= "(<small>$del</small>) ";
+ }
+
+ $s .= " $link <span class='history-user'>$user</span>";
+
+ if( $row->rev_minor_edit ) {
+ $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
+ }
+
+ $s .= $this->mSkin->revComment( $rev );
+ if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) {
+ $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
+ }
+ if( $row->rev_deleted & Revision::DELETED_TEXT ) {
+ $s .= ' ' . wfMsgHtml( 'deletedrev' );
+ }
+ $s .= "</li>\n";
+
+ return $s;
+ }
+
+ /** @todo document */
+ function revLink( $rev ) {
+ global $wgLang;
+ $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
+ if( $rev->userCan( Revision::DELETED_TEXT ) ) {
+ $link = $this->mSkin->makeKnownLinkObj(
+ $this->mTitle, $date, "oldid=" . $rev->getId() );
+ } else {
+ $link = $date;
+ }
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
+ }
+
+ /** @todo document */
+ function curLink( $rev, $latest ) {
+ $cur = wfMsgExt( 'cur', array( 'escape') );
+ if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ return $cur;
+ } else {
+ return $this->mSkin->makeKnownLinkObj(
+ $this->mTitle, $cur,
+ 'diff=' . $this->getLatestID() .
+ "&oldid=" . $rev->getId() );
+ }
+ }
+
+ /** @todo document */
+ function lastLink( $rev, $next, $counter ) {
+ $last = wfMsgExt( 'last', array( 'escape' ) );
+ if( is_null( $next ) ) {
+ if( $rev->getTimestamp() == $this->getEarliestOffset() ) {
+ return $last;
+ } else {
+ // Cut off by paging; there are more behind us...
+ return $this->mSkin->makeKnownLinkObj(
+ $this->mTitle,
+ $last,
+ "diff=" . $rev->getId() . "&oldid=prev" );
+ }
+ } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ return $last;
+ } else {
+ return $this->mSkin->makeKnownLinkObj(
+ $this->mTitle,
+ $last,
+ "diff=" . $rev->getId() . "&oldid={$next->rev_id}"
+ /*,
+ '',
+ '',
+ "tabindex={$counter}"*/ );
+ }
+ }
+
+ /** @todo document */
+ function diffButtons( $rev, $firstInList, $counter ) {
+ if( $this->linesonpage > 1) {
+ $radio = array(
+ 'type' => 'radio',
+ 'value' => $rev->getId(),
+# do we really need to flood this on every item?
+# 'title' => wfMsgHtml( 'selectolderversionfordiff' )
+ );
+
+ if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ $radio['disabled'] = 'disabled';
+ }
+
+ /** @todo: move title texts to javascript */
+ if ( $firstInList ) {
+ $first = wfElement( 'input', array_merge(
+ $radio,
+ array(
+ 'style' => 'visibility:hidden',
+ 'name' => 'oldid' ) ) );
+ $checkmark = array( 'checked' => 'checked' );
+ } else {
+ if( $counter == 2 ) {
+ $checkmark = array( 'checked' => 'checked' );
+ } else {
+ $checkmark = array();
+ }
+ $first = wfElement( 'input', array_merge(
+ $radio,
+ $checkmark,
+ array( 'name' => 'oldid' ) ) );
+ $checkmark = array();
+ }
+ $second = wfElement( 'input', array_merge(
+ $radio,
+ $checkmark,
+ array( 'name' => 'diff' ) ) );
+ return $first . $second;
+ } else {
+ return '';
+ }
+ }
+
+ /** @todo document */
+ function getLatestOffset( $id = null ) {
+ if ( $id === null) $id = $this->mTitle->getArticleID();
+ return $this->getExtremeOffset( $id, 'max' );
+ }
+
+ /** @todo document */
+ function getEarliestOffset( $id = null ) {
+ if ( $id === null) $id = $this->mTitle->getArticleID();
+ return $this->getExtremeOffset( $id, 'min' );
+ }
+
+ /** @todo document */
+ function getExtremeOffset( $id, $func ) {
+ $db =& wfGetDB(DB_SLAVE);
+ return $db->selectField( 'revision',
+ "$func(rev_timestamp)",
+ array( 'rev_page' => $id ),
+ 'PageHistory::getExtremeOffset' );
+ }
+
+ /** @todo document */
+ function getLatestId() {
+ if( is_null( $this->mLatestId ) ) {
+ $id = $this->mTitle->getArticleID();
+ $db =& wfGetDB(DB_SLAVE);
+ $this->mLatestId = $db->selectField( 'revision',
+ "max(rev_id)",
+ array( 'rev_page' => $id ),
+ 'PageHistory::getLatestID' );
+ }
+ return $this->mLatestId;
+ }
+
+ /** @todo document */
+ function getLastOffsetForPaging( $id, $step ) {
+ $fname = 'PageHistory::getLastOffsetForPaging';
+
+ $dbr =& wfGetDB(DB_SLAVE);
+ $res = $dbr->select(
+ 'revision',
+ 'rev_timestamp',
+ "rev_page=$id",
+ $fname,
+ array('ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $step));
+
+ $n = $dbr->numRows( $res );
+ $last = null;
+ while( $obj = $dbr->fetchObject( $res ) ) {
+ $last = $obj->rev_timestamp;
+ }
+ $dbr->freeResult( $res );
+ return $last;
+ }
+
+ /**
+ * @return returns the direction of browsing watchlist
+ */
+ function getDirection() {
+ global $wgRequest;
+ if ($wgRequest->getText("dir") == "prev")
+ return PageHistory::DIR_PREV;
+ else
+ return PageHistory::DIR_NEXT;
+ }
+
+ /** @todo document */
+ function fetchRevisions($limit, $offset, $direction) {
+ $fname = 'PageHistory::fetchRevisions';
+
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ if ($direction == PageHistory::DIR_PREV)
+ list($dirs, $oper) = array("ASC", ">=");
+ else /* $direction == PageHistory::DIR_NEXT */
+ list($dirs, $oper) = array("DESC", "<=");
+
+ if ($offset)
+ $offsets = array("rev_timestamp $oper '$offset'");
+ else
+ $offsets = array();
+
+ $page_id = $this->mTitle->getArticleID();
+
+ $res = $dbr->select(
+ 'revision',
+ array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text',
+ 'rev_timestamp', 'rev_minor_edit', 'rev_deleted'),
+ array_merge(array("rev_page=$page_id"), $offsets),
+ $fname,
+ array('ORDER BY' => "rev_timestamp $dirs",
+ 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
+ );
+
+ $result = array();
+ while (($obj = $dbr->fetchObject($res)) != NULL)
+ $result[] = $obj;
+
+ return $result;
+ }
+
+ /** @todo document */
+ function getNotificationTimestamp() {
+ global $wgUser, $wgShowUpdatedMarker;
+ $fname = 'PageHistory::getNotficationTimestamp';
+
+ if ($this->mNotificationTimestamp !== NULL)
+ return $this->mNotificationTimestamp;
+
+ if ($wgUser->isAnon() || !$wgShowUpdatedMarker)
+ return $this->mNotificationTimestamp = false;
+
+ $dbr =& wfGetDB(DB_SLAVE);
+
+ $this->mNotificationTimestamp = $dbr->selectField(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ array( 'wl_namespace' => $this->mTitle->getNamespace(),
+ 'wl_title' => $this->mTitle->getDBkey(),
+ 'wl_user' => $wgUser->getID()
+ ),
+ $fname);
+
+ // Don't use the special value reserved for telling whether the field is filled
+ if ( is_null( $this->mNotificationTimestamp ) ) {
+ $this->mNotificationTimestamp = false;
+ }
+
+ return $this->mNotificationTimestamp;
+ }
+
+ /** @todo document */
+ function makeNavbar($revisions, $offset, $limit, $direction) {
+ global $wgLang;
+
+ $revisions = array_slice($revisions, 0, $limit);
+
+ $latestTimestamp = wfTimestamp(TS_MW, $this->getLatestOffset());
+ $earliestTimestamp = wfTimestamp(TS_MW, $this->getEarliestOffset());
+
+ /*
+ * When we're displaying previous revisions, we need to reverse
+ * the array, because it's queried in reverse order.
+ */
+ if ($direction == PageHistory::DIR_PREV)
+ $revisions = array_reverse($revisions);
+
+ /*
+ * lowts is the timestamp of the first revision on this page.
+ * hights is the timestamp of the last revision.
+ */
+
+ $lowts = $hights = 0;
+
+ if( count( $revisions ) ) {
+ $latestShown = wfTimestamp(TS_MW, $revisions[0]->rev_timestamp);
+ $earliestShown = wfTimestamp(TS_MW, $revisions[count($revisions) - 1]->rev_timestamp);
+ } else {
+ $latestShown = null;
+ $earliestShown = null;
+ }
+
+ /* Don't announce the limit everywhere if it's the default */
+ $usefulLimit = $limit == $this->defaultLimit ? '' : $limit;
+
+ $urls = array();
+ foreach (array(20, 50, 100, 250, 500) as $num) {
+ $urls[] = $this->MakeLink( $wgLang->formatNum($num),
+ array('offset' => $offset == 0 ? '' : wfTimestamp(TS_MW, $offset), 'limit' => $num, ) );
+ }
+
+ $bits = implode($urls, ' | ');
+
+ wfDebug("latestShown=$latestShown latestTimestamp=$latestTimestamp\n");
+ if( $latestShown < $latestTimestamp ) {
+ $prevtext = $this->MakeLink( wfMsgHtml("prevn", $limit),
+ array( 'dir' => 'prev', 'offset' => $latestShown, 'limit' => $usefulLimit ) );
+ $lasttext = $this->MakeLink( wfMsgHtml('histlast'),
+ array( 'limit' => $usefulLimit ) );
+ } else {
+ $prevtext = wfMsgHtml("prevn", $limit);
+ $lasttext = wfMsgHtml('histlast');
+ }
+
+ wfDebug("earliestShown=$earliestShown earliestTimestamp=$earliestTimestamp\n");
+ if( $earliestShown > $earliestTimestamp ) {
+ $nexttext = $this->MakeLink( wfMsgHtml("nextn", $limit),
+ array( 'offset' => $earliestShown, 'limit' => $usefulLimit ) );
+ $firsttext = $this->MakeLink( wfMsgHtml('histfirst'),
+ array( 'go' => 'first', 'limit' => $usefulLimit ) );
+ } else {
+ $nexttext = wfMsgHtml("nextn", $limit);
+ $firsttext = wfMsgHtml('histfirst');
+ }
+
+ $firstlast = "($lasttext | $firsttext)";
+
+ return "$firstlast " . wfMsgHtml("viewprevnext", $prevtext, $nexttext, $bits);
+ }
+
+ function MakeLink($text, $query = NULL) {
+ if ( $query === null ) return $text;
+ return $this->mSkin->makeKnownLinkObj(
+ $this->mTitle, $text,
+ wfArrayToCGI( $query, array( 'action' => 'history' )));
+ }
+
+
+ /**
+ * Output a subscription feed listing recent edits to this page.
+ * @param string $type
+ */
+ function feed( $type ) {
+ require_once 'SpecialRecentchanges.php';
+
+ global $wgFeedClasses;
+ if( !isset( $wgFeedClasses[$type] ) ) {
+ global $wgOut;
+ $wgOut->addWikiText( wfMsg( 'feed-invalid' ) );
+ return;
+ }
+
+ $feed = new $wgFeedClasses[$type](
+ $this->mTitle->getPrefixedText() . ' - ' .
+ wfMsgForContent( 'history-feed-title' ),
+ wfMsgForContent( 'history-feed-description' ),
+ $this->mTitle->getFullUrl( 'action=history' ) );
+
+ $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT);
+ $feed->outHeader();
+ if( $items ) {
+ foreach( $items as $row ) {
+ $feed->outItem( $this->feedItem( $row ) );
+ }
+ } else {
+ $feed->outItem( $this->feedEmpty() );
+ }
+ $feed->outFooter();
+ }
+
+ function feedEmpty() {
+ global $wgOut;
+ return new FeedItem(
+ wfMsgForContent( 'nohistory' ),
+ $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ),
+ $this->mTitle->getFullUrl(),
+ wfTimestamp( TS_MW ),
+ '',
+ $this->mTitle->getTalkPage()->getFullUrl() );
+ }
+
+ /**
+ * Generate a FeedItem object from a given revision table row
+ * Borrows Recent Changes' feed generation functions for formatting;
+ * includes a diff to the previous revision (if any).
+ *
+ * @param $row
+ * @return FeedItem
+ */
+ function feedItem( $row ) {
+ $rev = new Revision( $row );
+ $rev->setTitle( $this->mTitle );
+ $text = rcFormatDiffRow( $this->mTitle,
+ $this->mTitle->getPreviousRevisionID( $rev->getId() ),
+ $rev->getId(),
+ $rev->getTimestamp(),
+ $rev->getComment() );
+
+ if( $rev->getComment() == '' ) {
+ global $wgContLang;
+ $title = wfMsgForContent( 'history-feed-item-nocomment',
+ $rev->getUserText(),
+ $wgContLang->timeanddate( $rev->getTimestamp() ) );
+ } else {
+ $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() );
+ }
+
+ return new FeedItem(
+ $title,
+ $text,
+ $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ),
+ $rev->getTimestamp(),
+ $rev->getUserText(),
+ $this->mTitle->getTalkPage()->getFullUrl() );
+ }
+
+ /**
+ * Quickie hack... strip out wikilinks to more legible form from the comment.
+ */
+ function stripComment( $text ) {
+ return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
+ }
+
+
+}
+
+?>
diff --git a/includes/Parser.php b/includes/Parser.php
new file mode 100644
index 00000000..31976baf
--- /dev/null
+++ b/includes/Parser.php
@@ -0,0 +1,4727 @@
+<?php
+/**
+ * File for Parser and related classes
+ *
+ * @package MediaWiki
+ * @subpackage Parser
+ */
+
+/**
+ * Update this version number when the ParserOutput format
+ * changes in an incompatible way, so the parser cache
+ * can automatically discard old data.
+ */
+define( 'MW_PARSER_VERSION', '1.6.1' );
+
+/**
+ * Variable substitution O(N^2) attack
+ *
+ * Without countermeasures, it would be possible to attack the parser by saving
+ * a page filled with a large number of inclusions of large pages. The size of
+ * the generated page would be proportional to the square of the input size.
+ * Hence, we limit the number of inclusions of any given page, thus bringing any
+ * attack back to O(N).
+ */
+
+define( 'MAX_INCLUDE_REPEAT', 100 );
+define( 'MAX_INCLUDE_SIZE', 1000000 ); // 1 Million
+
+define( 'RLH_FOR_UPDATE', 1 );
+
+# Allowed values for $mOutputType
+define( 'OT_HTML', 1 );
+define( 'OT_WIKI', 2 );
+define( 'OT_MSG' , 3 );
+
+# Flags for setFunctionHook
+define( 'SFH_NO_HASH', 1 );
+
+# string parameter for extractTags which will cause it
+# to strip HTML comments in addition to regular
+# <XML>-style tags. This should not be anything we
+# may want to use in wikisyntax
+define( 'STRIP_COMMENTS', 'HTMLCommentStrip' );
+
+# Constants needed for external link processing
+define( 'HTTP_PROTOCOLS', 'http:\/\/|https:\/\/' );
+# Everything except bracket, space, or control characters
+define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' );
+# Including space, but excluding newlines
+define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' );
+define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' );
+define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' );
+define( 'EXT_LINK_BRACKETED', '/\[(\b(' . wfUrlProtocols() . ')'.
+ EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' );
+define( 'EXT_IMAGE_REGEX',
+ '/^('.HTTP_PROTOCOLS.')'. # Protocol
+ '('.EXT_LINK_URL_CLASS.'+)\\/'. # Hostname and path
+ '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename
+);
+
+// State constants for the definition list colon extraction
+define( 'MW_COLON_STATE_TEXT', 0 );
+define( 'MW_COLON_STATE_TAG', 1 );
+define( 'MW_COLON_STATE_TAGSTART', 2 );
+define( 'MW_COLON_STATE_CLOSETAG', 3 );
+define( 'MW_COLON_STATE_TAGSLASH', 4 );
+define( 'MW_COLON_STATE_COMMENT', 5 );
+define( 'MW_COLON_STATE_COMMENTDASH', 6 );
+define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 );
+
+/**
+ * PHP Parser
+ *
+ * Processes wiki markup
+ *
+ * <pre>
+ * There are three main entry points into the Parser class:
+ * parse()
+ * produces HTML output
+ * preSaveTransform().
+ * produces altered wiki markup.
+ * transformMsg()
+ * performs brace substitution on MediaWiki messages
+ *
+ * Globals used:
+ * objects: $wgLang, $wgContLang
+ *
+ * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
+ *
+ * settings:
+ * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
+ * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
+ * $wgLocaltimezone, $wgAllowSpecialInclusion*
+ *
+ * * only within ParserOptions
+ * </pre>
+ *
+ * @package MediaWiki
+ */
+class Parser
+{
+ /**#@+
+ * @private
+ */
+ # Persistent:
+ var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables;
+
+ # Cleared with clearState():
+ var $mOutput, $mAutonumber, $mDTopen, $mStripState = array();
+ var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+ var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
+ var $mTemplates, // cache of already loaded templates, avoids
+ // multiple SQL queries for the same string
+ $mTemplatePath; // stores an unsorted hash of all the templates already loaded
+ // in this path. Used for loop detection.
+
+ # Temporary
+ # These are variables reset at least once per parse regardless of $clearState
+ var $mOptions, // ParserOptions object
+ $mTitle, // Title context, used for self-link rendering and similar things
+ $mOutputType, // Output type, one of the OT_xxx constants
+ $mRevisionId; // ID to display in {{REVISIONID}} tags
+
+ /**#@-*/
+
+ /**
+ * Constructor
+ *
+ * @public
+ */
+ function Parser() {
+ $this->mTagHooks = array();
+ $this->mFunctionHooks = array();
+ $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+ $this->mFirstCall = true;
+ }
+
+ /**
+ * Do various kinds of initialisation on the first call of the parser
+ */
+ function firstCallInit() {
+ if ( !$this->mFirstCall ) {
+ return;
+ }
+
+ wfProfileIn( __METHOD__ );
+ global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
+
+ $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+
+ $this->setFunctionHook( MAG_NS, array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_URLENCODE, array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_LCFIRST, array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_UCFIRST, array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_LC, array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_UC, array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_LOCALURL, array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_LOCALURLE, array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_FULLURL, array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_FULLURLE, array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_FORMATNUM, array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_GRAMMAR, array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_PLURAL, array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_NUMBEROFPAGES, array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_NUMBEROFUSERS, array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_NUMBEROFARTICLES, array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_NUMBEROFFILES, array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_NUMBEROFADMINS, array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH );
+ $this->setFunctionHook( MAG_LANGUAGE, array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH );
+
+ if ( $wgAllowDisplayTitle ) {
+ $this->setFunctionHook( MAG_DISPLAYTITLE, array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
+ }
+ if ( $wgAllowSlowParserFunctions ) {
+ $this->setFunctionHook( MAG_PAGESINNAMESPACE, array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
+ }
+
+ $this->initialiseVariables();
+
+ $this->mFirstCall = false;
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Clear Parser state
+ *
+ * @private
+ */
+ function clearState() {
+ if ( $this->mFirstCall ) {
+ $this->firstCallInit();
+ }
+ $this->mOutput = new ParserOutput;
+ $this->mAutonumber = 0;
+ $this->mLastSection = '';
+ $this->mDTopen = false;
+ $this->mIncludeCount = array();
+ $this->mStripState = array();
+ $this->mArgStack = array();
+ $this->mInPre = false;
+ $this->mInterwikiLinkHolders = array(
+ 'texts' => array(),
+ 'titles' => array()
+ );
+ $this->mLinkHolders = array(
+ 'namespaces' => array(),
+ 'dbkeys' => array(),
+ 'queries' => array(),
+ 'texts' => array(),
+ 'titles' => array()
+ );
+ $this->mRevisionId = null;
+
+ /**
+ * Prefix for temporary replacement strings for the multipass parser.
+ * \x07 should never appear in input as it's disallowed in XML.
+ * Using it at the front also gives us a little extra robustness
+ * since it shouldn't match when butted up against identifier-like
+ * string constructs.
+ */
+ $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+
+ # Clear these on every parse, bug 4549
+ $this->mTemplates = array();
+ $this->mTemplatePath = array();
+
+ $this->mShowToc = true;
+ $this->mForceTocPosition = false;
+
+ wfRunHooks( 'ParserClearState', array( &$this ) );
+ }
+
+ /**
+ * Accessor for mUniqPrefix.
+ *
+ * @public
+ */
+ function UniqPrefix() {
+ return $this->mUniqPrefix;
+ }
+
+ /**
+ * Convert wikitext to HTML
+ * Do not call this function recursively.
+ *
+ * @private
+ * @param string $text Text we want to parse
+ * @param Title &$title A title object
+ * @param array $options
+ * @param boolean $linestart
+ * @param boolean $clearState
+ * @param int $revid number to pass in {{REVISIONID}}
+ * @return ParserOutput a ParserOutput
+ */
+ function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
+ /**
+ * First pass--just handle <nowiki> sections, pass the rest off
+ * to internalParse() which does all the real work.
+ */
+
+ global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+ $fname = 'Parser::parse';
+ wfProfileIn( $fname );
+
+ if ( $clearState ) {
+ $this->clearState();
+ }
+
+ $this->mOptions = $options;
+ $this->mTitle =& $title;
+ $this->mRevisionId = $revid;
+ $this->mOutputType = OT_HTML;
+
+ //$text = $this->strip( $text, $this->mStripState );
+ // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5.
+ $x =& $this->mStripState;
+
+ wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) );
+ $text = $this->strip( $text, $x );
+ wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) );
+
+ # Hook to suspend the parser in this state
+ if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) {
+ wfProfileOut( $fname );
+ return $text ;
+ }
+
+ $text = $this->internalParse( $text );
+
+ $text = $this->unstrip( $text, $this->mStripState );
+
+ # Clean up special characters, only run once, next-to-last before doBlockLevels
+ $fixtags = array(
+ # french spaces, last one Guillemet-left
+ # only if there is something before the space
+ '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1&nbsp;\\2',
+ # french spaces, Guillemet-right
+ '/(\\302\\253) /' => '\\1&nbsp;',
+ );
+ $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+
+ # only once and last
+ $text = $this->doBlockLevels( $text, $linestart );
+
+ $this->replaceLinkHolders( $text );
+
+ # the position of the parserConvert() call should not be changed. it
+ # assumes that the links are all replaced and the only thing left
+ # is the <nowiki> mark.
+ # Side-effects: this calls $this->mOutput->setTitleText()
+ $text = $wgContLang->parserConvert( $text, $this );
+
+ $text = $this->unstripNoWiki( $text, $this->mStripState );
+
+ wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
+
+ $text = Sanitizer::normalizeCharReferences( $text );
+
+ if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
+ $text = Parser::tidy($text);
+ } else {
+ # attempt to sanitize at least some nesting problems
+ # (bug #2702 and quite a few others)
+ $tidyregs = array(
+ # ''Something [http://www.cool.com cool''] -->
+ # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
+ '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
+ '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
+ # fix up an anchor inside another anchor, only
+ # at least for a single single nested link (bug 3695)
+ '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
+ '\\1\\2</a>\\3</a>\\1\\4</a>',
+ # fix div inside inline elements- doBlockLevels won't wrap a line which
+ # contains a div, so fix it up here; replace
+ # div with escaped text
+ '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
+ '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
+ # remove empty italic or bold tag pairs, some
+ # introduced by rules above
+ '/<([bi])><\/\\1>/' => ''
+ );
+
+ $text = preg_replace(
+ array_keys( $tidyregs ),
+ array_values( $tidyregs ),
+ $text );
+ }
+
+ wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
+
+ $this->mOutput->setText( $text );
+ wfProfileOut( $fname );
+
+ return $this->mOutput;
+ }
+
+ /**
+ * Get a random string
+ *
+ * @private
+ * @static
+ */
+ function getRandomString() {
+ return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+ }
+
+ function &getTitle() { return $this->mTitle; }
+ function getOptions() { return $this->mOptions; }
+
+ function getFunctionLang() {
+ global $wgLang, $wgContLang;
+ return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+ }
+
+ /**
+ * Replaces all occurrences of HTML-style comments and the given tags
+ * in the text with a random marker and returns teh next text. The output
+ * parameter $matches will be an associative array filled with data in
+ * the form:
+ * 'UNIQ-xxxxx' => array(
+ * 'element',
+ * 'tag content',
+ * array( 'param' => 'x' ),
+ * '<element param="x">tag content</element>' ) )
+ *
+ * @param $elements list of element names. Comments are always extracted.
+ * @param $text Source text string.
+ * @param $uniq_prefix
+ *
+ * @private
+ * @static
+ */
+ function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+ $rand = Parser::getRandomString();
+ $n = 1;
+ $stripped = '';
+ $matches = array();
+
+ $taglist = implode( '|', $elements );
+ $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+
+ while ( '' != $text ) {
+ $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
+ $stripped .= $p[0];
+ if( count( $p ) < 5 ) {
+ break;
+ }
+ if( count( $p ) > 5 ) {
+ // comment
+ $element = $p[4];
+ $attributes = '';
+ $close = '';
+ $inside = $p[5];
+ } else {
+ // tag
+ $element = $p[1];
+ $attributes = $p[2];
+ $close = $p[3];
+ $inside = $p[4];
+ }
+
+ $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++) . '-QINU';
+ $stripped .= $marker;
+
+ if ( $close === '/>' ) {
+ // Empty element tag, <tag />
+ $content = null;
+ $text = $inside;
+ $tail = null;
+ } else {
+ if( $element == '!--' ) {
+ $end = '/(-->)/';
+ } else {
+ $end = "/(<\\/$element\\s*>)/i";
+ }
+ $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
+ $content = $q[0];
+ if( count( $q ) < 3 ) {
+ # No end tag -- let it run out to the end of the text.
+ $tail = '';
+ $text = '';
+ } else {
+ $tail = $q[1];
+ $text = $q[2];
+ }
+ }
+
+ $matches[$marker] = array( $element,
+ $content,
+ Sanitizer::decodeTagAttributes( $attributes ),
+ "<$element$attributes$close$content$tail" );
+ }
+ return $stripped;
+ }
+
+ /**
+ * Strips and renders nowiki, pre, math, hiero
+ * If $render is set, performs necessary rendering operations on plugins
+ * Returns the text, and fills an array with data needed in unstrip()
+ * If the $state is already a valid strip state, it adds to the state
+ *
+ * @param bool $stripcomments when set, HTML comments <!-- like this -->
+ * will be stripped in addition to other tags. This is important
+ * for section editing, where these comments cause confusion when
+ * counting the sections in the wikisource
+ *
+ * @param array dontstrip contains tags which should not be stripped;
+ * used to prevent stipping of <gallery> when saving (fixes bug 2700)
+ *
+ * @private
+ */
+ function strip( $text, &$state, $stripcomments = false , $dontstrip = array () ) {
+ $render = ($this->mOutputType == OT_HTML);
+
+ # Replace any instances of the placeholders
+ $uniq_prefix = $this->mUniqPrefix;
+ #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
+ $commentState = array();
+
+ $elements = array_merge(
+ array( 'nowiki', 'gallery' ),
+ array_keys( $this->mTagHooks ) );
+ global $wgRawHtml;
+ if( $wgRawHtml ) {
+ $elements[] = 'html';
+ }
+ if( $this->mOptions->getUseTeX() ) {
+ $elements[] = 'math';
+ }
+
+ # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
+ foreach ( $elements AS $k => $v ) {
+ if ( !in_array ( $v , $dontstrip ) ) continue;
+ unset ( $elements[$k] );
+ }
+
+ $matches = array();
+ $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+ foreach( $matches as $marker => $data ) {
+ list( $element, $content, $params, $tag ) = $data;
+ if( $render ) {
+ $tagName = strtolower( $element );
+ switch( $tagName ) {
+ case '!--':
+ // Comment
+ if( substr( $tag, -3 ) == '-->' ) {
+ $output = $tag;
+ } else {
+ // Unclosed comment in input.
+ // Close it so later stripping can remove it
+ $output = "$tag-->";
+ }
+ break;
+ case 'html':
+ if( $wgRawHtml ) {
+ $output = $content;
+ break;
+ }
+ // Shouldn't happen otherwise. :)
+ case 'nowiki':
+ $output = wfEscapeHTMLTagsOnly( $content );
+ break;
+ case 'math':
+ $output = MathRenderer::renderMath( $content );
+ break;
+ case 'gallery':
+ $output = $this->renderImageGallery( $content, $params );
+ break;
+ default:
+ if( isset( $this->mTagHooks[$tagName] ) ) {
+ $output = call_user_func_array( $this->mTagHooks[$tagName],
+ array( $content, $params, $this ) );
+ } else {
+ throw new MWException( "Invalid call hook $element" );
+ }
+ }
+ } else {
+ // Just stripping tags; keep the source
+ $output = $tag;
+ }
+ if( !$stripcomments && $element == '!--' ) {
+ $commentState[$marker] = $output;
+ } else {
+ $state[$element][$marker] = $output;
+ }
+ }
+
+ # Unstrip comments unless explicitly told otherwise.
+ # (The comments are always stripped prior to this point, so as to
+ # not invoke any extension tags / parser hooks contained within
+ # a comment.)
+ if ( !$stripcomments ) {
+ // Put them all back and forget them
+ $text = strtr( $text, $commentState );
+ }
+
+ return $text;
+ }
+
+ /**
+ * Restores pre, math, and other extensions removed by strip()
+ *
+ * always call unstripNoWiki() after this one
+ * @private
+ */
+ function unstrip( $text, &$state ) {
+ if ( !is_array( $state ) ) {
+ return $text;
+ }
+
+ $replacements = array();
+ foreach( $state as $tag => $contentDict ) {
+ if( $tag != 'nowiki' && $tag != 'html' ) {
+ foreach( $contentDict as $uniq => $content ) {
+ $replacements[$uniq] = $content;
+ }
+ }
+ }
+ $text = strtr( $text, $replacements );
+
+ return $text;
+ }
+
+ /**
+ * Always call this after unstrip() to preserve the order
+ *
+ * @private
+ */
+ function unstripNoWiki( $text, &$state ) {
+ if ( !is_array( $state ) ) {
+ return $text;
+ }
+
+ $replacements = array();
+ foreach( $state as $tag => $contentDict ) {
+ if( $tag == 'nowiki' || $tag == 'html' ) {
+ foreach( $contentDict as $uniq => $content ) {
+ $replacements[$uniq] = $content;
+ }
+ }
+ }
+ $text = strtr( $text, $replacements );
+
+ return $text;
+ }
+
+ /**
+ * Add an item to the strip state
+ * Returns the unique tag which must be inserted into the stripped text
+ * The tag will be replaced with the original text in unstrip()
+ *
+ * @private
+ */
+ function insertStripItem( $text, &$state ) {
+ $rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString();
+ if ( !$state ) {
+ $state = array();
+ }
+ $state['item'][$rnd] = $text;
+ return $rnd;
+ }
+
+ /**
+ * Interface with html tidy, used if $wgUseTidy = true.
+ * If tidy isn't able to correct the markup, the original will be
+ * returned in all its glory with a warning comment appended.
+ *
+ * Either the external tidy program or the in-process tidy extension
+ * will be used depending on availability. Override the default
+ * $wgTidyInternal setting to disable the internal if it's not working.
+ *
+ * @param string $text Hideous HTML input
+ * @return string Corrected HTML output
+ * @public
+ * @static
+ */
+ function tidy( $text ) {
+ global $wgTidyInternal;
+ $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
+' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
+'<head><title>test</title></head><body>'.$text.'</body></html>';
+ if( $wgTidyInternal ) {
+ $correctedtext = Parser::internalTidy( $wrappedtext );
+ } else {
+ $correctedtext = Parser::externalTidy( $wrappedtext );
+ }
+ if( is_null( $correctedtext ) ) {
+ wfDebug( "Tidy error detected!\n" );
+ return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
+ }
+ return $correctedtext;
+ }
+
+ /**
+ * Spawn an external HTML tidy process and get corrected markup back from it.
+ *
+ * @private
+ * @static
+ */
+ function externalTidy( $text ) {
+ global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
+ $fname = 'Parser::externalTidy';
+ wfProfileIn( $fname );
+
+ $cleansource = '';
+ $opts = ' -utf8';
+
+ $descriptorspec = array(
+ 0 => array('pipe', 'r'),
+ 1 => array('pipe', 'w'),
+ 2 => array('file', '/dev/null', 'a')
+ );
+ $pipes = array();
+ $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
+ if (is_resource($process)) {
+ // Theoretically, this style of communication could cause a deadlock
+ // here. If the stdout buffer fills up, then writes to stdin could
+ // block. This doesn't appear to happen with tidy, because tidy only
+ // writes to stdout after it's finished reading from stdin. Search
+ // for tidyParseStdin and tidySaveStdout in console/tidy.c
+ fwrite($pipes[0], $text);
+ fclose($pipes[0]);
+ while (!feof($pipes[1])) {
+ $cleansource .= fgets($pipes[1], 1024);
+ }
+ fclose($pipes[1]);
+ proc_close($process);
+ }
+
+ wfProfileOut( $fname );
+
+ if( $cleansource == '' && $text != '') {
+ // Some kind of error happened, so we couldn't get the corrected text.
+ // Just give up; we'll use the source text and append a warning.
+ return null;
+ } else {
+ return $cleansource;
+ }
+ }
+
+ /**
+ * Use the HTML tidy PECL extension to use the tidy library in-process,
+ * saving the overhead of spawning a new process. Currently written to
+ * the PHP 4.3.x version of the extension, may not work on PHP 5.
+ *
+ * 'pear install tidy' should be able to compile the extension module.
+ *
+ * @private
+ * @static
+ */
+ function internalTidy( $text ) {
+ global $wgTidyConf;
+ $fname = 'Parser::internalTidy';
+ wfProfileIn( $fname );
+
+ tidy_load_config( $wgTidyConf );
+ tidy_set_encoding( 'utf8' );
+ tidy_parse_string( $text );
+ tidy_clean_repair();
+ if( tidy_get_status() == 2 ) {
+ // 2 is magic number for fatal error
+ // http://www.php.net/manual/en/function.tidy-get-status.php
+ $cleansource = null;
+ } else {
+ $cleansource = tidy_get_output();
+ }
+ wfProfileOut( $fname );
+ return $cleansource;
+ }
+
+ /**
+ * parse the wiki syntax used to render tables
+ *
+ * @private
+ */
+ function doTableStuff ( $t ) {
+ $fname = 'Parser::doTableStuff';
+ wfProfileIn( $fname );
+
+ $t = explode ( "\n" , $t ) ;
+ $td = array () ; # Is currently a td tag open?
+ $ltd = array () ; # Was it TD or TH?
+ $tr = array () ; # Is currently a tr tag open?
+ $ltr = array () ; # tr attributes
+ $has_opened_tr = array(); # Did this table open a <tr> element?
+ $indent_level = 0; # indent level of the table
+ foreach ( $t AS $k => $x )
+ {
+ $x = trim ( $x ) ;
+ $fc = substr ( $x , 0 , 1 ) ;
+ if ( preg_match( '/^(:*)\{\|(.*)$/', $x, $matches ) ) {
+ $indent_level = strlen( $matches[1] );
+
+ $attributes = $this->unstripForHTML( $matches[2] );
+
+ $t[$k] = str_repeat( '<dl><dd>', $indent_level ) .
+ '<table' . Sanitizer::fixTagAttributes ( $attributes, 'table' ) . '>' ;
+ array_push ( $td , false ) ;
+ array_push ( $ltd , '' ) ;
+ array_push ( $tr , false ) ;
+ array_push ( $ltr , '' ) ;
+ array_push ( $has_opened_tr, false );
+ }
+ else if ( count ( $td ) == 0 ) { } # Don't do any of the following
+ else if ( '|}' == substr ( $x , 0 , 2 ) ) {
+ $z = "</table>" . substr ( $x , 2);
+ $l = array_pop ( $ltd ) ;
+ if ( !array_pop ( $has_opened_tr ) ) $z = "<tr><td></td></tr>" . $z ;
+ if ( array_pop ( $tr ) ) $z = '</tr>' . $z ;
+ if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
+ array_pop ( $ltr ) ;
+ $t[$k] = $z . str_repeat( '</dd></dl>', $indent_level );
+ }
+ else if ( '|-' == substr ( $x , 0 , 2 ) ) { # Allows for |---------------
+ $x = substr ( $x , 1 ) ;
+ while ( $x != '' && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
+ $z = '' ;
+ $l = array_pop ( $ltd ) ;
+ array_pop ( $has_opened_tr );
+ array_push ( $has_opened_tr , true ) ;
+ if ( array_pop ( $tr ) ) $z = '</tr>' . $z ;
+ if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
+ array_pop ( $ltr ) ;
+ $t[$k] = $z ;
+ array_push ( $tr , false ) ;
+ array_push ( $td , false ) ;
+ array_push ( $ltd , '' ) ;
+ $attributes = $this->unstripForHTML( $x );
+ array_push ( $ltr , Sanitizer::fixTagAttributes ( $attributes, 'tr' ) ) ;
+ }
+ else if ( '|' == $fc || '!' == $fc || '|+' == substr ( $x , 0 , 2 ) ) { # Caption
+ # $x is a table row
+ if ( '|+' == substr ( $x , 0 , 2 ) ) {
+ $fc = '+' ;
+ $x = substr ( $x , 1 ) ;
+ }
+ $after = substr ( $x , 1 ) ;
+ if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ;
+
+ // Split up multiple cells on the same line.
+ // FIXME: This can result in improper nesting of tags processed
+ // by earlier parser steps, but should avoid splitting up eg
+ // attribute values containing literal "||".
+ $after = wfExplodeMarkup( '||', $after );
+
+ $t[$k] = '' ;
+
+ # Loop through each table cell
+ foreach ( $after AS $theline )
+ {
+ $z = '' ;
+ if ( $fc != '+' )
+ {
+ $tra = array_pop ( $ltr ) ;
+ if ( !array_pop ( $tr ) ) $z = '<tr'.$tra.">\n" ;
+ array_push ( $tr , true ) ;
+ array_push ( $ltr , '' ) ;
+ array_pop ( $has_opened_tr );
+ array_push ( $has_opened_tr , true ) ;
+ }
+
+ $l = array_pop ( $ltd ) ;
+ if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
+ if ( $fc == '|' ) $l = 'td' ;
+ else if ( $fc == '!' ) $l = 'th' ;
+ else if ( $fc == '+' ) $l = 'caption' ;
+ else $l = '' ;
+ array_push ( $ltd , $l ) ;
+
+ # Cell parameters
+ $y = explode ( '|' , $theline , 2 ) ;
+ # Note that a '|' inside an invalid link should not
+ # be mistaken as delimiting cell parameters
+ if ( strpos( $y[0], '[[' ) !== false ) {
+ $y = array ($theline);
+ }
+ if ( count ( $y ) == 1 )
+ $y = "{$z}<{$l}>{$y[0]}" ;
+ else {
+ $attributes = $this->unstripForHTML( $y[0] );
+ $y = "{$z}<{$l}".Sanitizer::fixTagAttributes($attributes, $l).">{$y[1]}" ;
+ }
+ $t[$k] .= $y ;
+ array_push ( $td , true ) ;
+ }
+ }
+ }
+
+ # Closing open td, tr && table
+ while ( count ( $td ) > 0 )
+ {
+ $l = array_pop ( $ltd ) ;
+ if ( array_pop ( $td ) ) $t[] = '</td>' ;
+ if ( array_pop ( $tr ) ) $t[] = '</tr>' ;
+ if ( !array_pop ( $has_opened_tr ) ) $t[] = "<tr><td></td></tr>" ;
+ $t[] = '</table>' ;
+ }
+
+ $t = implode ( "\n" , $t ) ;
+ # special case: don't return empty table
+ if($t == "<table>\n<tr><td></td></tr>\n</table>")
+ $t = '';
+ wfProfileOut( $fname );
+ return $t ;
+ }
+
+ /**
+ * Helper function for parse() that transforms wiki markup into
+ * HTML. Only called for $mOutputType == OT_HTML.
+ *
+ * @private
+ */
+ function internalParse( $text ) {
+ $args = array();
+ $isMain = true;
+ $fname = 'Parser::internalParse';
+ wfProfileIn( $fname );
+
+ # Remove <noinclude> tags and <includeonly> sections
+ $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) );
+ $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
+ $text = preg_replace( '/<includeonly>.*?<\/includeonly>/s', '', $text );
+
+ $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) );
+
+ $text = $this->replaceVariables( $text, $args );
+
+ // Tables need to come after variable replacement for things to work
+ // properly; putting them before other transformations should keep
+ // exciting things like link expansions from showing up in surprising
+ // places.
+ $text = $this->doTableStuff( $text );
+
+ $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
+
+ $text = $this->stripToc( $text );
+ $this->stripNoGallery( $text );
+ $text = $this->doHeadings( $text );
+ if($this->mOptions->getUseDynamicDates()) {
+ $df =& DateFormatter::getInstance();
+ $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
+ }
+ $text = $this->doAllQuotes( $text );
+ $text = $this->replaceInternalLinks( $text );
+ $text = $this->replaceExternalLinks( $text );
+
+ # replaceInternalLinks may sometimes leave behind
+ # absolute URLs, which have to be masked to hide them from replaceExternalLinks
+ $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
+
+ $text = $this->doMagicLinks( $text );
+ $text = $this->formatHeadings( $text, $isMain );
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Replace special strings like "ISBN xxx" and "RFC xxx" with
+ * magic external links.
+ *
+ * @private
+ */
+ function &doMagicLinks( &$text ) {
+ $text = $this->magicISBN( $text );
+ $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' );
+ $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' );
+ return $text;
+ }
+
+ /**
+ * Parse headers and return html
+ *
+ * @private
+ */
+ function doHeadings( $text ) {
+ $fname = 'Parser::doHeadings';
+ wfProfileIn( $fname );
+ for ( $i = 6; $i >= 1; --$i ) {
+ $h = str_repeat( '=', $i );
+ $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
+ "<h{$i}>\\1</h{$i}>\\2", $text );
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Replace single quotes with HTML markup
+ * @private
+ * @return string the altered text
+ */
+ function doAllQuotes( $text ) {
+ $fname = 'Parser::doAllQuotes';
+ wfProfileIn( $fname );
+ $outtext = '';
+ $lines = explode( "\n", $text );
+ foreach ( $lines as $line ) {
+ $outtext .= $this->doQuotes ( $line ) . "\n";
+ }
+ $outtext = substr($outtext, 0,-1);
+ wfProfileOut( $fname );
+ return $outtext;
+ }
+
+ /**
+ * Helper function for doAllQuotes()
+ * @private
+ */
+ function doQuotes( $text ) {
+ $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+ if ( count( $arr ) == 1 )
+ return $text;
+ else
+ {
+ # First, do some preliminary work. This may shift some apostrophes from
+ # being mark-up to being text. It also counts the number of occurrences
+ # of bold and italics mark-ups.
+ $i = 0;
+ $numbold = 0;
+ $numitalics = 0;
+ foreach ( $arr as $r )
+ {
+ if ( ( $i % 2 ) == 1 )
+ {
+ # If there are ever four apostrophes, assume the first is supposed to
+ # be text, and the remaining three constitute mark-up for bold text.
+ if ( strlen( $arr[$i] ) == 4 )
+ {
+ $arr[$i-1] .= "'";
+ $arr[$i] = "'''";
+ }
+ # If there are more than 5 apostrophes in a row, assume they're all
+ # text except for the last 5.
+ else if ( strlen( $arr[$i] ) > 5 )
+ {
+ $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
+ $arr[$i] = "'''''";
+ }
+ # Count the number of occurrences of bold and italics mark-ups.
+ # We are not counting sequences of five apostrophes.
+ if ( strlen( $arr[$i] ) == 2 ) $numitalics++; else
+ if ( strlen( $arr[$i] ) == 3 ) $numbold++; else
+ if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
+ }
+ $i++;
+ }
+
+ # If there is an odd number of both bold and italics, it is likely
+ # that one of the bold ones was meant to be an apostrophe followed
+ # by italics. Which one we cannot know for certain, but it is more
+ # likely to be one that has a single-letter word before it.
+ if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
+ {
+ $i = 0;
+ $firstsingleletterword = -1;
+ $firstmultiletterword = -1;
+ $firstspace = -1;
+ foreach ( $arr as $r )
+ {
+ if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
+ {
+ $x1 = substr ($arr[$i-1], -1);
+ $x2 = substr ($arr[$i-1], -2, 1);
+ if ($x1 == ' ') {
+ if ($firstspace == -1) $firstspace = $i;
+ } else if ($x2 == ' ') {
+ if ($firstsingleletterword == -1) $firstsingleletterword = $i;
+ } else {
+ if ($firstmultiletterword == -1) $firstmultiletterword = $i;
+ }
+ }
+ $i++;
+ }
+
+ # If there is a single-letter word, use it!
+ if ($firstsingleletterword > -1)
+ {
+ $arr [ $firstsingleletterword ] = "''";
+ $arr [ $firstsingleletterword-1 ] .= "'";
+ }
+ # If not, but there's a multi-letter word, use that one.
+ else if ($firstmultiletterword > -1)
+ {
+ $arr [ $firstmultiletterword ] = "''";
+ $arr [ $firstmultiletterword-1 ] .= "'";
+ }
+ # ... otherwise use the first one that has neither.
+ # (notice that it is possible for all three to be -1 if, for example,
+ # there is only one pentuple-apostrophe in the line)
+ else if ($firstspace > -1)
+ {
+ $arr [ $firstspace ] = "''";
+ $arr [ $firstspace-1 ] .= "'";
+ }
+ }
+
+ # Now let's actually convert our apostrophic mush to HTML!
+ $output = '';
+ $buffer = '';
+ $state = '';
+ $i = 0;
+ foreach ($arr as $r)
+ {
+ if (($i % 2) == 0)
+ {
+ if ($state == 'both')
+ $buffer .= $r;
+ else
+ $output .= $r;
+ }
+ else
+ {
+ if (strlen ($r) == 2)
+ {
+ if ($state == 'i')
+ { $output .= '</i>'; $state = ''; }
+ else if ($state == 'bi')
+ { $output .= '</i>'; $state = 'b'; }
+ else if ($state == 'ib')
+ { $output .= '</b></i><b>'; $state = 'b'; }
+ else if ($state == 'both')
+ { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
+ else # $state can be 'b' or ''
+ { $output .= '<i>'; $state .= 'i'; }
+ }
+ else if (strlen ($r) == 3)
+ {
+ if ($state == 'b')
+ { $output .= '</b>'; $state = ''; }
+ else if ($state == 'bi')
+ { $output .= '</i></b><i>'; $state = 'i'; }
+ else if ($state == 'ib')
+ { $output .= '</b>'; $state = 'i'; }
+ else if ($state == 'both')
+ { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
+ else # $state can be 'i' or ''
+ { $output .= '<b>'; $state .= 'b'; }
+ }
+ else if (strlen ($r) == 5)
+ {
+ if ($state == 'b')
+ { $output .= '</b><i>'; $state = 'i'; }
+ else if ($state == 'i')
+ { $output .= '</i><b>'; $state = 'b'; }
+ else if ($state == 'bi')
+ { $output .= '</i></b>'; $state = ''; }
+ else if ($state == 'ib')
+ { $output .= '</b></i>'; $state = ''; }
+ else if ($state == 'both')
+ { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
+ else # ($state == '')
+ { $buffer = ''; $state = 'both'; }
+ }
+ }
+ $i++;
+ }
+ # Now close all remaining tags. Notice that the order is important.
+ if ($state == 'b' || $state == 'ib')
+ $output .= '</b>';
+ if ($state == 'i' || $state == 'bi' || $state == 'ib')
+ $output .= '</i>';
+ if ($state == 'bi')
+ $output .= '</b>';
+ if ($state == 'both')
+ $output .= '<b><i>'.$buffer.'</i></b>';
+ return $output;
+ }
+ }
+
+ /**
+ * Replace external links
+ *
+ * Note: this is all very hackish and the order of execution matters a lot.
+ * Make sure to run maintenance/parserTests.php if you change this code.
+ *
+ * @private
+ */
+ function replaceExternalLinks( $text ) {
+ global $wgContLang;
+ $fname = 'Parser::replaceExternalLinks';
+ wfProfileIn( $fname );
+
+ $sk =& $this->mOptions->getSkin();
+
+ $bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+
+ $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
+
+ $i = 0;
+ while ( $i<count( $bits ) ) {
+ $url = $bits[$i++];
+ $protocol = $bits[$i++];
+ $text = $bits[$i++];
+ $trail = $bits[$i++];
+
+ # The characters '<' and '>' (which were escaped by
+ # removeHTMLtags()) should not be included in
+ # URLs, per RFC 2396.
+ if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+ $text = substr($url, $m2[0][1]) . ' ' . $text;
+ $url = substr($url, 0, $m2[0][1]);
+ }
+
+ # If the link text is an image URL, replace it with an <img> tag
+ # This happened by accident in the original parser, but some people used it extensively
+ $img = $this->maybeMakeExternalImage( $text );
+ if ( $img !== false ) {
+ $text = $img;
+ }
+
+ $dtrail = '';
+
+ # Set linktype for CSS - if URL==text, link is essentially free
+ $linktype = ($text == $url) ? 'free' : 'text';
+
+ # No link text, e.g. [http://domain.tld/some.link]
+ if ( $text == '' ) {
+ # Autonumber if allowed. See bug #5918
+ if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
+ $text = '[' . ++$this->mAutonumber . ']';
+ $linktype = 'autonumber';
+ } else {
+ # Otherwise just use the URL
+ $text = htmlspecialchars( $url );
+ $linktype = 'free';
+ }
+ } else {
+ # Have link text, e.g. [http://domain.tld/some.link text]s
+ # Check for trail
+ list( $dtrail, $trail ) = Linker::splitTrail( $trail );
+ }
+
+ $text = $wgContLang->markNoConversion($text);
+
+ # Normalize any HTML entities in input. They will be
+ # re-escaped by makeExternalLink().
+ $url = Sanitizer::decodeCharReferences( $url );
+
+ # Escape any control characters introduced by the above step
+ $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url );
+
+ # Process the trail (i.e. everything after this link up until start of the next link),
+ # replacing any non-bracketed links
+ $trail = $this->replaceFreeExternalLinks( $trail );
+
+ # Use the encoded URL
+ # This means that users can paste URLs directly into the text
+ # Funny characters like &ouml; aren't valid in URLs anyway
+ # This was changed in August 2004
+ $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+
+ # Register link in the output object.
+ # Replace unnecessary URL escape codes with the referenced character
+ # This prevents spammers from hiding links from the filters
+ $pasteurized = Parser::replaceUnusualEscapes( $url );
+ $this->mOutput->addExternalLink( $pasteurized );
+ }
+
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /**
+ * Replace anything that looks like a URL with a link
+ * @private
+ */
+ function replaceFreeExternalLinks( $text ) {
+ global $wgContLang;
+ $fname = 'Parser::replaceFreeExternalLinks';
+ wfProfileIn( $fname );
+
+ $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+ $s = array_shift( $bits );
+ $i = 0;
+
+ $sk =& $this->mOptions->getSkin();
+
+ while ( $i < count( $bits ) ){
+ $protocol = $bits[$i++];
+ $remainder = $bits[$i++];
+
+ if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
+ # Found some characters after the protocol that look promising
+ $url = $protocol . $m[1];
+ $trail = $m[2];
+
+ # special case: handle urls as url args:
+ # http://www.example.com/foo?=http://www.example.com/bar
+ if(strlen($trail) == 0 &&
+ isset($bits[$i]) &&
+ preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
+ preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
+ {
+ # add protocol, arg
+ $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
+ $i += 2;
+ $trail = $m[2];
+ }
+
+ # The characters '<' and '>' (which were escaped by
+ # removeHTMLtags()) should not be included in
+ # URLs, per RFC 2396.
+ if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+ $trail = substr($url, $m2[0][1]) . $trail;
+ $url = substr($url, 0, $m2[0][1]);
+ }
+
+ # Move trailing punctuation to $trail
+ $sep = ',;\.:!?';
+ # If there is no left bracket, then consider right brackets fair game too
+ if ( strpos( $url, '(' ) === false ) {
+ $sep .= ')';
+ }
+
+ $numSepChars = strspn( strrev( $url ), $sep );
+ if ( $numSepChars ) {
+ $trail = substr( $url, -$numSepChars ) . $trail;
+ $url = substr( $url, 0, -$numSepChars );
+ }
+
+ # Normalize any HTML entities in input. They will be
+ # re-escaped by makeExternalLink() or maybeMakeExternalImage()
+ $url = Sanitizer::decodeCharReferences( $url );
+
+ # Escape any control characters introduced by the above step
+ $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url );
+
+ # Is this an external image?
+ $text = $this->maybeMakeExternalImage( $url );
+ if ( $text === false ) {
+ # Not an image, make a link
+ $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+ # Register it in the output object...
+ # Replace unnecessary URL escape codes with their equivalent characters
+ $pasteurized = Parser::replaceUnusualEscapes( $url );
+ $this->mOutput->addExternalLink( $pasteurized );
+ }
+ $s .= $text . $trail;
+ } else {
+ $s .= $protocol . $remainder;
+ }
+ }
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /**
+ * Replace unusual URL escape codes with their equivalent characters
+ * @param string
+ * @return string
+ * @static
+ * @fixme This can merge genuinely required bits in the path or query string,
+ * breaking legit URLs. A proper fix would treat the various parts of
+ * the URL differently; as a workaround, just use the output for
+ * statistical records, not for actual linking/output.
+ */
+ function replaceUnusualEscapes( $url ) {
+ return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
+ array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
+ }
+
+ /**
+ * Callback function used in replaceUnusualEscapes().
+ * Replaces unusual URL escape codes with their equivalent character
+ * @static
+ * @private
+ */
+ function replaceUnusualEscapesCallback( $matches ) {
+ $char = urldecode( $matches[0] );
+ $ord = ord( $char );
+ // Is it an unsafe or HTTP reserved character according to RFC 1738?
+ if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
+ // No, shouldn't be escaped
+ return $char;
+ } else {
+ // Yes, leave it escaped
+ return $matches[0];
+ }
+ }
+
+ /**
+ * make an image if it's allowed, either through the global
+ * option or through the exception
+ * @private
+ */
+ function maybeMakeExternalImage( $url ) {
+ $sk =& $this->mOptions->getSkin();
+ $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
+ $imagesexception = !empty($imagesfrom);
+ $text = false;
+ if ( $this->mOptions->getAllowExternalImages()
+ || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+ if ( preg_match( EXT_IMAGE_REGEX, $url ) ) {
+ # Image found
+ $text = $sk->makeExternalImage( htmlspecialchars( $url ) );
+ }
+ }
+ return $text;
+ }
+
+ /**
+ * Process [[ ]] wikilinks
+ *
+ * @private
+ */
+ function replaceInternalLinks( $s ) {
+ global $wgContLang;
+ static $fname = 'Parser::replaceInternalLinks' ;
+
+ wfProfileIn( $fname );
+
+ wfProfileIn( $fname.'-setup' );
+ static $tc = FALSE;
+ # the % is needed to support urlencoded titles as well
+ if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+
+ $sk =& $this->mOptions->getSkin();
+
+ #split the entire text string on occurences of [[
+ $a = explode( '[[', ' ' . $s );
+ #get the first element (all text up to first [[), and remove the space we added
+ $s = array_shift( $a );
+ $s = substr( $s, 1 );
+
+ # Match a link having the form [[namespace:link|alternate]]trail
+ static $e1 = FALSE;
+ if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
+ # Match cases where there is no "]]", which might still be images
+ static $e1_img = FALSE;
+ if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
+ # Match the end of a line for a word that's not followed by whitespace,
+ # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
+ $e2 = wfMsgForContent( 'linkprefix' );
+
+ $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
+
+ if( is_null( $this->mTitle ) ) {
+ throw new MWException( 'nooo' );
+ }
+ $nottalk = !$this->mTitle->isTalkPage();
+
+ if ( $useLinkPrefixExtension ) {
+ if ( preg_match( $e2, $s, $m ) ) {
+ $first_prefix = $m[2];
+ } else {
+ $first_prefix = false;
+ }
+ } else {
+ $prefix = '';
+ }
+
+ $selflink = $this->mTitle->getPrefixedText();
+ wfProfileOut( $fname.'-setup' );
+
+ $checkVariantLink = sizeof($wgContLang->getVariants())>1;
+ $useSubpages = $this->areSubpagesAllowed();
+
+ # Loop for each link
+ for ($k = 0; isset( $a[$k] ); $k++) {
+ $line = $a[$k];
+ if ( $useLinkPrefixExtension ) {
+ wfProfileIn( $fname.'-prefixhandling' );
+ if ( preg_match( $e2, $s, $m ) ) {
+ $prefix = $m[2];
+ $s = $m[1];
+ } else {
+ $prefix='';
+ }
+ # first link
+ if($first_prefix) {
+ $prefix = $first_prefix;
+ $first_prefix = false;
+ }
+ wfProfileOut( $fname.'-prefixhandling' );
+ }
+
+ $might_be_img = false;
+
+ if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
+ $text = $m[2];
+ # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
+ # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
+ # the real problem is with the $e1 regex
+ # See bug 1300.
+ #
+ # Still some problems for cases where the ] is meant to be outside punctuation,
+ # and no image is in sight. See bug 2095.
+ #
+ if( $text !== '' &&
+ preg_match( "/^\](.*)/s", $m[3], $n ) &&
+ strpos($text, '[') !== false
+ )
+ {
+ $text .= ']'; # so that replaceExternalLinks($text) works later
+ $m[3] = $n[1];
+ }
+ # fix up urlencoded title texts
+ if(preg_match('/%/', $m[1] ))
+ # Should anchors '#' also be rejected?
+ $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
+ $trail = $m[3];
+ } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
+ $might_be_img = true;
+ $text = $m[2];
+ if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]);
+ $trail = "";
+ } else { # Invalid form; output directly
+ $s .= $prefix . '[[' . $line ;
+ continue;
+ }
+
+ # Don't allow internal links to pages containing
+ # PROTO: where PROTO is a valid URL protocol; these
+ # should be external links.
+ if (preg_match('/^(\b(?:' . wfUrlProtocols() . '))/', $m[1])) {
+ $s .= $prefix . '[[' . $line ;
+ continue;
+ }
+
+ # Make subpage if necessary
+ if( $useSubpages ) {
+ $link = $this->maybeDoSubpageLink( $m[1], $text );
+ } else {
+ $link = $m[1];
+ }
+
+ $noforce = (substr($m[1], 0, 1) != ':');
+ if (!$noforce) {
+ # Strip off leading ':'
+ $link = substr($link, 1);
+ }
+
+ $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) );
+ if( !$nt ) {
+ $s .= $prefix . '[[' . $line;
+ continue;
+ }
+
+ #check other language variants of the link
+ #if the article does not exist
+ if( $checkVariantLink
+ && $nt->getArticleID() == 0 ) {
+ $wgContLang->findVariantLink($link, $nt);
+ }
+
+ $ns = $nt->getNamespace();
+ $iw = $nt->getInterWiki();
+
+ if ($might_be_img) { # if this is actually an invalid link
+ if ($ns == NS_IMAGE && $noforce) { #but might be an image
+ $found = false;
+ while (isset ($a[$k+1]) ) {
+ #look at the next 'line' to see if we can close it there
+ $spliced = array_splice( $a, $k + 1, 1 );
+ $next_line = array_shift( $spliced );
+ if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) {
+ # the first ]] closes the inner link, the second the image
+ $found = true;
+ $text .= '[[' . $m[1];
+ $trail = $m[2];
+ break;
+ } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) {
+ #if there's exactly one ]] that's fine, we'll keep looking
+ $text .= '[[' . $m[0];
+ } else {
+ #if $next_line is invalid too, we need look no further
+ $text .= '[[' . $next_line;
+ break;
+ }
+ }
+ if ( !$found ) {
+ # we couldn't find the end of this imageLink, so output it raw
+ #but don't ignore what might be perfectly normal links in the text we've examined
+ $text = $this->replaceInternalLinks($text);
+ $s .= $prefix . '[[' . $link . '|' . $text;
+ # note: no $trail, because without an end, there *is* no trail
+ continue;
+ }
+ } else { #it's not an image, so output it raw
+ $s .= $prefix . '[[' . $link . '|' . $text;
+ # note: no $trail, because without an end, there *is* no trail
+ continue;
+ }
+ }
+
+ $wasblank = ( '' == $text );
+ if( $wasblank ) $text = $link;
+
+
+ # Link not escaped by : , create the various objects
+ if( $noforce ) {
+
+ # Interwikis
+ if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+ $this->mOutput->addLanguageLink( $nt->getFullText() );
+ $s = rtrim($s . "\n");
+ $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+ continue;
+ }
+
+ if ( $ns == NS_IMAGE ) {
+ wfProfileIn( "$fname-image" );
+ if ( !wfIsBadImage( $nt->getDBkey() ) ) {
+ # recursively parse links inside the image caption
+ # actually, this will parse them in any other parameters, too,
+ # but it might be hard to fix that, and it doesn't matter ATM
+ $text = $this->replaceExternalLinks($text);
+ $text = $this->replaceInternalLinks($text);
+
+ # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
+ $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
+ $this->mOutput->addImage( $nt->getDBkey() );
+
+ wfProfileOut( "$fname-image" );
+ continue;
+ } else {
+ # We still need to record the image's presence on the page
+ $this->mOutput->addImage( $nt->getDBkey() );
+ }
+ wfProfileOut( "$fname-image" );
+
+ }
+
+ if ( $ns == NS_CATEGORY ) {
+ wfProfileIn( "$fname-category" );
+ $s = rtrim($s . "\n"); # bug 87
+
+ if ( $wasblank ) {
+ if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
+ $sortkey = $this->mTitle->getText();
+ } else {
+ $sortkey = $this->mTitle->getPrefixedText();
+ }
+ } else {
+ $sortkey = $text;
+ }
+ $sortkey = Sanitizer::decodeCharReferences( $sortkey );
+ $sortkey = str_replace( "\n", '', $sortkey );
+ $sortkey = $wgContLang->convertCategoryKey( $sortkey );
+ $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
+
+ /**
+ * Strip the whitespace Category links produce, see bug 87
+ * @todo We might want to use trim($tmp, "\n") here.
+ */
+ $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+
+ wfProfileOut( "$fname-category" );
+ continue;
+ }
+ }
+
+ if( ( $nt->getPrefixedText() === $selflink ) &&
+ ( $nt->getFragment() === '' ) ) {
+ # Self-links are handled specially; generally de-link and change to bold.
+ $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
+ continue;
+ }
+
+ # Special and Media are pseudo-namespaces; no pages actually exist in them
+ if( $ns == NS_MEDIA ) {
+ $link = $sk->makeMediaLinkObj( $nt, $text );
+ # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
+ $s .= $prefix . $this->armorLinks( $link ) . $trail;
+ $this->mOutput->addImage( $nt->getDBkey() );
+ continue;
+ } elseif( $ns == NS_SPECIAL ) {
+ $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ continue;
+ } elseif( $ns == NS_IMAGE ) {
+ $img = Image::newFromTitle( $nt );
+ if( $img->exists() ) {
+ // Force a blue link if the file exists; may be a remote
+ // upload on the shared repository, and we want to see its
+ // auto-generated page.
+ $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ continue;
+ }
+ }
+ $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+ }
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /**
+ * Make a link placeholder. The text returned can be later resolved to a real link with
+ * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
+ * parsing of interwiki links, and secondly to allow all extistence checks and
+ * article length checks (for stub links) to be bundled into a single query.
+ *
+ */
+ function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ if ( ! is_object($nt) ) {
+ # Fail gracefully
+ $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ } else {
+ # Separate the link trail from the rest of the link
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+ if ( $nt->isExternal() ) {
+ $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
+ $this->mInterwikiLinkHolders['titles'][] = $nt;
+ $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
+ } else {
+ $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
+ $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
+ $this->mLinkHolders['queries'][] = $query;
+ $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
+ $this->mLinkHolders['titles'][] = $nt;
+
+ $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
+ }
+ }
+ return $retVal;
+ }
+
+ /**
+ * Render a forced-blue link inline; protect against double expansion of
+ * URLs if we're in a mode that prepends full URL prefixes to internal links.
+ * Since this little disaster has to split off the trail text to avoid
+ * breaking URLs in the following text without breaking trails on the
+ * wiki links, it's been made into a horrible function.
+ *
+ * @param Title $nt
+ * @param string $text
+ * @param string $query
+ * @param string $trail
+ * @param string $prefix
+ * @return string HTML-wikitext mix oh yuck
+ */
+ function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ $sk =& $this->mOptions->getSkin();
+ $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
+ return $this->armorLinks( $link ) . $trail;
+ }
+
+ /**
+ * Insert a NOPARSE hacky thing into any inline links in a chunk that's
+ * going to go through further parsing steps before inline URL expansion.
+ *
+ * In particular this is important when using action=render, which causes
+ * full URLs to be included.
+ *
+ * Oh man I hate our multi-layer parser!
+ *
+ * @param string more-or-less HTML
+ * @return string less-or-more HTML with NOPARSE bits
+ */
+ function armorLinks( $text ) {
+ return preg_replace( "/\b(" . wfUrlProtocols() . ')/',
+ "{$this->mUniqPrefix}NOPARSE$1", $text );
+ }
+
+ /**
+ * Return true if subpage links should be expanded on this page.
+ * @return bool
+ */
+ function areSubpagesAllowed() {
+ # Some namespaces don't allow subpages
+ global $wgNamespacesWithSubpages;
+ return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]);
+ }
+
+ /**
+ * Handle link to subpage if necessary
+ * @param string $target the source of the link
+ * @param string &$text the link text, modified as necessary
+ * @return string the full name of the link
+ * @private
+ */
+ function maybeDoSubpageLink($target, &$text) {
+ # Valid link forms:
+ # Foobar -- normal
+ # :Foobar -- override special treatment of prefix (images, language links)
+ # /Foobar -- convert to CurrentPage/Foobar
+ # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
+ # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
+ # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
+
+ $fname = 'Parser::maybeDoSubpageLink';
+ wfProfileIn( $fname );
+ $ret = $target; # default return value is no change
+
+ # Some namespaces don't allow subpages,
+ # so only perform processing if subpages are allowed
+ if( $this->areSubpagesAllowed() ) {
+ # Look at the first character
+ if( $target != '' && $target{0} == '/' ) {
+ # / at end means we don't want the slash to be shown
+ if( substr( $target, -1, 1 ) == '/' ) {
+ $target = substr( $target, 1, -1 );
+ $noslash = $target;
+ } else {
+ $noslash = substr( $target, 1 );
+ }
+
+ $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash);
+ if( '' === $text ) {
+ $text = $target;
+ } # this might be changed for ugliness reasons
+ } else {
+ # check for .. subpage backlinks
+ $dotdotcount = 0;
+ $nodotdot = $target;
+ while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
+ ++$dotdotcount;
+ $nodotdot = substr( $nodotdot, 3 );
+ }
+ if($dotdotcount > 0) {
+ $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
+ if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
+ $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
+ # / at the end means don't show full path
+ if( substr( $nodotdot, -1, 1 ) == '/' ) {
+ $nodotdot = substr( $nodotdot, 0, -1 );
+ if( '' === $text ) {
+ $text = $nodotdot;
+ }
+ }
+ $nodotdot = trim( $nodotdot );
+ if( $nodotdot != '' ) {
+ $ret .= '/' . $nodotdot;
+ }
+ }
+ }
+ }
+ }
+
+ wfProfileOut( $fname );
+ return $ret;
+ }
+
+ /**#@+
+ * Used by doBlockLevels()
+ * @private
+ */
+ /* private */ function closeParagraph() {
+ $result = '';
+ if ( '' != $this->mLastSection ) {
+ $result = '</' . $this->mLastSection . ">\n";
+ }
+ $this->mInPre = false;
+ $this->mLastSection = '';
+ return $result;
+ }
+ # getCommon() returns the length of the longest common substring
+ # of both arguments, starting at the beginning of both.
+ #
+ /* private */ function getCommon( $st1, $st2 ) {
+ $fl = strlen( $st1 );
+ $shorter = strlen( $st2 );
+ if ( $fl < $shorter ) { $shorter = $fl; }
+
+ for ( $i = 0; $i < $shorter; ++$i ) {
+ if ( $st1{$i} != $st2{$i} ) { break; }
+ }
+ return $i;
+ }
+ # These next three functions open, continue, and close the list
+ # element appropriate to the prefix character passed into them.
+ #
+ /* private */ function openList( $char ) {
+ $result = $this->closeParagraph();
+
+ if ( '*' == $char ) { $result .= '<ul><li>'; }
+ else if ( '#' == $char ) { $result .= '<ol><li>'; }
+ else if ( ':' == $char ) { $result .= '<dl><dd>'; }
+ else if ( ';' == $char ) {
+ $result .= '<dl><dt>';
+ $this->mDTopen = true;
+ }
+ else { $result = '<!-- ERR 1 -->'; }
+
+ return $result;
+ }
+
+ /* private */ function nextItem( $char ) {
+ if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
+ else if ( ':' == $char || ';' == $char ) {
+ $close = '</dd>';
+ if ( $this->mDTopen ) { $close = '</dt>'; }
+ if ( ';' == $char ) {
+ $this->mDTopen = true;
+ return $close . '<dt>';
+ } else {
+ $this->mDTopen = false;
+ return $close . '<dd>';
+ }
+ }
+ return '<!-- ERR 2 -->';
+ }
+
+ /* private */ function closeList( $char ) {
+ if ( '*' == $char ) { $text = '</li></ul>'; }
+ else if ( '#' == $char ) { $text = '</li></ol>'; }
+ else if ( ':' == $char ) {
+ if ( $this->mDTopen ) {
+ $this->mDTopen = false;
+ $text = '</dt></dl>';
+ } else {
+ $text = '</dd></dl>';
+ }
+ }
+ else { return '<!-- ERR 3 -->'; }
+ return $text."\n";
+ }
+ /**#@-*/
+
+ /**
+ * Make lists from lines starting with ':', '*', '#', etc.
+ *
+ * @private
+ * @return string the lists rendered as HTML
+ */
+ function doBlockLevels( $text, $linestart ) {
+ $fname = 'Parser::doBlockLevels';
+ wfProfileIn( $fname );
+
+ # Parsing through the text line by line. The main thing
+ # happening here is handling of block-level elements p, pre,
+ # and making lists from lines starting with * # : etc.
+ #
+ $textLines = explode( "\n", $text );
+
+ $lastPrefix = $output = '';
+ $this->mDTopen = $inBlockElem = false;
+ $prefixLength = 0;
+ $paragraphStack = false;
+
+ if ( !$linestart ) {
+ $output .= array_shift( $textLines );
+ }
+ foreach ( $textLines as $oLine ) {
+ $lastPrefixLength = strlen( $lastPrefix );
+ $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
+ $preOpenMatch = preg_match('/<pre/i', $oLine );
+ if ( !$this->mInPre ) {
+ # Multiple prefixes may abut each other for nested lists.
+ $prefixLength = strspn( $oLine, '*#:;' );
+ $pref = substr( $oLine, 0, $prefixLength );
+
+ # eh?
+ $pref2 = str_replace( ';', ':', $pref );
+ $t = substr( $oLine, $prefixLength );
+ $this->mInPre = !empty($preOpenMatch);
+ } else {
+ # Don't interpret any other prefixes in preformatted text
+ $prefixLength = 0;
+ $pref = $pref2 = '';
+ $t = $oLine;
+ }
+
+ # List generation
+ if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+ # Same as the last item, so no need to deal with nesting or opening stuff
+ $output .= $this->nextItem( substr( $pref, -1 ) );
+ $paragraphStack = false;
+
+ if ( substr( $pref, -1 ) == ';') {
+ # The one nasty exception: definition lists work like this:
+ # ; title : definition text
+ # So we check for : in the remainder text to split up the
+ # title and definition, without b0rking links.
+ $term = $t2 = '';
+ if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+ $t = $t2;
+ $output .= $term . $this->nextItem( ':' );
+ }
+ }
+ } elseif( $prefixLength || $lastPrefixLength ) {
+ # Either open or close a level...
+ $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+ $paragraphStack = false;
+
+ while( $commonPrefixLength < $lastPrefixLength ) {
+ $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+ --$lastPrefixLength;
+ }
+ if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
+ $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+ }
+ while ( $prefixLength > $commonPrefixLength ) {
+ $char = substr( $pref, $commonPrefixLength, 1 );
+ $output .= $this->openList( $char );
+
+ if ( ';' == $char ) {
+ # FIXME: This is dupe of code above
+ if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+ $t = $t2;
+ $output .= $term . $this->nextItem( ':' );
+ }
+ }
+ ++$commonPrefixLength;
+ }
+ $lastPrefix = $pref2;
+ }
+ if( 0 == $prefixLength ) {
+ wfProfileIn( "$fname-paragraph" );
+ # No prefix (not in list)--go to paragraph mode
+ // XXX: use a stack for nestable elements like span, table and div
+ $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/center|<\\/tr|<\\/td|<\\/th)/iS', $t );
+ $closematch = preg_match(
+ '/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
+ '<td|<th|<div|<\\/div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<center)/iS', $t );
+ if ( $openmatch or $closematch ) {
+ $paragraphStack = false;
+ # TODO bug 5718: paragraph closed
+ $output .= $this->closeParagraph();
+ if ( $preOpenMatch and !$preCloseMatch ) {
+ $this->mInPre = true;
+ }
+ if ( $closematch ) {
+ $inBlockElem = false;
+ } else {
+ $inBlockElem = true;
+ }
+ } else if ( !$inBlockElem && !$this->mInPre ) {
+ if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+ // pre
+ if ($this->mLastSection != 'pre') {
+ $paragraphStack = false;
+ $output .= $this->closeParagraph().'<pre>';
+ $this->mLastSection = 'pre';
+ }
+ $t = substr( $t, 1 );
+ } else {
+ // paragraph
+ if ( '' == trim($t) ) {
+ if ( $paragraphStack ) {
+ $output .= $paragraphStack.'<br />';
+ $paragraphStack = false;
+ $this->mLastSection = 'p';
+ } else {
+ if ($this->mLastSection != 'p' ) {
+ $output .= $this->closeParagraph();
+ $this->mLastSection = '';
+ $paragraphStack = '<p>';
+ } else {
+ $paragraphStack = '</p><p>';
+ }
+ }
+ } else {
+ if ( $paragraphStack ) {
+ $output .= $paragraphStack;
+ $paragraphStack = false;
+ $this->mLastSection = 'p';
+ } else if ($this->mLastSection != 'p') {
+ $output .= $this->closeParagraph().'<p>';
+ $this->mLastSection = 'p';
+ }
+ }
+ }
+ }
+ wfProfileOut( "$fname-paragraph" );
+ }
+ // somewhere above we forget to get out of pre block (bug 785)
+ if($preCloseMatch && $this->mInPre) {
+ $this->mInPre = false;
+ }
+ if ($paragraphStack === false) {
+ $output .= $t."\n";
+ }
+ }
+ while ( $prefixLength ) {
+ $output .= $this->closeList( $pref2{$prefixLength-1} );
+ --$prefixLength;
+ }
+ if ( '' != $this->mLastSection ) {
+ $output .= '</' . $this->mLastSection . '>';
+ $this->mLastSection = '';
+ }
+
+ wfProfileOut( $fname );
+ return $output;
+ }
+
+ /**
+ * Split up a string on ':', ignoring any occurences inside tags
+ * to prevent illegal overlapping.
+ * @param string $str the string to split
+ * @param string &$before set to everything before the ':'
+ * @param string &$after set to everything after the ':'
+ * return string the position of the ':', or false if none found
+ */
+ function findColonNoLinks($str, &$before, &$after) {
+ $fname = 'Parser::findColonNoLinks';
+ wfProfileIn( $fname );
+
+ $pos = strpos( $str, ':' );
+ if( $pos === false ) {
+ // Nothing to find!
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ $lt = strpos( $str, '<' );
+ if( $lt === false || $lt > $pos ) {
+ // Easy; no tag nesting to worry about
+ $before = substr( $str, 0, $pos );
+ $after = substr( $str, $pos+1 );
+ wfProfileOut( $fname );
+ return $pos;
+ }
+
+ // Ugly state machine to walk through avoiding tags.
+ $state = MW_COLON_STATE_TEXT;
+ $stack = 0;
+ $len = strlen( $str );
+ for( $i = 0; $i < $len; $i++ ) {
+ $c = $str{$i};
+
+ switch( $state ) {
+ // (Using the number is a performance hack for common cases)
+ case 0: // MW_COLON_STATE_TEXT:
+ switch( $c ) {
+ case "<":
+ // Could be either a <start> tag or an </end> tag
+ $state = MW_COLON_STATE_TAGSTART;
+ break;
+ case ":":
+ if( $stack == 0 ) {
+ // We found it!
+ $before = substr( $str, 0, $i );
+ $after = substr( $str, $i + 1 );
+ wfProfileOut( $fname );
+ return $i;
+ }
+ // Embedded in a tag; don't break it.
+ break;
+ default:
+ // Skip ahead looking for something interesting
+ $colon = strpos( $str, ':', $i );
+ if( $colon === false ) {
+ // Nothing else interesting
+ wfProfileOut( $fname );
+ return false;
+ }
+ $lt = strpos( $str, '<', $i );
+ if( $stack === 0 ) {
+ if( $lt === false || $colon < $lt ) {
+ // We found it!
+ $before = substr( $str, 0, $colon );
+ $after = substr( $str, $colon + 1 );
+ wfProfileOut( $fname );
+ return $i;
+ }
+ }
+ if( $lt === false ) {
+ // Nothing else interesting to find; abort!
+ // We're nested, but there's no close tags left. Abort!
+ break 2;
+ }
+ // Skip ahead to next tag start
+ $i = $lt;
+ $state = MW_COLON_STATE_TAGSTART;
+ }
+ break;
+ case 1: // MW_COLON_STATE_TAG:
+ // In a <tag>
+ switch( $c ) {
+ case ">":
+ $stack++;
+ $state = MW_COLON_STATE_TEXT;
+ break;
+ case "/":
+ // Slash may be followed by >?
+ $state = MW_COLON_STATE_TAGSLASH;
+ break;
+ default:
+ // ignore
+ }
+ break;
+ case 2: // MW_COLON_STATE_TAGSTART:
+ switch( $c ) {
+ case "/":
+ $state = MW_COLON_STATE_CLOSETAG;
+ break;
+ case "!":
+ $state = MW_COLON_STATE_COMMENT;
+ break;
+ case ">":
+ // Illegal early close? This shouldn't happen D:
+ $state = MW_COLON_STATE_TEXT;
+ break;
+ default:
+ $state = MW_COLON_STATE_TAG;
+ }
+ break;
+ case 3: // MW_COLON_STATE_CLOSETAG:
+ // In a </tag>
+ if( $c == ">" ) {
+ $stack--;
+ if( $stack < 0 ) {
+ wfDebug( "Invalid input in $fname; too many close tags\n" );
+ wfProfileOut( $fname );
+ return false;
+ }
+ $state = MW_COLON_STATE_TEXT;
+ }
+ break;
+ case MW_COLON_STATE_TAGSLASH:
+ if( $c == ">" ) {
+ // Yes, a self-closed tag <blah/>
+ $state = MW_COLON_STATE_TEXT;
+ } else {
+ // Probably we're jumping the gun, and this is an attribute
+ $state = MW_COLON_STATE_TAG;
+ }
+ break;
+ case 5: // MW_COLON_STATE_COMMENT:
+ if( $c == "-" ) {
+ $state = MW_COLON_STATE_COMMENTDASH;
+ }
+ break;
+ case MW_COLON_STATE_COMMENTDASH:
+ if( $c == "-" ) {
+ $state = MW_COLON_STATE_COMMENTDASHDASH;
+ } else {
+ $state = MW_COLON_STATE_COMMENT;
+ }
+ break;
+ case MW_COLON_STATE_COMMENTDASHDASH:
+ if( $c == ">" ) {
+ $state = MW_COLON_STATE_TEXT;
+ } else {
+ $state = MW_COLON_STATE_COMMENT;
+ }
+ break;
+ default:
+ throw new MWException( "State machine error in $fname" );
+ }
+ }
+ if( $stack > 0 ) {
+ wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+ return false;
+ }
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ /**
+ * Return value of a magic variable (like PAGENAME)
+ *
+ * @private
+ */
+ function getVariableValue( $index ) {
+ global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
+
+ /**
+ * Some of these require message or data lookups and can be
+ * expensive to check many times.
+ */
+ static $varCache = array();
+ if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) )
+ if ( isset( $varCache[$index] ) )
+ return $varCache[$index];
+
+ $ts = time();
+ wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
+
+ switch ( $index ) {
+ case MAG_CURRENTMONTH:
+ return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) );
+ case MAG_CURRENTMONTHNAME:
+ return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) );
+ case MAG_CURRENTMONTHNAMEGEN:
+ return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) );
+ case MAG_CURRENTMONTHABBREV:
+ return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) );
+ case MAG_CURRENTDAY:
+ return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) );
+ case MAG_CURRENTDAY2:
+ return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) );
+ case MAG_PAGENAME:
+ return $this->mTitle->getText();
+ case MAG_PAGENAMEE:
+ return $this->mTitle->getPartialURL();
+ case MAG_FULLPAGENAME:
+ return $this->mTitle->getPrefixedText();
+ case MAG_FULLPAGENAMEE:
+ return $this->mTitle->getPrefixedURL();
+ case MAG_SUBPAGENAME:
+ return $this->mTitle->getSubpageText();
+ case MAG_SUBPAGENAMEE:
+ return $this->mTitle->getSubpageUrlForm();
+ case MAG_BASEPAGENAME:
+ return $this->mTitle->getBaseText();
+ case MAG_BASEPAGENAMEE:
+ return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+ case MAG_TALKPAGENAME:
+ if( $this->mTitle->canTalk() ) {
+ $talkPage = $this->mTitle->getTalkPage();
+ return $talkPage->getPrefixedText();
+ } else {
+ return '';
+ }
+ case MAG_TALKPAGENAMEE:
+ if( $this->mTitle->canTalk() ) {
+ $talkPage = $this->mTitle->getTalkPage();
+ return $talkPage->getPrefixedUrl();
+ } else {
+ return '';
+ }
+ case MAG_SUBJECTPAGENAME:
+ $subjPage = $this->mTitle->getSubjectPage();
+ return $subjPage->getPrefixedText();
+ case MAG_SUBJECTPAGENAMEE:
+ $subjPage = $this->mTitle->getSubjectPage();
+ return $subjPage->getPrefixedUrl();
+ case MAG_REVISIONID:
+ return $this->mRevisionId;
+ case MAG_NAMESPACE:
+ return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ case MAG_NAMESPACEE:
+ return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ case MAG_TALKSPACE:
+ return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+ case MAG_TALKSPACEE:
+ return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+ case MAG_SUBJECTSPACE:
+ return $this->mTitle->getSubjectNsText();
+ case MAG_SUBJECTSPACEE:
+ return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+ case MAG_CURRENTDAYNAME:
+ return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 );
+ case MAG_CURRENTYEAR:
+ return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true );
+ case MAG_CURRENTTIME:
+ return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+ case MAG_CURRENTWEEK:
+ // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+ // int to remove the padding
+ return $varCache[$index] = $wgContLang->formatNum( (int)date( 'W', $ts ) );
+ case MAG_CURRENTDOW:
+ return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) );
+ case MAG_NUMBEROFARTICLES:
+ return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() );
+ case MAG_NUMBEROFFILES:
+ return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() );
+ case MAG_NUMBEROFUSERS:
+ return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() );
+ case MAG_NUMBEROFPAGES:
+ return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() );
+ case MAG_NUMBEROFADMINS:
+ return $varCache[$index] = $wgContLang->formatNum( wfNumberOfAdmins() );
+ case MAG_CURRENTTIMESTAMP:
+ return $varCache[$index] = wfTimestampNow();
+ case MAG_CURRENTVERSION:
+ global $wgVersion;
+ return $wgVersion;
+ case MAG_SITENAME:
+ return $wgSitename;
+ case MAG_SERVER:
+ return $wgServer;
+ case MAG_SERVERNAME:
+ return $wgServerName;
+ case MAG_SCRIPTPATH:
+ return $wgScriptPath;
+ case MAG_DIRECTIONMARK:
+ return $wgContLang->getDirMark();
+ case MAG_CONTENTLANGUAGE:
+ global $wgContLanguageCode;
+ return $wgContLanguageCode;
+ default:
+ $ret = null;
+ if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
+ return $ret;
+ else
+ return null;
+ }
+ }
+
+ /**
+ * initialise the magic variables (like CURRENTMONTHNAME)
+ *
+ * @private
+ */
+ function initialiseVariables() {
+ $fname = 'Parser::initialiseVariables';
+ wfProfileIn( $fname );
+ global $wgVariableIDs;
+
+ $this->mVariables = array();
+ foreach ( $wgVariableIDs as $id ) {
+ $mw =& MagicWord::get( $id );
+ $mw->addToArray( $this->mVariables, $id );
+ }
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * parse any parentheses in format ((title|part|part))
+ * and call callbacks to get a replacement text for any found piece
+ *
+ * @param string $text The text to parse
+ * @param array $callbacks rules in form:
+ * '{' => array( # opening parentheses
+ * 'end' => '}', # closing parentheses
+ * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found
+ * 4 => callback # replacement callback to call if {{{{..}}}} is found
+ * )
+ * )
+ * @private
+ */
+ function replace_callback ($text, $callbacks) {
+ wfProfileIn( __METHOD__ . '-self' );
+ $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet
+ $lastOpeningBrace = -1; # last not closed parentheses
+
+ for ($i = 0; $i < strlen($text); $i++) {
+ # check for any opening brace
+ $rule = null;
+ $nextPos = -1;
+ foreach ($callbacks as $key => $value) {
+ $pos = strpos ($text, $key, $i);
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)) {
+ $rule = $value;
+ $nextPos = $pos;
+ }
+ }
+
+ if ($lastOpeningBrace >= 0) {
+ $pos = strpos ($text, $openingBraceStack[$lastOpeningBrace]['braceEnd'], $i);
+
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
+ $rule = null;
+ $nextPos = $pos;
+ }
+
+ $pos = strpos ($text, '|', $i);
+
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
+ $rule = null;
+ $nextPos = $pos;
+ }
+ }
+
+ if ($nextPos == -1)
+ break;
+
+ $i = $nextPos;
+
+ # found openning brace, lets add it to parentheses stack
+ if (null != $rule) {
+ $piece = array('brace' => $text[$i],
+ 'braceEnd' => $rule['end'],
+ 'count' => 1,
+ 'title' => '',
+ 'parts' => null);
+
+ # count openning brace characters
+ while ($i+1 < strlen($text) && $text[$i+1] == $piece['brace']) {
+ $piece['count']++;
+ $i++;
+ }
+
+ $piece['startAt'] = $i+1;
+ $piece['partStart'] = $i+1;
+
+ # we need to add to stack only if openning brace count is enough for any given rule
+ foreach ($rule['cb'] as $cnt => $fn) {
+ if ($piece['count'] >= $cnt) {
+ $lastOpeningBrace ++;
+ $openingBraceStack[$lastOpeningBrace] = $piece;
+ break;
+ }
+ }
+
+ continue;
+ }
+ else if ($lastOpeningBrace >= 0) {
+ # first check if it is a closing brace
+ if ($openingBraceStack[$lastOpeningBrace]['braceEnd'] == $text[$i]) {
+ # lets check if it is enough characters for closing brace
+ $count = 1;
+ while ($i+$count < strlen($text) && $text[$i+$count] == $text[$i])
+ $count++;
+
+ # if there are more closing parentheses than opening ones, we parse less
+ if ($openingBraceStack[$lastOpeningBrace]['count'] < $count)
+ $count = $openingBraceStack[$lastOpeningBrace]['count'];
+
+ # check for maximum matching characters (if there are 5 closing characters, we will probably need only 3 - depending on the rules)
+ $matchingCount = 0;
+ $matchingCallback = null;
+ foreach ($callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]['cb'] as $cnt => $fn) {
+ if ($count >= $cnt && $matchingCount < $cnt) {
+ $matchingCount = $cnt;
+ $matchingCallback = $fn;
+ }
+ }
+
+ if ($matchingCount == 0) {
+ $i += $count - 1;
+ continue;
+ }
+
+ # lets set a title or last part (if '|' was found)
+ if (null === $openingBraceStack[$lastOpeningBrace]['parts'])
+ $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+ else
+ $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+
+ $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
+ $pieceEnd = $i + $matchingCount;
+
+ if( is_callable( $matchingCallback ) ) {
+ $cbArgs = array (
+ 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
+ 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
+ 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
+ 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")),
+ );
+ # finally we can call a user callback and replace piece of text
+ wfProfileOut( __METHOD__ . '-self' );
+ $replaceWith = call_user_func( $matchingCallback, $cbArgs );
+ wfProfileIn( __METHOD__ . '-self' );
+ $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
+ $i = $pieceStart + strlen($replaceWith) - 1;
+ }
+ else {
+ # null value for callback means that parentheses should be parsed, but not replaced
+ $i += $matchingCount - 1;
+ }
+
+ # reset last openning parentheses, but keep it in case there are unused characters
+ $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
+ 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
+ 'count' => $openingBraceStack[$lastOpeningBrace]['count'],
+ 'title' => '',
+ 'parts' => null,
+ 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
+ $openingBraceStack[$lastOpeningBrace--] = null;
+
+ if ($matchingCount < $piece['count']) {
+ $piece['count'] -= $matchingCount;
+ $piece['startAt'] -= $matchingCount;
+ $piece['partStart'] = $piece['startAt'];
+ # do we still qualify for any callback with remaining count?
+ foreach ($callbacks[$piece['brace']]['cb'] as $cnt => $fn) {
+ if ($piece['count'] >= $cnt) {
+ $lastOpeningBrace ++;
+ $openingBraceStack[$lastOpeningBrace] = $piece;
+ break;
+ }
+ }
+ }
+ continue;
+ }
+
+ # lets set a title if it is a first separator, or next part otherwise
+ if ($text[$i] == '|') {
+ if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+ $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+ $openingBraceStack[$lastOpeningBrace]['parts'] = array();
+ }
+ else
+ $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+
+ $openingBraceStack[$lastOpeningBrace]['partStart'] = $i + 1;
+ }
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-self' );
+ return $text;
+ }
+
+ /**
+ * Replace magic variables, templates, and template arguments
+ * with the appropriate text. Templates are substituted recursively,
+ * taking care to avoid infinite loops.
+ *
+ * Note that the substitution depends on value of $mOutputType:
+ * OT_WIKI: only {{subst:}} templates
+ * OT_MSG: only magic variables
+ * OT_HTML: all templates and magic variables
+ *
+ * @param string $tex The text to transform
+ * @param array $args Key-value pairs representing template parameters to substitute
+ * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
+ * @private
+ */
+ function replaceVariables( $text, $args = array(), $argsOnly = false ) {
+ # Prevent too big inclusions
+ if( strlen( $text ) > MAX_INCLUDE_SIZE ) {
+ return $text;
+ }
+
+ $fname = 'Parser::replaceVariables';
+ wfProfileIn( $fname );
+
+ # This function is called recursively. To keep track of arguments we need a stack:
+ array_push( $this->mArgStack, $args );
+
+ $braceCallbacks = array();
+ if ( !$argsOnly ) {
+ $braceCallbacks[2] = array( &$this, 'braceSubstitution' );
+ }
+ if ( $this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI ) {
+ $braceCallbacks[3] = array( &$this, 'argSubstitution' );
+ }
+ $callbacks = array();
+ $callbacks['{'] = array('end' => '}', 'cb' => $braceCallbacks);
+ $callbacks['['] = array('end' => ']', 'cb' => array(2=>null));
+ $text = $this->replace_callback ($text, $callbacks);
+
+ array_pop( $this->mArgStack );
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Replace magic variables
+ * @private
+ */
+ function variableSubstitution( $matches ) {
+ $fname = 'Parser::variableSubstitution';
+ $varname = $matches[1];
+ wfProfileIn( $fname );
+ $skip = false;
+ if ( $this->mOutputType == OT_WIKI ) {
+ # Do only magic variables prefixed by SUBST
+ $mwSubst =& MagicWord::get( MAG_SUBST );
+ if (!$mwSubst->matchStartAndRemove( $varname ))
+ $skip = true;
+ # Note that if we don't substitute the variable below,
+ # we don't remove the {{subst:}} magic word, in case
+ # it is a template rather than a magic variable.
+ }
+ if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) {
+ $id = $this->mVariables[$varname];
+ $text = $this->getVariableValue( $id );
+ $this->mOutput->mContainsOldMagic = true;
+ } else {
+ $text = $matches[0];
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ # Split template arguments
+ function getTemplateArgs( $argsString ) {
+ if ( $argsString === '' ) {
+ return array();
+ }
+
+ $args = explode( '|', substr( $argsString, 1 ) );
+
+ # If any of the arguments contains a '[[' but no ']]', it needs to be
+ # merged with the next arg because the '|' character between belongs
+ # to the link syntax and not the template parameter syntax.
+ $argc = count($args);
+
+ for ( $i = 0; $i < $argc-1; $i++ ) {
+ if ( substr_count ( $args[$i], '[[' ) != substr_count ( $args[$i], ']]' ) ) {
+ $args[$i] .= '|'.$args[$i+1];
+ array_splice($args, $i+1, 1);
+ $i--;
+ $argc--;
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Return the text of a template, after recursively
+ * replacing any variables or templates within the template.
+ *
+ * @param array $piece The parts of the template
+ * $piece['text']: matched text
+ * $piece['title']: the title, i.e. the part before the |
+ * $piece['parts']: the parameter array
+ * @return string the text of the template
+ * @private
+ */
+ function braceSubstitution( $piece ) {
+ global $wgContLang, $wgLang, $wgAllowDisplayTitle, $action;
+ $fname = 'Parser::braceSubstitution';
+ wfProfileIn( $fname );
+
+ # Flags
+ $found = false; # $text has been filled
+ $nowiki = false; # wiki markup in $text should be escaped
+ $noparse = false; # Unsafe HTML tags should not be stripped, etc.
+ $noargs = false; # Don't replace triple-brace arguments in $text
+ $replaceHeadings = false; # Make the edit section links go to the template not the article
+ $isHTML = false; # $text is HTML, armour it against wikitext transformation
+ $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
+
+ # Title object, where $text came from
+ $title = NULL;
+
+ $linestart = '';
+
+ # $part1 is the bit before the first |, and must contain only title characters
+ # $args is a list of arguments, starting from index 0, not including $part1
+
+ $part1 = $piece['title'];
+ # If the third subpattern matched anything, it will start with |
+
+ if (null == $piece['parts']) {
+ $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title']));
+ if ($replaceWith != $piece['text']) {
+ $text = $replaceWith;
+ $found = true;
+ $noparse = true;
+ $noargs = true;
+ }
+ }
+
+ $args = (null == $piece['parts']) ? array() : $piece['parts'];
+ $argc = count( $args );
+
+ # SUBST
+ if ( !$found ) {
+ $mwSubst =& MagicWord::get( MAG_SUBST );
+ if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == OT_WIKI) ) {
+ # One of two possibilities is true:
+ # 1) Found SUBST but not in the PST phase
+ # 2) Didn't find SUBST and in the PST phase
+ # In either case, return without further processing
+ $text = $piece['text'];
+ $found = true;
+ $noparse = true;
+ $noargs = true;
+ }
+ }
+
+ # MSG, MSGNW, INT and RAW
+ if ( !$found ) {
+ # Check for MSGNW:
+ $mwMsgnw =& MagicWord::get( MAG_MSGNW );
+ if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
+ $nowiki = true;
+ } else {
+ # Remove obsolete MSG:
+ $mwMsg =& MagicWord::get( MAG_MSG );
+ $mwMsg->matchStartAndRemove( $part1 );
+ }
+
+ # Check for RAW:
+ $mwRaw =& MagicWord::get( MAG_RAW );
+ if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
+ $forceRawInterwiki = true;
+ }
+
+ # Check if it is an internal message
+ $mwInt =& MagicWord::get( MAG_INT );
+ if ( $mwInt->matchStartAndRemove( $part1 ) ) {
+ if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) {
+ $text = $linestart . wfMsgReal( $part1, $args, true );
+ $found = true;
+ }
+ }
+ }
+
+ # Parser functions
+ if ( !$found ) {
+ wfProfileIn( __METHOD__ . '-pfunc' );
+
+ $colonPos = strpos( $part1, ':' );
+ if ( $colonPos !== false ) {
+ # Case sensitive functions
+ $function = substr( $part1, 0, $colonPos );
+ if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
+ $function = $this->mFunctionSynonyms[1][$function];
+ } else {
+ # Case insensitive functions
+ $function = strtolower( $function );
+ if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
+ $function = $this->mFunctionSynonyms[0][$function];
+ } else {
+ $function = false;
+ }
+ }
+ if ( $function ) {
+ $funcArgs = array_map( 'trim', $args );
+ $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
+ $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs );
+ $found = true;
+
+ // The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
+ //$noargs = true;
+ //$noparse = true;
+
+ if ( is_array( $result ) ) {
+ if ( isset( $result[0] ) ) {
+ $text = $linestart . $result[0];
+ unset( $result[0] );
+ }
+
+ // Extract flags into the local scope
+ // This allows callers to set flags such as nowiki, noparse, found, etc.
+ extract( $result );
+ } else {
+ $text = $linestart . $result;
+ }
+ }
+ }
+ wfProfileOut( __METHOD__ . '-pfunc' );
+ }
+
+ # Template table test
+
+ # Did we encounter this template already? If yes, it is in the cache
+ # and we need to check for loops.
+ if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) {
+ $found = true;
+
+ # Infinite loop test
+ if ( isset( $this->mTemplatePath[$part1] ) ) {
+ $noparse = true;
+ $noargs = true;
+ $found = true;
+ $text = $linestart .
+ '{{' . $part1 . '}}' .
+ '<!-- WARNING: template loop detected -->';
+ wfDebug( "$fname: template loop broken at '$part1'\n" );
+ } else {
+ # set $text to cached message.
+ $text = $linestart . $this->mTemplates[$piece['title']];
+ }
+ }
+
+ # Load from database
+ $lastPathLevel = $this->mTemplatePath;
+ if ( !$found ) {
+ wfProfileIn( __METHOD__ . '-loadtpl' );
+ $ns = NS_TEMPLATE;
+ # declaring $subpage directly in the function call
+ # does not work correctly with references and breaks
+ # {{/subpage}}-style inclusions
+ $subpage = '';
+ $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
+ if ($subpage !== '') {
+ $ns = $this->mTitle->getNamespace();
+ }
+ $title = Title::newFromText( $part1, $ns );
+
+
+ if ( !is_null( $title ) ) {
+ $checkVariantLink = sizeof($wgContLang->getVariants())>1;
+ # Check for language variants if the template is not found
+ if($checkVariantLink && $title->getArticleID() == 0){
+ $wgContLang->findVariantLink($part1, $title);
+ }
+
+ if ( !$title->isExternal() ) {
+ # Check for excessive inclusion
+ $dbk = $title->getPrefixedDBkey();
+ if ( $this->incrementIncludeCount( $dbk ) ) {
+ if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->mOutputType != OT_WIKI ) {
+ $text = SpecialPage::capturePath( $title );
+ if ( is_string( $text ) ) {
+ $found = true;
+ $noparse = true;
+ $noargs = true;
+ $isHTML = true;
+ $this->disableCache();
+ }
+ } else {
+ $articleContent = $this->fetchTemplate( $title );
+ if ( $articleContent !== false ) {
+ $found = true;
+ $text = $articleContent;
+ $replaceHeadings = true;
+ }
+ }
+ }
+
+ # If the title is valid but undisplayable, make a link to it
+ if ( $this->mOutputType == OT_HTML && !$found ) {
+ $text = '[['.$title->getPrefixedText().']]';
+ $found = true;
+ }
+ } elseif ( $title->isTrans() ) {
+ // Interwiki transclusion
+ if ( $this->mOutputType == OT_HTML && !$forceRawInterwiki ) {
+ $text = $this->interwikiTransclude( $title, 'render' );
+ $isHTML = true;
+ $noparse = true;
+ } else {
+ $text = $this->interwikiTransclude( $title, 'raw' );
+ $replaceHeadings = true;
+ }
+ $found = true;
+ }
+
+ # Template cache array insertion
+ # Use the original $piece['title'] not the mangled $part1, so that
+ # modifiers such as RAW: produce separate cache entries
+ if( $found ) {
+ if( $isHTML ) {
+ // A special page; don't store it in the template cache.
+ } else {
+ $this->mTemplates[$piece['title']] = $text;
+ }
+ $text = $linestart . $text;
+ }
+ }
+ wfProfileOut( __METHOD__ . '-loadtpl' );
+ }
+
+ # Recursive parsing, escaping and link table handling
+ # Only for HTML output
+ if ( $nowiki && $found && $this->mOutputType == OT_HTML ) {
+ $text = wfEscapeWikiText( $text );
+ } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found ) {
+ if ( $noargs ) {
+ $assocArgs = array();
+ } else {
+ # Clean up argument array
+ $assocArgs = array();
+ $index = 1;
+ foreach( $args as $arg ) {
+ $eqpos = strpos( $arg, '=' );
+ if ( $eqpos === false ) {
+ $assocArgs[$index++] = $arg;
+ } else {
+ $name = trim( substr( $arg, 0, $eqpos ) );
+ $value = trim( substr( $arg, $eqpos+1 ) );
+ if ( $value === false ) {
+ $value = '';
+ }
+ if ( $name !== false ) {
+ $assocArgs[$name] = $value;
+ }
+ }
+ }
+
+ # Add a new element to the templace recursion path
+ $this->mTemplatePath[$part1] = 1;
+ }
+
+ if ( !$noparse ) {
+ # If there are any <onlyinclude> tags, only include them
+ if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) {
+ preg_match_all( '/<onlyinclude>(.*?)\n?<\/onlyinclude>/s', $text, $m );
+ $text = '';
+ foreach ($m[1] as $piece)
+ $text .= $piece;
+ }
+ # Remove <noinclude> sections and <includeonly> tags
+ $text = preg_replace( '/<noinclude>.*?<\/noinclude>/s', '', $text );
+ $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) );
+
+ if( $this->mOutputType == OT_HTML ) {
+ # Strip <nowiki>, <pre>, etc.
+ $text = $this->strip( $text, $this->mStripState );
+ $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
+ }
+ $text = $this->replaceVariables( $text, $assocArgs );
+
+ # If the template begins with a table or block-level
+ # element, it should be treated as beginning a new line.
+ if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) {
+ $text = "\n" . $text;
+ }
+ } elseif ( !$noargs ) {
+ # $noparse and !$noargs
+ # Just replace the arguments, not any double-brace items
+ # This is used for rendered interwiki transclusion
+ $text = $this->replaceVariables( $text, $assocArgs, true );
+ }
+ }
+ # Prune lower levels off the recursion check path
+ $this->mTemplatePath = $lastPathLevel;
+
+ if ( !$found ) {
+ wfProfileOut( $fname );
+ return $piece['text'];
+ } else {
+ wfProfileIn( __METHOD__ . '-placeholders' );
+ if ( $isHTML ) {
+ # Replace raw HTML by a placeholder
+ # Add a blank line preceding, to prevent it from mucking up
+ # immediately preceding headings
+ $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState );
+ } else {
+ # replace ==section headers==
+ # XXX this needs to go away once we have a better parser.
+ if ( $this->mOutputType != OT_WIKI && $replaceHeadings ) {
+ if( !is_null( $title ) )
+ $encodedname = base64_encode($title->getPrefixedDBkey());
+ else
+ $encodedname = base64_encode("");
+ $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+ $text = '';
+ $nsec = 0;
+ for( $i = 0; $i < count($m); $i += 2 ) {
+ $text .= $m[$i];
+ if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
+ $hl = $m[$i + 1];
+ if( strstr($hl, "<!--MWTEMPLATESECTION") ) {
+ $text .= $hl;
+ continue;
+ }
+ preg_match('/^(={1,6})(.*?)(={1,6})\s*?$/m', $hl, $m2);
+ $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION="
+ . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3];
+
+ $nsec++;
+ }
+ }
+ }
+ wfProfileOut( __METHOD__ . '-placeholders' );
+ }
+
+ # Prune lower levels off the recursion check path
+ $this->mTemplatePath = $lastPathLevel;
+
+ if ( !$found ) {
+ wfProfileOut( $fname );
+ return $piece['text'];
+ } else {
+ wfProfileOut( $fname );
+ return $text;
+ }
+ }
+
+ /**
+ * Fetch the unparsed text of a template and register a reference to it.
+ */
+ function fetchTemplate( $title ) {
+ $text = false;
+ // Loop to fetch the article, with up to 1 redirect
+ for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
+ $rev = Revision::newFromTitle( $title );
+ $this->mOutput->addTemplate( $title, $title->getArticleID() );
+ if ( !$rev ) {
+ break;
+ }
+ $text = $rev->getText();
+ if ( $text === false ) {
+ break;
+ }
+ // Redirect?
+ $title = Title::newFromRedirect( $text );
+ }
+ return $text;
+ }
+
+ /**
+ * Transclude an interwiki link.
+ */
+ function interwikiTransclude( $title, $action ) {
+ global $wgEnableScaryTranscluding, $wgCanonicalNamespaceNames;
+
+ if (!$wgEnableScaryTranscluding)
+ return wfMsg('scarytranscludedisabled');
+
+ // The namespace will actually only be 0 or 10, depending on whether there was a leading :
+ // But we'll handle it generally anyway
+ if ( $title->getNamespace() ) {
+ // Use the canonical namespace, which should work anywhere
+ $articleName = $wgCanonicalNamespaceNames[$title->getNamespace()] . ':' . $title->getDBkey();
+ } else {
+ $articleName = $title->getDBkey();
+ }
+
+ $url = str_replace('$1', urlencode($articleName), Title::getInterwikiLink($title->getInterwiki()));
+ $url .= "?action=$action";
+ if (strlen($url) > 255)
+ return wfMsg('scarytranscludetoolong');
+ return $this->fetchScaryTemplateMaybeFromCache($url);
+ }
+
+ function fetchScaryTemplateMaybeFromCache($url) {
+ global $wgTranscludeCacheExpiry;
+ $dbr =& wfGetDB(DB_SLAVE);
+ $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
+ array('tc_url' => $url));
+ if ($obj) {
+ $time = $obj->tc_time;
+ $text = $obj->tc_contents;
+ if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
+ return $text;
+ }
+ }
+
+ $text = Http::get($url);
+ if (!$text)
+ return wfMsg('scarytranscludefailed', $url);
+
+ $dbw =& wfGetDB(DB_MASTER);
+ $dbw->replace('transcache', array('tc_url'), array(
+ 'tc_url' => $url,
+ 'tc_time' => time(),
+ 'tc_contents' => $text));
+ return $text;
+ }
+
+
+ /**
+ * Triple brace replacement -- used for template arguments
+ * @private
+ */
+ function argSubstitution( $matches ) {
+ $arg = trim( $matches['title'] );
+ $text = $matches['text'];
+ $inputArgs = end( $this->mArgStack );
+
+ if ( array_key_exists( $arg, $inputArgs ) ) {
+ $text = $inputArgs[$arg];
+ } else if ($this->mOutputType == OT_HTML && null != $matches['parts'] && count($matches['parts']) > 0) {
+ $text = $matches['parts'][0];
+ }
+
+ return $text;
+ }
+
+ /**
+ * Returns true if the function is allowed to include this entity
+ * @private
+ */
+ function incrementIncludeCount( $dbk ) {
+ if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
+ $this->mIncludeCount[$dbk] = 0;
+ }
+ if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Detect __NOGALLERY__ magic word and set a placeholder
+ */
+ function stripNoGallery( &$text ) {
+ # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
+ # do not add TOC
+ $mw = MagicWord::get( MAG_NOGALLERY );
+ $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
+ }
+
+ /**
+ * Detect __TOC__ magic word and set a placeholder
+ */
+ function stripToc( $text ) {
+ # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
+ # do not add TOC
+ $mw = MagicWord::get( MAG_NOTOC );
+ if( $mw->matchAndRemove( $text ) ) {
+ $this->mShowToc = false;
+ }
+
+ $mw = MagicWord::get( MAG_TOC );
+ if( $mw->match( $text ) ) {
+ $this->mShowToc = true;
+ $this->mForceTocPosition = true;
+
+ // Set a placeholder. At the end we'll fill it in with the TOC.
+ $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+
+ // Only keep the first one.
+ $text = $mw->replace( '', $text );
+ }
+ return $text;
+ }
+
+ /**
+ * This function accomplishes several tasks:
+ * 1) Auto-number headings if that option is enabled
+ * 2) Add an [edit] link to sections for logged in users who have enabled the option
+ * 3) Add a Table of contents on the top for users who have enabled the option
+ * 4) Auto-anchor headings
+ *
+ * It loops through all headlines, collects the necessary data, then splits up the
+ * string and re-inserts the newly formatted headlines.
+ *
+ * @param string $text
+ * @param boolean $isMain
+ * @private
+ */
+ function formatHeadings( $text, $isMain=true ) {
+ global $wgMaxTocLevel, $wgContLang;
+
+ $doNumberHeadings = $this->mOptions->getNumberHeadings();
+ if( !$this->mTitle->userCanEdit() ) {
+ $showEditLink = 0;
+ } else {
+ $showEditLink = $this->mOptions->getEditSection();
+ }
+
+ # Inhibit editsection links if requested in the page
+ $esw =& MagicWord::get( MAG_NOEDITSECTION );
+ if( $esw->matchAndRemove( $text ) ) {
+ $showEditLink = 0;
+ }
+
+ # Get all headlines for numbering them and adding funky stuff like [edit]
+ # links - this is for later, but we need the number of headlines right now
+ $numMatches = preg_match_all( '/<H([1-6])(.*?'.'>)(.*?)<\/H[1-6] *>/i', $text, $matches );
+
+ # if there are fewer than 4 headlines in the article, do not show TOC
+ # unless it's been explicitly enabled.
+ $enoughToc = $this->mShowToc &&
+ (($numMatches >= 4) || $this->mForceTocPosition);
+
+ # Allow user to stipulate that a page should have a "new section"
+ # link added via __NEWSECTIONLINK__
+ $mw =& MagicWord::get( MAG_NEWSECTIONLINK );
+ if( $mw->matchAndRemove( $text ) )
+ $this->mOutput->setNewSection( true );
+
+ # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+ # override above conditions and always show TOC above first header
+ $mw =& MagicWord::get( MAG_FORCETOC );
+ if ($mw->matchAndRemove( $text ) ) {
+ $this->mShowToc = true;
+ $enoughToc = true;
+ }
+
+ # Never ever show TOC if no headers
+ if( $numMatches < 1 ) {
+ $enoughToc = false;
+ }
+
+ # We need this to perform operations on the HTML
+ $sk =& $this->mOptions->getSkin();
+
+ # headline counter
+ $headlineCount = 0;
+ $sectionCount = 0; # headlineCount excluding template sections
+
+ # Ugh .. the TOC should have neat indentation levels which can be
+ # passed to the skin functions. These are determined here
+ $toc = '';
+ $full = '';
+ $head = array();
+ $sublevelCount = array();
+ $levelCount = array();
+ $toclevel = 0;
+ $level = 0;
+ $prevlevel = 0;
+ $toclevel = 0;
+ $prevtoclevel = 0;
+
+ foreach( $matches[3] as $headline ) {
+ $istemplate = 0;
+ $templatetitle = '';
+ $templatesection = 0;
+ $numbering = '';
+
+ if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) {
+ $istemplate = 1;
+ $templatetitle = base64_decode($mat[1]);
+ $templatesection = 1 + (int)base64_decode($mat[2]);
+ $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline);
+ }
+
+ if( $toclevel ) {
+ $prevlevel = $level;
+ $prevtoclevel = $toclevel;
+ }
+ $level = $matches[1][$headlineCount];
+
+ if( $doNumberHeadings || $enoughToc ) {
+
+ if ( $level > $prevlevel ) {
+ # Increase TOC level
+ $toclevel++;
+ $sublevelCount[$toclevel] = 0;
+ if( $toclevel<$wgMaxTocLevel ) {
+ $toc .= $sk->tocIndent();
+ }
+ }
+ elseif ( $level < $prevlevel && $toclevel > 1 ) {
+ # Decrease TOC level, find level to jump to
+
+ if ( $toclevel == 2 && $level <= $levelCount[1] ) {
+ # Can only go down to level 1
+ $toclevel = 1;
+ } else {
+ for ($i = $toclevel; $i > 0; $i--) {
+ if ( $levelCount[$i] == $level ) {
+ # Found last matching level
+ $toclevel = $i;
+ break;
+ }
+ elseif ( $levelCount[$i] < $level ) {
+ # Found first matching level below current level
+ $toclevel = $i + 1;
+ break;
+ }
+ }
+ }
+ if( $toclevel<$wgMaxTocLevel ) {
+ $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+ }
+ }
+ else {
+ # No change in level, end TOC line
+ if( $toclevel<$wgMaxTocLevel ) {
+ $toc .= $sk->tocLineEnd();
+ }
+ }
+
+ $levelCount[$toclevel] = $level;
+
+ # count number of headlines for each level
+ @$sublevelCount[$toclevel]++;
+ $dot = 0;
+ for( $i = 1; $i <= $toclevel; $i++ ) {
+ if( !empty( $sublevelCount[$i] ) ) {
+ if( $dot ) {
+ $numbering .= '.';
+ }
+ $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+ $dot = 1;
+ }
+ }
+ }
+
+ # The canonized header is a version of the header text safe to use for links
+ # Avoid insertion of weird stuff like <math> by expanding the relevant sections
+ $canonized_headline = $this->unstrip( $headline, $this->mStripState );
+ $canonized_headline = $this->unstripNoWiki( $canonized_headline, $this->mStripState );
+
+ # Remove link placeholders by the link text.
+ # <!--LINK number-->
+ # turns into
+ # link text with suffix
+ $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
+ "\$this->mLinkHolders['texts'][\$1]",
+ $canonized_headline );
+ $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
+ "\$this->mInterwikiLinkHolders['texts'][\$1]",
+ $canonized_headline );
+
+ # strip out HTML
+ $canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline );
+ $tocline = trim( $canonized_headline );
+ # Save headline for section edit hint before it's escaped
+ $headline_hint = trim( $canonized_headline );
+ $canonized_headline = Sanitizer::escapeId( $tocline );
+ $refers[$headlineCount] = $canonized_headline;
+
+ # count how many in assoc. array so we can track dupes in anchors
+ @$refers[$canonized_headline]++;
+ $refcount[$headlineCount]=$refers[$canonized_headline];
+
+ # Don't number the heading if it is the only one (looks silly)
+ if( $doNumberHeadings && count( $matches[3] ) > 1) {
+ # the two are different if the line contains a link
+ $headline=$numbering . ' ' . $headline;
+ }
+
+ # Create the anchor for linking from the TOC to the section
+ $anchor = $canonized_headline;
+ if($refcount[$headlineCount] > 1 ) {
+ $anchor .= '_' . $refcount[$headlineCount];
+ }
+ if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
+ $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
+ }
+ if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
+ if ( empty( $head[$headlineCount] ) ) {
+ $head[$headlineCount] = '';
+ }
+ if( $istemplate )
+ $head[$headlineCount] .= $sk->editSectionLinkForOther($templatetitle, $templatesection);
+ else
+ $head[$headlineCount] .= $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
+ }
+
+ # give headline the correct <h#> tag
+ @$head[$headlineCount] .= "<a name=\"$anchor\"></a><h".$level.$matches[2][$headlineCount] .$headline.'</h'.$level.'>';
+
+ $headlineCount++;
+ if( !$istemplate )
+ $sectionCount++;
+ }
+
+ if( $enoughToc ) {
+ if( $toclevel<$wgMaxTocLevel ) {
+ $toc .= $sk->tocUnindent( $toclevel - 1 );
+ }
+ $toc = $sk->tocList( $toc );
+ }
+
+ # split up and insert constructed headlines
+
+ $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
+ $i = 0;
+
+ foreach( $blocks as $block ) {
+ if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+ # This is the [edit] link that appears for the top block of text when
+ # section editing is enabled
+
+ # Disabled because it broke block formatting
+ # For example, a bullet point in the top line
+ # $full .= $sk->editSectionLink(0);
+ }
+ $full .= $block;
+ if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
+ # Top anchor now in skin
+ $full = $full.$toc;
+ }
+
+ if( !empty( $head[$i] ) ) {
+ $full .= $head[$i];
+ }
+ $i++;
+ }
+ if( $this->mForceTocPosition ) {
+ return str_replace( '<!--MWTOC-->', $toc, $full );
+ } else {
+ return $full;
+ }
+ }
+
+ /**
+ * Return an HTML link for the "ISBN 123456" text
+ * @private
+ */
+ function magicISBN( $text ) {
+ $fname = 'Parser::magicISBN';
+ wfProfileIn( $fname );
+
+ $a = split( 'ISBN ', ' '.$text );
+ if ( count ( $a ) < 2 ) {
+ wfProfileOut( $fname );
+ return $text;
+ }
+ $text = substr( array_shift( $a ), 1);
+ $valid = '0123456789-Xx';
+
+ foreach ( $a as $x ) {
+ # hack: don't replace inside thumbnail title/alt
+ # attributes
+ if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
+ $text .= "ISBN $x";
+ continue;
+ }
+
+ $isbn = $blank = '' ;
+ while ( $x !== '' && ' ' == $x{0} ) {
+ $blank .= ' ';
+ $x = substr( $x, 1 );
+ }
+ if ( $x == '' ) { # blank isbn
+ $text .= "ISBN $blank";
+ continue;
+ }
+ while ( strstr( $valid, $x{0} ) != false ) {
+ $isbn .= $x{0};
+ $x = substr( $x, 1 );
+ }
+ $num = str_replace( '-', '', $isbn );
+ $num = str_replace( ' ', '', $num );
+ $num = str_replace( 'x', 'X', $num );
+
+ if ( '' == $num ) {
+ $text .= "ISBN $blank$x";
+ } else {
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' );
+ $text .= '<a href="' .
+ $titleObj->escapeLocalUrl( 'isbn='.$num ) .
+ "\" class=\"internal\">ISBN $isbn</a>";
+ $text .= $x;
+ }
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Return an HTML link for the "RFC 1234" text
+ *
+ * @private
+ * @param string $text Text to be processed
+ * @param string $keyword Magic keyword to use (default RFC)
+ * @param string $urlmsg Interface message to use (default rfcurl)
+ * @return string
+ */
+ function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl' ) {
+
+ $valid = '0123456789';
+ $internal = false;
+
+ $a = split( $keyword, ' '.$text );
+ if ( count ( $a ) < 2 ) {
+ return $text;
+ }
+ $text = substr( array_shift( $a ), 1);
+
+ /* Check if keyword is preceed by [[.
+ * This test is made here cause of the array_shift above
+ * that prevent the test to be done in the foreach.
+ */
+ if ( substr( $text, -2 ) == '[[' ) {
+ $internal = true;
+ }
+
+ foreach ( $a as $x ) {
+ /* token might be empty if we have RFC RFC 1234 */
+ if ( $x=='' ) {
+ $text.=$keyword;
+ continue;
+ }
+
+ # hack: don't replace inside thumbnail title/alt
+ # attributes
+ if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
+ $text .= $keyword . $x;
+ continue;
+ }
+
+ $id = $blank = '' ;
+
+ /** remove and save whitespaces in $blank */
+ while ( $x{0} == ' ' ) {
+ $blank .= ' ';
+ $x = substr( $x, 1 );
+ }
+
+ /** remove and save the rfc number in $id */
+ while ( strstr( $valid, $x{0} ) != false ) {
+ $id .= $x{0};
+ $x = substr( $x, 1 );
+ }
+
+ if ( $id == '' ) {
+ /* call back stripped spaces*/
+ $text .= $keyword.$blank.$x;
+ } elseif( $internal ) {
+ /* normal link */
+ $text .= $keyword.$id.$x;
+ } else {
+ /* build the external link*/
+ $url = wfMsg( $urlmsg, $id);
+ $sk =& $this->mOptions->getSkin();
+ $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+ $text .= "<a href=\"{$url}\"{$la}>{$keyword}{$id}</a>{$x}";
+ }
+
+ /* Check if the next RFC keyword is preceed by [[ */
+ $internal = ( substr($x,-2) == '[[' );
+ }
+ return $text;
+ }
+
+ /**
+ * Transform wiki markup when saving a page by doing \r\n -> \n
+ * conversion, substitting signatures, {{subst:}} templates, etc.
+ *
+ * @param string $text the text to transform
+ * @param Title &$title the Title object for the current article
+ * @param User &$user the User object describing the current user
+ * @param ParserOptions $options parsing options
+ * @param bool $clearState whether to clear the parser state first
+ * @return string the altered wiki markup
+ * @public
+ */
+ function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) {
+ $this->mOptions = $options;
+ $this->mTitle =& $title;
+ $this->mOutputType = OT_WIKI;
+
+ if ( $clearState ) {
+ $this->clearState();
+ }
+
+ $stripState = false;
+ $pairs = array(
+ "\r\n" => "\n",
+ );
+ $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
+ $text = $this->strip( $text, $stripState, true, array( 'gallery' ) );
+ $text = $this->pstPass2( $text, $stripState, $user );
+ $text = $this->unstrip( $text, $stripState );
+ $text = $this->unstripNoWiki( $text, $stripState );
+ return $text;
+ }
+
+ /**
+ * Pre-save transform helper function
+ * @private
+ */
+ function pstPass2( $text, &$stripState, &$user ) {
+ global $wgContLang, $wgLocaltimezone;
+
+ /* Note: This is the timestamp saved as hardcoded wikitext to
+ * the database, we use $wgContLang here in order to give
+ * everyone the same signature and use the default one rather
+ * than the one selected in each user's preferences.
+ */
+ if ( isset( $wgLocaltimezone ) ) {
+ $oldtz = getenv( 'TZ' );
+ putenv( 'TZ='.$wgLocaltimezone );
+ }
+ $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) .
+ ' (' . date( 'T' ) . ')';
+ if ( isset( $wgLocaltimezone ) ) {
+ putenv( 'TZ='.$oldtz );
+ }
+
+ # Variable replacement
+ # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+ $text = $this->replaceVariables( $text );
+
+ # Strip out <nowiki> etc. added via replaceVariables
+ $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
+
+ # Signatures
+ $sigText = $this->getUserSig( $user );
+ $text = strtr( $text, array(
+ '~~~~~' => $d,
+ '~~~~' => "$sigText $d",
+ '~~~' => $sigText
+ ) );
+
+ # Context links: [[|name]] and [[name (context)|]]
+ #
+ global $wgLegalTitleChars;
+ $tc = "[$wgLegalTitleChars]";
+ $np = str_replace( array( '(', ')' ), array( '', '' ), $tc ); # No parens
+
+ $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
+ $conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
+
+ $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/"; # [[page (context)|]]
+ $p2 = "/\[\[\\|({$tc}+)]]/"; # [[|page]]
+ $p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/"; # [[namespace:page|]] and [[:namespace:page|]]
+ $p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]]
+ $context = '';
+ $t = $this->mTitle->getText();
+ if ( preg_match( $conpat, $t, $m ) ) {
+ $context = $m[2];
+ }
+ $text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text );
+ $text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text );
+ $text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text );
+
+ if ( '' == $context ) {
+ $text = preg_replace( $p2, '[[\\1]]', $text );
+ } else {
+ $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
+ }
+
+ # Trim trailing whitespace
+ # MAG_END (__END__) tag allows for trailing
+ # whitespace to be deliberately included
+ $text = rtrim( $text );
+ $mw =& MagicWord::get( MAG_END );
+ $mw->matchAndRemove( $text );
+
+ return $text;
+ }
+
+ /**
+ * Fetch the user's signature text, if any, and normalize to
+ * validated, ready-to-insert wikitext.
+ *
+ * @param User $user
+ * @return string
+ * @private
+ */
+ function getUserSig( &$user ) {
+ $username = $user->getName();
+ $nickname = $user->getOption( 'nickname' );
+ $nickname = $nickname === '' ? $username : $nickname;
+
+ if( $user->getBoolOption( 'fancysig' ) !== false ) {
+ # Sig. might contain markup; validate this
+ if( $this->validateSig( $nickname ) !== false ) {
+ # Validated; clean up (if needed) and return it
+ return $this->cleanSig( $nickname, true );
+ } else {
+ # Failed to validate; fall back to the default
+ $nickname = $username;
+ wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+ }
+ }
+
+ // Make sure nickname doesnt get a sig in a sig
+ $nickname = $this->cleanSigInSig( $nickname );
+
+ # If we're still here, make it a link to the user page
+ $userpage = $user->getUserPage();
+ return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' );
+ }
+
+ /**
+ * Check that the user's signature contains no bad XML
+ *
+ * @param string $text
+ * @return mixed An expanded string, or false if invalid.
+ */
+ function validateSig( $text ) {
+ return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+ }
+
+ /**
+ * Clean up signature text
+ *
+ * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
+ * 2) Substitute all transclusions
+ *
+ * @param string $text
+ * @param $parsing Whether we're cleaning (preferences save) or parsing
+ * @return string Signature text
+ */
+ function cleanSig( $text, $parsing = false ) {
+ global $wgTitle;
+ $this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG );
+
+ $substWord = MagicWord::get( MAG_SUBST );
+ $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
+ $substText = '{{' . $substWord->getSynonym( 0 );
+
+ $text = preg_replace( $substRegex, $substText, $text );
+ $text = $this->cleanSigInSig( $text );
+ $text = $this->replaceVariables( $text );
+
+ $this->clearState();
+ return $text;
+ }
+
+ /**
+ * Strip ~~~, ~~~~ and ~~~~~ out of signatures
+ * @param string $text
+ * @return string Signature text with /~{3,5}/ removed
+ */
+ function cleanSigInSig( $text ) {
+ $text = preg_replace( '/~{3,5}/', '', $text );
+ return $text;
+ }
+
+ /**
+ * Set up some variables which are usually set up in parse()
+ * so that an external function can call some class members with confidence
+ * @public
+ */
+ function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+ $this->mTitle =& $title;
+ $this->mOptions = $options;
+ $this->mOutputType = $outputType;
+ if ( $clearState ) {
+ $this->clearState();
+ }
+ }
+
+ /**
+ * Transform a MediaWiki message by replacing magic variables.
+ *
+ * @param string $text the text to transform
+ * @param ParserOptions $options options
+ * @return string the text with variables substituted
+ * @public
+ */
+ function transformMsg( $text, $options ) {
+ global $wgTitle;
+ static $executing = false;
+
+ $fname = "Parser::transformMsg";
+
+ # Guard against infinite recursion
+ if ( $executing ) {
+ return $text;
+ }
+ $executing = true;
+
+ wfProfileIn($fname);
+
+ $this->mTitle = $wgTitle;
+ $this->mOptions = $options;
+ $this->mOutputType = OT_MSG;
+ $this->clearState();
+ $text = $this->replaceVariables( $text );
+
+ $executing = false;
+ wfProfileOut($fname);
+ return $text;
+ }
+
+ /**
+ * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+ * The callback should have the following form:
+ * function myParserHook( $text, $params, &$parser ) { ... }
+ *
+ * Transform and return $text. Use $parser for any required context, e.g. use
+ * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
+ *
+ * @public
+ *
+ * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
+ * @param mixed $callback The callback function (and object) to use for the tag
+ *
+ * @return The old value of the mTagHooks array associated with the hook
+ */
+ function setHook( $tag, $callback ) {
+ $tag = strtolower( $tag );
+ $oldVal = @$this->mTagHooks[$tag];
+ $this->mTagHooks[$tag] = $callback;
+
+ return $oldVal;
+ }
+
+ /**
+ * Create a function, e.g. {{sum:1|2|3}}
+ * The callback function should have the form:
+ * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
+ *
+ * The callback may either return the text result of the function, or an array with the text
+ * in element 0, and a number of flags in the other elements. The names of the flags are
+ * specified in the keys. Valid flags are:
+ * found The text returned is valid, stop processing the template. This
+ * is on by default.
+ * nowiki Wiki markup in the return value should be escaped
+ * noparse Unsafe HTML tags should not be stripped, etc.
+ * noargs Don't replace triple-brace arguments in the return value
+ * isHTML The returned text is HTML, armour it against wikitext transformation
+ *
+ * @public
+ *
+ * @param mixed $id The magic word ID, or (deprecated) the function name. Function names are case-insensitive.
+ * @param mixed $callback The callback function (and object) to use
+ * @param integer $flags a combination of the following flags:
+ * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
+ *
+ * @return The old callback function for this name, if any
+ */
+ function setFunctionHook( $id, $callback, $flags = 0 ) {
+ if( is_string( $id ) ) {
+ $id = strtolower( $id );
+ }
+ $oldVal = @$this->mFunctionHooks[$id];
+ $this->mFunctionHooks[$id] = $callback;
+
+ # Add to function cache
+ if ( is_int( $id ) ) {
+ $mw = MagicWord::get( $id );
+ $synonyms = $mw->getSynonyms();
+ $sensitive = intval( $mw->isCaseSensitive() );
+ } else {
+ $synonyms = array( $id );
+ $sensitive = 0;
+ }
+
+ foreach ( $synonyms as $syn ) {
+ # Case
+ if ( !$sensitive ) {
+ $syn = strtolower( $syn );
+ }
+ # Add leading hash
+ if ( !( $flags & SFH_NO_HASH ) ) {
+ $syn = '#' . $syn;
+ }
+ # Remove trailing colon
+ if ( substr( $syn, -1, 1 ) == ':' ) {
+ $syn = substr( $syn, 0, -1 );
+ }
+ $this->mFunctionSynonyms[$sensitive][$syn] = $id;
+ }
+ return $oldVal;
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with actual links, in the buffer
+ * Placeholders created in Skin::makeLinkObj()
+ * Returns an array of links found, indexed by PDBK:
+ * 0 - broken
+ * 1 - normal link
+ * 2 - stub
+ * $options is a bit field, RLH_FOR_UPDATE to select for update
+ */
+ function replaceLinkHolders( &$text, $options = 0 ) {
+ global $wgUser;
+ global $wgOutputReplace;
+
+ $fname = 'Parser::replaceLinkHolders';
+ wfProfileIn( $fname );
+
+ $pdbks = array();
+ $colours = array();
+ $sk =& $this->mOptions->getSkin();
+ $linkCache =& LinkCache::singleton();
+
+ if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
+ wfProfileIn( $fname.'-check' );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $threshold = $wgUser->getOption('stubthreshold');
+
+ # Sort by namespace
+ asort( $this->mLinkHolders['namespaces'] );
+
+ # Generate query
+ $query = false;
+ foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+ # Make title object
+ $title = $this->mLinkHolders['titles'][$key];
+
+ # Skip invalid entries.
+ # Result will be ugly, but prevents crash.
+ if ( is_null( $title ) ) {
+ continue;
+ }
+ $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
+
+ # Check if it's a static known link, e.g. interwiki
+ if ( $title->isAlwaysKnown() ) {
+ $colours[$pdbk] = 1;
+ } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+ $colours[$pdbk] = 1;
+ $this->mOutput->addLink( $title, $id );
+ } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+ $colours[$pdbk] = 0;
+ } else {
+ # Not in the link cache, add it to the query
+ if ( !isset( $current ) ) {
+ $current = $ns;
+ $query = "SELECT page_id, page_namespace, page_title";
+ if ( $threshold > 0 ) {
+ $query .= ', page_len, page_is_redirect';
+ }
+ $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
+ } elseif ( $current != $ns ) {
+ $current = $ns;
+ $query .= ")) OR (page_namespace=$ns AND page_title IN(";
+ } else {
+ $query .= ', ';
+ }
+
+ $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
+ }
+ }
+ if ( $query ) {
+ $query .= '))';
+ if ( $options & RLH_FOR_UPDATE ) {
+ $query .= ' FOR UPDATE';
+ }
+
+ $res = $dbr->query( $query, $fname );
+
+ # Fetch data and form into an associative array
+ # non-existent = broken
+ # 1 = known
+ # 2 = stub
+ while ( $s = $dbr->fetchObject($res) ) {
+ $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $pdbk = $title->getPrefixedDBkey();
+ $linkCache->addGoodLinkObj( $s->page_id, $title );
+ $this->mOutput->addLink( $title, $s->page_id );
+
+ if ( $threshold > 0 ) {
+ $size = $s->page_len;
+ if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
+ $colours[$pdbk] = 1;
+ } else {
+ $colours[$pdbk] = 2;
+ }
+ } else {
+ $colours[$pdbk] = 1;
+ }
+ }
+ }
+ wfProfileOut( $fname.'-check' );
+
+ # Construct search and replace arrays
+ wfProfileIn( $fname.'-construct' );
+ $wgOutputReplace = array();
+ foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+ $pdbk = $pdbks[$key];
+ $searchkey = "<!--LINK $key-->";
+ $title = $this->mLinkHolders['titles'][$key];
+ if ( empty( $colours[$pdbk] ) ) {
+ $linkCache->addBadLinkObj( $title );
+ $colours[$pdbk] = 0;
+ $this->mOutput->addLink( $title, 0 );
+ $wgOutputReplace[$searchkey] = $sk->makeBrokenLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ } elseif ( $colours[$pdbk] == 1 ) {
+ $wgOutputReplace[$searchkey] = $sk->makeKnownLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ } elseif ( $colours[$pdbk] == 2 ) {
+ $wgOutputReplace[$searchkey] = $sk->makeStubLinkObj( $title,
+ $this->mLinkHolders['texts'][$key],
+ $this->mLinkHolders['queries'][$key] );
+ }
+ }
+ wfProfileOut( $fname.'-construct' );
+
+ # Do the thing
+ wfProfileIn( $fname.'-replace' );
+
+ $text = preg_replace_callback(
+ '/(<!--LINK .*?-->)/',
+ "wfOutputReplaceMatches",
+ $text);
+
+ wfProfileOut( $fname.'-replace' );
+ }
+
+ # Now process interwiki link holders
+ # This is quite a bit simpler than internal links
+ if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
+ wfProfileIn( $fname.'-interwiki' );
+ # Make interwiki link HTML
+ $wgOutputReplace = array();
+ foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
+ $title = $this->mInterwikiLinkHolders['titles'][$key];
+ $wgOutputReplace[$key] = $sk->makeLinkObj( $title, $link );
+ }
+
+ $text = preg_replace_callback(
+ '/<!--IWLINK (.*?)-->/',
+ "wfOutputReplaceMatches",
+ $text );
+ wfProfileOut( $fname.'-interwiki' );
+ }
+
+ wfProfileOut( $fname );
+ return $colours;
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with plain text of links
+ * (not HTML-formatted).
+ * @param string $text
+ * @return string
+ */
+ function replaceLinkHoldersText( $text ) {
+ $fname = 'Parser::replaceLinkHoldersText';
+ wfProfileIn( $fname );
+
+ $text = preg_replace_callback(
+ '/<!--(LINK|IWLINK) (.*?)-->/',
+ array( &$this, 'replaceLinkHoldersTextCallback' ),
+ $text );
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function replaceLinkHoldersTextCallback( $matches ) {
+ $type = $matches[1];
+ $key = $matches[2];
+ if( $type == 'LINK' ) {
+ if( isset( $this->mLinkHolders['texts'][$key] ) ) {
+ return $this->mLinkHolders['texts'][$key];
+ }
+ } elseif( $type == 'IWLINK' ) {
+ if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
+ return $this->mInterwikiLinkHolders['texts'][$key];
+ }
+ }
+ return $matches[0];
+ }
+
+ /**
+ * Tag hook handler for 'pre'.
+ */
+ function renderPreTag( $text, $attribs, $parser ) {
+ // Backwards-compatibility hack
+ $content = preg_replace( '!<nowiki>(.*?)</nowiki>!is', '\\1', $text );
+
+ $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+ return wfOpenElement( 'pre', $attribs ) .
+ wfEscapeHTMLTagsOnly( $content ) .
+ '</pre>';
+ }
+
+ /**
+ * Renders an image gallery from a text with one line per image.
+ * text labels may be given by using |-style alternative text. E.g.
+ * Image:one.jpg|The number "1"
+ * Image:tree.jpg|A tree
+ * given as text will return the HTML of a gallery with two images,
+ * labeled 'The number "1"' and
+ * 'A tree'.
+ */
+ function renderImageGallery( $text, $params ) {
+ $ig = new ImageGallery();
+ $ig->setShowBytes( false );
+ $ig->setShowFilename( false );
+ $ig->setParsing();
+ $ig->useSkin( $this->mOptions->getSkin() );
+
+ if( isset( $params['caption'] ) )
+ $ig->setCaption( $params['caption'] );
+
+ $lines = explode( "\n", $text );
+ foreach ( $lines as $line ) {
+ # match lines like these:
+ # Image:someimage.jpg|This is some image
+ preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
+ # Skip empty lines
+ if ( count( $matches ) == 0 ) {
+ continue;
+ }
+ $nt =& Title::newFromText( $matches[1] );
+ if( is_null( $nt ) ) {
+ # Bogus title. Ignore these so we don't bomb out later.
+ continue;
+ }
+ if ( isset( $matches[3] ) ) {
+ $label = $matches[3];
+ } else {
+ $label = '';
+ }
+
+ $pout = $this->parse( $label,
+ $this->mTitle,
+ $this->mOptions,
+ false, // Strip whitespace...?
+ false // Don't clear state!
+ );
+ $html = $pout->getText();
+
+ $ig->add( new Image( $nt ), $html );
+
+ # Only add real images (bug #5586)
+ if ( $nt->getNamespace() == NS_IMAGE ) {
+ $this->mOutput->addImage( $nt->getDBkey() );
+ }
+ }
+ return $ig->toHTML();
+ }
+
+ /**
+ * Parse image options text and use it to make an image
+ */
+ function makeImage( &$nt, $options ) {
+ global $wgUseImageResize;
+
+ $align = '';
+
+ # Check if the options text is of the form "options|alt text"
+ # Options are:
+ # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
+ # * left no resizing, just left align. label is used for alt= only
+ # * right same, but right aligned
+ # * none same, but not aligned
+ # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
+ # * center center the image
+ # * framed Keep original image size, no magnify-button.
+
+ $part = explode( '|', $options);
+
+ $mwThumb =& MagicWord::get( MAG_IMG_THUMBNAIL );
+ $mwManualThumb =& MagicWord::get( MAG_IMG_MANUALTHUMB );
+ $mwLeft =& MagicWord::get( MAG_IMG_LEFT );
+ $mwRight =& MagicWord::get( MAG_IMG_RIGHT );
+ $mwNone =& MagicWord::get( MAG_IMG_NONE );
+ $mwWidth =& MagicWord::get( MAG_IMG_WIDTH );
+ $mwCenter =& MagicWord::get( MAG_IMG_CENTER );
+ $mwFramed =& MagicWord::get( MAG_IMG_FRAMED );
+ $caption = '';
+
+ $width = $height = $framed = $thumb = false;
+ $manual_thumb = '' ;
+
+ foreach( $part as $key => $val ) {
+ if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) {
+ $thumb=true;
+ } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) {
+ # use manually specified thumbnail
+ $thumb=true;
+ $manual_thumb = $match;
+ } elseif ( ! is_null( $mwRight->matchVariableStartToEnd($val) ) ) {
+ # remember to set an alignment, don't render immediately
+ $align = 'right';
+ } elseif ( ! is_null( $mwLeft->matchVariableStartToEnd($val) ) ) {
+ # remember to set an alignment, don't render immediately
+ $align = 'left';
+ } elseif ( ! is_null( $mwCenter->matchVariableStartToEnd($val) ) ) {
+ # remember to set an alignment, don't render immediately
+ $align = 'center';
+ } elseif ( ! is_null( $mwNone->matchVariableStartToEnd($val) ) ) {
+ # remember to set an alignment, don't render immediately
+ $align = 'none';
+ } elseif ( $wgUseImageResize && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) {
+ wfDebug( "MAG_IMG_WIDTH match: $match\n" );
+ # $match is the image width in pixels
+ if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) {
+ $width = intval( $m[1] );
+ $height = intval( $m[2] );
+ } else {
+ $width = intval($match);
+ }
+ } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) {
+ $framed=true;
+ } else {
+ $caption = $val;
+ }
+ }
+ # Strip bad stuff out of the alt text
+ $alt = $this->replaceLinkHoldersText( $caption );
+
+ # make sure there are no placeholders in thumbnail attributes
+ # that are later expanded to html- so expand them now and
+ # remove the tags
+ $alt = $this->unstrip($alt, $this->mStripState);
+ $alt = Sanitizer::stripAllTags( $alt );
+
+ # Linker does the rest
+ $sk =& $this->mOptions->getSkin();
+ return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb );
+ }
+
+ /**
+ * Set a flag in the output object indicating that the content is dynamic and
+ * shouldn't be cached.
+ */
+ function disableCache() {
+ wfDebug( "Parser output marked as uncacheable.\n" );
+ $this->mOutput->mCacheTime = -1;
+ }
+
+ /**#@+
+ * Callback from the Sanitizer for expanding items found in HTML attribute
+ * values, so they can be safely tested and escaped.
+ * @param string $text
+ * @param array $args
+ * @return string
+ * @private
+ */
+ function attributeStripCallback( &$text, $args ) {
+ $text = $this->replaceVariables( $text, $args );
+ $text = $this->unstripForHTML( $text );
+ return $text;
+ }
+
+ function unstripForHTML( $text ) {
+ $text = $this->unstrip( $text, $this->mStripState );
+ $text = $this->unstripNoWiki( $text, $this->mStripState );
+ return $text;
+ }
+ /**#@-*/
+
+ /**#@+
+ * Accessor/mutator
+ */
+ function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
+ function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
+ function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
+ /**#@-*/
+
+ /**#@+
+ * Accessor
+ */
+ function getTags() { return array_keys( $this->mTagHooks ); }
+ /**#@-*/
+
+
+ /**
+ * Break wikitext input into sections, and either pull or replace
+ * some particular section's text.
+ *
+ * External callers should use the getSection and replaceSection methods.
+ *
+ * @param $text Page wikitext
+ * @param $section Numbered section. 0 pulls the text before the first
+ * heading; other numbers will pull the given section
+ * along with its lower-level subsections.
+ * @param $mode One of "get" or "replace"
+ * @param $newtext Replacement text for section data.
+ * @return string for "get", the extracted section text.
+ * for "replace", the whole page with the section replaced.
+ */
+ private function extractSections( $text, $section, $mode, $newtext='' ) {
+ # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
+ # comments to be stripped as well)
+ $striparray = array();
+
+ $oldOutputType = $this->mOutputType;
+ $oldOptions = $this->mOptions;
+ $this->mOptions = new ParserOptions();
+ $this->mOutputType = OT_WIKI;
+
+ $striptext = $this->strip( $text, $striparray, true );
+
+ $this->mOutputType = $oldOutputType;
+ $this->mOptions = $oldOptions;
+
+ # now that we can be sure that no pseudo-sections are in the source,
+ # split it up by section
+ $uniq = preg_quote( $this->uniqPrefix(), '/' );
+ $comment = "(?:$uniq-!--.*?QINU)";
+ $secs = preg_split(
+ /*
+ "/
+ ^(
+ (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+ (?:
+ (=+) # Should this be limited to 6?
+ .+? # Section title...
+ \\2 # Ending = count must match start
+ |
+ ^
+ <h([1-6])\b.*?>
+ .*?
+ <\/h\\3\s*>
+ )
+ (?:$comment|<\/?noinclude>|\s+)* # Trailing whitespace ok
+ )$
+ /mix",
+ */
+ "/
+ (
+ ^
+ (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+ (=+) # Should this be limited to 6?
+ .+? # Section title...
+ \\2 # Ending = count must match start
+ (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
+ $
+ |
+ <h([1-6])\b.*?>
+ .*?
+ <\/h\\3\s*>
+ )
+ /mix",
+ $striptext, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ if( $mode == "get" ) {
+ if( $section == 0 ) {
+ // "Section 0" returns the content before any other section.
+ $rv = $secs[0];
+ } else {
+ $rv = "";
+ }
+ } elseif( $mode == "replace" ) {
+ if( $section == 0 ) {
+ $rv = $newtext . "\n\n";
+ $remainder = true;
+ } else {
+ $rv = $secs[0];
+ $remainder = false;
+ }
+ }
+ $count = 0;
+ $sectionLevel = 0;
+ for( $index = 1; $index < count( $secs ); ) {
+ $headerLine = $secs[$index++];
+ if( $secs[$index] ) {
+ // A wiki header
+ $headerLevel = strlen( $secs[$index++] );
+ } else {
+ // An HTML header
+ $index++;
+ $headerLevel = intval( $secs[$index++] );
+ }
+ $content = $secs[$index++];
+
+ $count++;
+ if( $mode == "get" ) {
+ if( $count == $section ) {
+ $rv = $headerLine . $content;
+ $sectionLevel = $headerLevel;
+ } elseif( $count > $section ) {
+ if( $sectionLevel && $headerLevel > $sectionLevel ) {
+ $rv .= $headerLine . $content;
+ } else {
+ // Broke out to a higher-level section
+ break;
+ }
+ }
+ } elseif( $mode == "replace" ) {
+ if( $count < $section ) {
+ $rv .= $headerLine . $content;
+ } elseif( $count == $section ) {
+ $rv .= $newtext . "\n\n";
+ $sectionLevel = $headerLevel;
+ } elseif( $count > $section ) {
+ if( $headerLevel <= $sectionLevel ) {
+ // Passed the section's sub-parts.
+ $remainder = true;
+ }
+ if( $remainder ) {
+ $rv .= $headerLine . $content;
+ }
+ }
+ }
+ }
+ # reinsert stripped tags
+ $rv = $this->unstrip( $rv, $striparray );
+ $rv = $this->unstripNoWiki( $rv, $striparray );
+ $rv = trim( $rv );
+ return $rv;
+ }
+
+ /**
+ * This function returns the text of a section, specified by a number ($section).
+ * A section is text under a heading like == Heading == or \<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
+ */
+ function getSection( $text, $section ) {
+ return $this->extractSections( $text, $section, "get" );
+ }
+
+ function replaceSection( $oldtext, $section, $text ) {
+ return $this->extractSections( $oldtext, $section, "replace", $text );
+ }
+
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class ParserOutput
+{
+ var $mText, # The output text
+ $mLanguageLinks, # List of the full text of language links, in the order they appear
+ $mCategories, # Map of category names to sort keys
+ $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
+ $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
+ $mVersion, # Compatibility check
+ $mTitleText, # title text of the chosen language variant
+ $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
+ $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
+ $mImages, # DB keys of the images used, in the array key only
+ $mExternalLinks, # External link URLs, in the key only
+ $mHTMLtitle, # Display HTML title
+ $mSubtitle, # Additional subtitle
+ $mNewSection, # Show a new section link?
+ $mNoGallery; # No gallery on category page? (__NOGALLERY__)
+
+ function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
+ $containsOldMagic = false, $titletext = '' )
+ {
+ $this->mText = $text;
+ $this->mLanguageLinks = $languageLinks;
+ $this->mCategories = $categoryLinks;
+ $this->mContainsOldMagic = $containsOldMagic;
+ $this->mCacheTime = '';
+ $this->mVersion = MW_PARSER_VERSION;
+ $this->mTitleText = $titletext;
+ $this->mLinks = array();
+ $this->mTemplates = array();
+ $this->mImages = array();
+ $this->mExternalLinks = array();
+ $this->mHTMLtitle = "" ;
+ $this->mSubtitle = "" ;
+ $this->mNewSection = false;
+ $this->mNoGallery = false;
+ }
+
+ function getText() { return $this->mText; }
+ function &getLanguageLinks() { return $this->mLanguageLinks; }
+ function getCategoryLinks() { return array_keys( $this->mCategories ); }
+ function &getCategories() { return $this->mCategories; }
+ function getCacheTime() { return $this->mCacheTime; }
+ function getTitleText() { return $this->mTitleText; }
+ function &getLinks() { return $this->mLinks; }
+ function &getTemplates() { return $this->mTemplates; }
+ function &getImages() { return $this->mImages; }
+ function &getExternalLinks() { return $this->mExternalLinks; }
+ function getNoGallery() { return $this->mNoGallery; }
+
+ function containsOldMagic() { return $this->mContainsOldMagic; }
+ function setText( $text ) { return wfSetVar( $this->mText, $text ); }
+ function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
+ function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); }
+ function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
+ function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
+ function setTitleText( $t ) { return wfSetVar ($this->mTitleText, $t); }
+
+ function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; }
+ function addImage( $name ) { $this->mImages[$name] = 1; }
+ function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; }
+ function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; }
+
+ function setNewSection( $value ) {
+ $this->mNewSection = (bool)$value;
+ }
+ function getNewSection() {
+ return (bool)$this->mNewSection;
+ }
+
+ function addLink( $title, $id ) {
+ $ns = $title->getNamespace();
+ $dbk = $title->getDBkey();
+ if ( !isset( $this->mLinks[$ns] ) ) {
+ $this->mLinks[$ns] = array();
+ }
+ $this->mLinks[$ns][$dbk] = $id;
+ }
+
+ function addTemplate( $title, $id ) {
+ $ns = $title->getNamespace();
+ $dbk = $title->getDBkey();
+ if ( !isset( $this->mTemplates[$ns] ) ) {
+ $this->mTemplates[$ns] = array();
+ }
+ $this->mTemplates[$ns][$dbk] = $id;
+ }
+
+ /**
+ * Return true if this cached output object predates the global or
+ * per-article cache invalidation timestamps, or if it comes from
+ * an incompatible older version.
+ *
+ * @param string $touched the affected article's last touched timestamp
+ * @return bool
+ * @public
+ */
+ function expired( $touched ) {
+ global $wgCacheEpoch;
+ return $this->getCacheTime() == -1 || // parser says it's uncacheable
+ $this->getCacheTime() < $touched ||
+ $this->getCacheTime() <= $wgCacheEpoch ||
+ !isset( $this->mVersion ) ||
+ version_compare( $this->mVersion, MW_PARSER_VERSION, "lt" );
+ }
+}
+
+/**
+ * Set options of the Parser
+ * @todo document
+ * @package MediaWiki
+ */
+class ParserOptions
+{
+ # All variables are private
+ var $mUseTeX; # Use texvc to expand <math> tags
+ var $mUseDynamicDates; # Use DateFormatter to format dates
+ var $mInterwikiMagic; # Interlanguage links are removed and returned in an array
+ var $mAllowExternalImages; # Allow external images inline
+ var $mAllowExternalImagesFrom; # If not, any exception?
+ var $mSkin; # Reference to the preferred skin
+ var $mDateFormat; # Date format index
+ var $mEditSection; # Create "edit section" links
+ var $mNumberHeadings; # Automatically number headings
+ var $mAllowSpecialInclusion; # Allow inclusion of special pages
+ var $mTidy; # Ask for tidy cleanup
+ var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR
+
+ var $mUser; # Stored user object, just used to initialise the skin
+
+ function getUseTeX() { return $this->mUseTeX; }
+ function getUseDynamicDates() { return $this->mUseDynamicDates; }
+ function getInterwikiMagic() { return $this->mInterwikiMagic; }
+ function getAllowExternalImages() { return $this->mAllowExternalImages; }
+ function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; }
+ function getDateFormat() { return $this->mDateFormat; }
+ function getEditSection() { return $this->mEditSection; }
+ function getNumberHeadings() { return $this->mNumberHeadings; }
+ function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; }
+ function getTidy() { return $this->mTidy; }
+ function getInterfaceMessage() { return $this->mInterfaceMessage; }
+
+ function &getSkin() {
+ if ( !isset( $this->mSkin ) ) {
+ $this->mSkin = $this->mUser->getSkin();
+ }
+ return $this->mSkin;
+ }
+
+ function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); }
+ function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
+ function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
+ function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
+ function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
+ function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
+ function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
+ function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
+ function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
+ function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); }
+ function setSkin( &$x ) { $this->mSkin =& $x; }
+ function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); }
+
+ function ParserOptions( $user = null ) {
+ $this->initialiseFromUser( $user );
+ }
+
+ /**
+ * Get parser options
+ * @static
+ */
+ function newFromUser( &$user ) {
+ return new ParserOptions( $user );
+ }
+
+ /** Get user options */
+ function initialiseFromUser( &$userInput ) {
+ global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
+ global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion;
+ $fname = 'ParserOptions::initialiseFromUser';
+ wfProfileIn( $fname );
+ if ( !$userInput ) {
+ global $wgUser;
+ if ( isset( $wgUser ) ) {
+ $user = $wgUser;
+ } else {
+ $user = new User;
+ $user->setLoaded( true );
+ }
+ } else {
+ $user =& $userInput;
+ }
+
+ $this->mUser = $user;
+
+ $this->mUseTeX = $wgUseTeX;
+ $this->mUseDynamicDates = $wgUseDynamicDates;
+ $this->mInterwikiMagic = $wgInterwikiMagic;
+ $this->mAllowExternalImages = $wgAllowExternalImages;
+ $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
+ $this->mSkin = null; # Deferred
+ $this->mDateFormat = $user->getOption( 'date' );
+ $this->mEditSection = true;
+ $this->mNumberHeadings = $user->getOption( 'numberheadings' );
+ $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
+ $this->mTidy = false;
+ $this->mInterfaceMessage = false;
+ wfProfileOut( $fname );
+ }
+}
+
+/**
+ * Callback function used by Parser::replaceLinkHolders()
+ * to substitute link placeholders.
+ */
+function &wfOutputReplaceMatches( $matches ) {
+ global $wgOutputReplace;
+ return $wgOutputReplace[$matches[1]];
+}
+
+/**
+ * Return the total number of articles
+ */
+function wfNumberOfArticles() {
+ global $wgNumberOfArticles;
+
+ wfLoadSiteStats();
+ return $wgNumberOfArticles;
+}
+
+/**
+ * Return the number of files
+ */
+function wfNumberOfFiles() {
+ $fname = 'wfNumberOfFiles';
+
+ wfProfileIn( $fname );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $numImages = $dbr->selectField('site_stats', 'ss_images', array(), $fname );
+ wfProfileOut( $fname );
+
+ return $numImages;
+}
+
+/**
+ * Return the number of user accounts
+ * @return integer
+ */
+function wfNumberOfUsers() {
+ wfProfileIn( 'wfNumberOfUsers' );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $count = $dbr->selectField( 'site_stats', 'ss_users', array(), 'wfNumberOfUsers' );
+ wfProfileOut( 'wfNumberOfUsers' );
+ return (int)$count;
+}
+
+/**
+ * Return the total number of pages
+ * @return integer
+ */
+function wfNumberOfPages() {
+ wfProfileIn( 'wfNumberOfPages' );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $count = $dbr->selectField( 'site_stats', 'ss_total_pages', array(), 'wfNumberOfPages' );
+ wfProfileOut( 'wfNumberOfPages' );
+ return (int)$count;
+}
+
+/**
+ * Return the total number of admins
+ *
+ * @return integer
+ */
+function wfNumberOfAdmins() {
+ static $admins = -1;
+ wfProfileIn( 'wfNumberOfAdmins' );
+ if( $admins == -1 ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), 'wfNumberOfAdmins' );
+ }
+ wfProfileOut( 'wfNumberOfAdmins' );
+ return (int)$admins;
+}
+
+/**
+ * Count the number of pages in a particular namespace
+ *
+ * @param $ns Namespace
+ * @return integer
+ */
+function wfPagesInNs( $ns ) {
+ static $pageCount = array();
+ wfProfileIn( 'wfPagesInNs' );
+ if( !isset( $pageCount[$ns] ) ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $pageCount[$ns] = $dbr->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $ns ), 'wfPagesInNs' );
+ }
+ wfProfileOut( 'wfPagesInNs' );
+ return (int)$pageCount[$ns];
+}
+
+/**
+ * Get various statistics from the database
+ * @private
+ */
+function wfLoadSiteStats() {
+ global $wgNumberOfArticles, $wgTotalViews, $wgTotalEdits;
+ $fname = 'wfLoadSiteStats';
+
+ if ( -1 != $wgNumberOfArticles ) return;
+ $dbr =& wfGetDB( DB_SLAVE );
+ $s = $dbr->selectRow( 'site_stats',
+ array( 'ss_total_views', 'ss_total_edits', 'ss_good_articles' ),
+ array( 'ss_row_id' => 1 ), $fname
+ );
+
+ if ( $s === false ) {
+ return;
+ } else {
+ $wgTotalViews = $s->ss_total_views;
+ $wgTotalEdits = $s->ss_total_edits;
+ $wgNumberOfArticles = $s->ss_good_articles;
+ }
+}
+
+/**
+ * Escape html tags
+ * Basically replacing " > and < with HTML entities ( &quot;, &gt;, &lt;)
+ *
+ * @param $in String: text that might contain HTML tags.
+ * @return string Escaped string
+ */
+function wfEscapeHTMLTagsOnly( $in ) {
+ return str_replace(
+ array( '"', '>', '<' ),
+ array( '&quot;', '&gt;', '&lt;' ),
+ $in );
+}
+
+?>
diff --git a/includes/ParserCache.php b/includes/ParserCache.php
new file mode 100644
index 00000000..3ec7512f
--- /dev/null
+++ b/includes/ParserCache.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage Cache
+ */
+
+/**
+ *
+ * @package MediaWiki
+ */
+class ParserCache {
+ /**
+ * Get an instance of this object
+ */
+ function &singleton() {
+ static $instance;
+ if ( !isset( $instance ) ) {
+ global $parserMemc;
+ $instance = new ParserCache( $parserMemc );
+ }
+ return $instance;
+ }
+
+ /**
+ * Setup a cache pathway with a given back-end storage mechanism.
+ * May be a memcached client or a BagOStuff derivative.
+ *
+ * @param object $memCached
+ */
+ function ParserCache( &$memCached ) {
+ $this->mMemc =& $memCached;
+ }
+
+ function getKey( &$article, &$user ) {
+ global $wgDBname, $action;
+ $hash = $user->getPageRenderingHash();
+ if( !$article->mTitle->userCanEdit() ) {
+ // section edit links are suppressed even if the user has them on
+ $edit = '!edit=0';
+ } else {
+ $edit = '';
+ }
+ $pageid = intval( $article->getID() );
+ $renderkey = (int)($action == 'render');
+ $key = "$wgDBname:pcache:idhash:$pageid-$renderkey!$hash$edit";
+ return $key;
+ }
+
+ function getETag( &$article, &$user ) {
+ return 'W/"' . $this->getKey($article, $user) . "--" . $article->mTouched. '"';
+ }
+
+ function get( &$article, &$user ) {
+ global $wgCacheEpoch;
+ $fname = 'ParserCache::get';
+ wfProfileIn( $fname );
+
+ $hash = $user->getPageRenderingHash();
+ $pageid = intval( $article->getID() );
+ $key = $this->getKey( $article, $user );
+
+ wfDebug( "Trying parser cache $key\n" );
+ $value = $this->mMemc->get( $key );
+ if ( is_object( $value ) ) {
+ wfDebug( "Found.\n" );
+ # Delete if article has changed since the cache was made
+ $canCache = $article->checkTouched();
+ $cacheTime = $value->getCacheTime();
+ $touched = $article->mTouched;
+ if ( !$canCache || $value->expired( $touched ) ) {
+ if ( !$canCache ) {
+ wfIncrStats( "pcache_miss_invalid" );
+ wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+ } else {
+ wfIncrStats( "pcache_miss_expired" );
+ wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+ }
+ $this->mMemc->delete( $key );
+ $value = false;
+ } else {
+ if ( isset( $value->mTimestamp ) ) {
+ $article->mTimestamp = $value->mTimestamp;
+ }
+ wfIncrStats( "pcache_hit" );
+ }
+ } else {
+ wfDebug( "Parser cache miss.\n" );
+ wfIncrStats( "pcache_miss_absent" );
+ $value = false;
+ }
+
+ wfProfileOut( $fname );
+ return $value;
+ }
+
+ function save( $parserOutput, &$article, &$user ){
+ global $wgParserCacheExpireTime;
+ $key = $this->getKey( $article, $user );
+
+ if( $parserOutput->getCacheTime() != -1 ) {
+
+ $now = wfTimestampNow();
+ $parserOutput->setCacheTime( $now );
+
+ // Save the timestamp so that we don't have to load the revision row on view
+ $parserOutput->mTimestamp = $article->getTimestamp();
+
+ $parserOutput->mText .= "\n<!-- Saved in parser cache with key $key and timestamp $now -->\n";
+ wfDebug( "Saved in parser cache with key $key and timestamp $now\n" );
+
+ if( $parserOutput->containsOldMagic() ){
+ $expire = 3600; # 1 hour
+ } else {
+ $expire = $wgParserCacheExpireTime;
+ }
+ $this->mMemc->set( $key, $parserOutput, $expire );
+
+ } else {
+ wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
+ }
+
+ }
+
+}
+
+?>
diff --git a/includes/ParserXML.php b/includes/ParserXML.php
new file mode 100644
index 00000000..e7b64f6e
--- /dev/null
+++ b/includes/ParserXML.php
@@ -0,0 +1,643 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage Experimental
+ */
+
+/** */
+require_once ('Parser.php');
+
+/**
+ * This should one day become the XML->(X)HTML parser
+ * Based on work by Jan Hidders and Magnus Manske
+ * To use, set
+ * $wgUseXMLparser = true ;
+ * $wgEnableParserCache = false ;
+ * $wgWiki2xml to the path and executable of the command line version (cli)
+ * in LocalSettings.php
+ * @package MediaWiki
+ * @subpackage Experimental
+ */
+
+/**
+ * the base class for an element
+ * @package MediaWiki
+ * @subpackage Experimental
+ */
+class element {
+ var $name = '';
+ var $attrs = array ();
+ var $children = array ();
+
+ /**
+ * This finds the ATTRS element and returns the ATTR sub-children as a single string
+ * @todo FIXME $parser always empty when calling makeXHTML()
+ */
+ function getSourceAttrs() {
+ $ret = '';
+ foreach ($this->children as $child) {
+ if (!is_string($child) AND $child->name == 'ATTRS') {
+ $ret = $child->makeXHTML($parser);
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * This collects the ATTR thingies for getSourceAttrs()
+ */
+ function getTheseAttrs() {
+ $ret = array ();
+ foreach ($this->children as $child) {
+ if (!is_string($child) AND $child->name == 'ATTR') {
+ $ret[] = $child->attrs["NAME"]."='".$child->children[0]."'";
+ }
+ }
+ return implode(' ', $ret);
+ }
+
+ function fixLinkTails(& $parser, $key) {
+ $k2 = $key +1;
+ if (!isset ($this->children[$k2]))
+ return;
+ if (!is_string($this->children[$k2]))
+ return;
+ if (is_string($this->children[$key]))
+ return;
+ if ($this->children[$key]->name != "LINK")
+ return;
+
+ $n = $this->children[$k2];
+ $s = '';
+ while ($n != '' AND (($n[0] >= 'a' AND $n[0] <= 'z') OR $n[0] == 'ä' OR $n[0] == 'ö' OR $n[0] == 'ü' OR $n[0] == 'ß')) {
+ $s .= $n[0];
+ $n = substr($n, 1);
+ }
+ $this->children[$k2] = $n;
+
+ if (count($this->children[$key]->children) > 1) {
+ $kl = array_keys($this->children[$key]->children);
+ $kl = array_pop($kl);
+ $this->children[$key]->children[$kl]->children[] = $s;
+ } else {
+ $e = new element;
+ $e->name = "LINKOPTION";
+ $t = $this->children[$key]->sub_makeXHTML($parser);
+ $e->children[] = trim($t).$s;
+ $this->children[$key]->children[] = $e;
+ }
+ }
+
+ /**
+ * This function generates the XHTML for the entire subtree
+ */
+ function sub_makeXHTML(& $parser, $tag = '', $attr = '') {
+ $ret = '';
+
+ $attr2 = $this->getSourceAttrs();
+ if ($attr != '' AND $attr2 != '')
+ $attr .= ' ';
+ $attr .= $attr2;
+
+ if ($tag != '') {
+ $ret .= '<'.$tag;
+ if ($attr != '')
+ $ret .= ' '.$attr;
+ $ret .= '>';
+ }
+
+ # THIS SHOULD BE DONE IN THE WIKI2XML-PARSER INSTEAD
+ # foreach ( array_keys ( $this->children ) AS $x )
+ # $this->fixLinkTails ( $parser , $x ) ;
+
+ foreach ($this->children as $child) {
+ if (is_string($child)) {
+ $ret .= $child;
+ } elseif ($child->name != 'ATTRS') {
+ $ret .= $child->makeXHTML($parser);
+ }
+ }
+ if ($tag != '')
+ $ret .= '</'.$tag.">\n";
+ return $ret;
+ }
+
+ /**
+ * Link functions
+ */
+ function createInternalLink(& $parser, $target, $display_title, $options) {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ $tp = explode(':', $target); # tp = target parts
+ $title = ''; # The plain title
+ $language = ''; # The language/meta/etc. part
+ $namespace = ''; # The namespace, if any
+ $subtarget = ''; # The '#' thingy
+
+ $nt = Title :: newFromText($target);
+ $fl = strtoupper($this->attrs['FORCEDLINK']) == 'YES';
+
+ if ($fl || count($tp) == 1) {
+ # Plain and simple case
+ $title = $target;
+ } else {
+ # There's stuff missing here...
+ if ($nt->getNamespace() == NS_IMAGE) {
+ $options[] = $display_title;
+ return $parser->makeImage($nt, implode('|', $options));
+ } else {
+ # Default
+ $title = $target;
+ }
+ }
+
+ if ($language != '') {
+ # External link within the WikiMedia project
+ return "{language link}";
+ } else {
+ if ($namespace != '') {
+ # Link to another namespace, check for image/media stuff
+ return "{namespace link}";
+ } else {
+ return $skin->makeLink($target, $display_title);
+ }
+ }
+ }
+
+ /** @todo document */
+ function makeInternalLink(& $parser) {
+ $target = '';
+ $option = array ();
+ foreach ($this->children as $child) {
+ if (is_string($child)) {
+ # This shouldn't be the case!
+ } else {
+ if ($child->name == 'LINKTARGET') {
+ $target = trim($child->makeXHTML($parser));
+ } else {
+ $option[] = trim($child->makeXHTML($parser));
+ }
+ }
+ }
+
+ if (count($option) == 0)
+ $option[] = $target; # Create dummy display title
+ $display_title = array_pop($option);
+ return $this->createInternalLink($parser, $target, $display_title, $option);
+ }
+
+ /** @todo document */
+ function getTemplateXHTML($title, $parts, & $parser) {
+ global $wgLang, $wgUser;
+ $skin = $wgUser->getSkin();
+ $ot = $title; # Original title
+ if (count(explode(':', $title)) == 1)
+ $title = $wgLang->getNsText(NS_TEMPLATE).":".$title;
+ $nt = Title :: newFromText($title);
+ $id = $nt->getArticleID();
+ if ($id == 0) {
+ # No/non-existing page
+ return $skin->makeBrokenLink($title, $ot);
+ }
+
+ $a = 0;
+ $tv = array (); # Template variables
+ foreach ($parts AS $part) {
+ $a ++;
+ $x = explode('=', $part, 2);
+ if (count($x) == 1)
+ $key = "{$a}";
+ else
+ $key = $x[0];
+ $value = array_pop($x);
+ $tv[$key] = $value;
+ }
+ $art = new Article($nt);
+ $text = $art->getContent(false);
+ $parser->plain_parse($text, true, $tv);
+
+ return $text;
+ }
+
+ /**
+ * This function actually converts wikiXML into XHTML tags
+ * @todo use switch() !
+ */
+ function makeXHTML(& $parser) {
+ $ret = '';
+ $n = $this->name; # Shortcut
+
+ if ($n == 'EXTENSION') {
+ # Fix allowed HTML
+ $old_n = $n;
+ $ext = strtoupper($this->attrs['NAME']);
+
+ switch($ext) {
+ case 'B':
+ case 'STRONG':
+ $n = 'BOLD';
+ break;
+ case 'I':
+ case 'EM':
+ $n = 'ITALICS';
+ break;
+ case 'U':
+ $n = 'UNDERLINED'; # Hey, virtual wiki tag! ;-)
+ break;
+ case 'S':
+ $n = 'STRIKE';
+ break;
+ case 'P':
+ $n = 'PARAGRAPH';
+ break;
+ case 'TABLE':
+ $n = 'TABLE';
+ break;
+ case 'TR':
+ $n = 'TABLEROW';
+ break;
+ case 'TD':
+ $n = 'TABLECELL';
+ break;
+ case 'TH':
+ $n = 'TABLEHEAD';
+ break;
+ case 'CAPTION':
+ $n = 'CAPTION';
+ break;
+ case 'NOWIKI':
+ $n = 'NOWIKI';
+ break;
+ }
+ if ($n != $old_n) {
+ unset ($this->attrs['NAME']); # Cleanup
+ } elseif ($parser->nowiki > 0) {
+ # No 'real' wiki tags allowed in nowiki section
+ $n = '';
+ }
+ } // $n = 'EXTENSION'
+
+ switch($n) {
+ case 'ARTICLE':
+ $ret .= $this->sub_makeXHTML($parser);
+ break;
+ case 'HEADING':
+ $ret .= $this->sub_makeXHTML($parser, 'h'.$this->attrs['LEVEL']);
+ break;
+ case 'PARAGRAPH':
+ $ret .= $this->sub_makeXHTML($parser, 'p');
+ break;
+ case 'BOLD':
+ $ret .= $this->sub_makeXHTML($parser, 'strong');
+ break;
+ case 'ITALICS':
+ $ret .= $this->sub_makeXHTML($parser, 'em');
+ break;
+
+ # These don't exist as wiki markup
+ case 'UNDERLINED':
+ $ret .= $this->sub_makeXHTML($parser, 'u');
+ break;
+ case 'STRIKE':
+ $ret .= $this->sub_makeXHTML($parser, 'strike');
+ break;
+
+ # HTML comment
+ case 'COMMENT':
+ # Comments are parsed out
+ $ret .= '';
+ break;
+
+
+ # Links
+ case 'LINK':
+ $ret .= $this->makeInternalLink($parser);
+ break;
+ case 'LINKTARGET':
+ case 'LINKOPTION':
+ $ret .= $this->sub_makeXHTML($parser);
+ break;
+
+ case 'TEMPLATE':
+ $parts = $this->sub_makeXHTML($parser);
+ $parts = explode('|', $parts);
+ $title = array_shift($parts);
+ $ret .= $this->getTemplateXHTML($title, $parts, & $parser);
+ break;
+
+ case 'TEMPLATEVAR':
+ $x = $this->sub_makeXHTML($parser);
+ if (isset ($parser->mCurrentTemplateOptions["{$x}"]))
+ $ret .= $parser->mCurrentTemplateOptions["{$x}"];
+ break;
+
+ # Internal use, not generated by wiki2xml parser
+ case 'IGNORE':
+ $ret .= $this->sub_makeXHTML($parser);
+
+ case 'NOWIKI':
+ $parser->nowiki++;
+ $ret .= $this->sub_makeXHTML($parser, '');
+ $parser->nowiki--;
+
+
+ # Unknown HTML extension
+ case 'EXTENSION': # This is currently a dummy!!!
+ $ext = $this->attrs['NAME'];
+
+ $ret .= '&lt;'.$ext.'&gt;';
+ $ret .= $this->sub_makeXHTML($parser);
+ $ret .= '&lt;/'.$ext.'&gt; ';
+ break;
+
+
+ # Table stuff
+
+ case 'TABLE':
+ $ret .= $this->sub_makeXHTML($parser, 'table');
+ break;
+ case 'TABLEROW':
+ $ret .= $this->sub_makeXHTML($parser, 'tr');
+ break;
+ case 'TABLECELL':
+ $ret .= $this->sub_makeXHTML($parser, 'td');
+ break;
+ case 'TABLEHEAD':
+ $ret .= $this->sub_makeXHTML($parser, 'th');
+ break;
+ case 'CAPTION':
+ $ret .= $this->sub_makeXHTML($parser, 'caption');
+ break;
+ case 'ATTRS': # SPECIAL CASE : returning attributes
+ return $this->getTheseAttrs();
+
+
+ # Lists stuff
+ case 'LISTITEM':
+ if ($parser->mListType == 'dl')
+ $ret .= $this->sub_makeXHTML($parser, 'dd');
+ else
+ $ret .= $this->sub_makeXHTML($parser, 'li');
+ break;
+ case 'LIST':
+ $type = 'ol'; # Default
+ if ($this->attrs['TYPE'] == 'bullet')
+ $type = 'ul';
+ else
+ if ($this->attrs['TYPE'] == 'indent')
+ $type = 'dl';
+ $oldtype = $parser->mListType;
+ $parser->mListType = $type;
+ $ret .= $this->sub_makeXHTML($parser, $type);
+ $parser->mListType = $oldtype;
+ break;
+
+ # Something else entirely
+ default:
+ $ret .= '&lt;'.$n.'&gt;';
+ $ret .= $this->sub_makeXHTML($parser);
+ $ret .= '&lt;/'.$n.'&gt; ';
+ } // switch($n)
+
+ $ret = "\n{$ret}\n";
+ $ret = str_replace("\n\n", "\n", $ret);
+ return $ret;
+ }
+
+ /**
+ * A function for additional debugging output
+ */
+ function myPrint() {
+ $ret = "<ul>\n";
+ $ret .= "<li> <b> Name: </b> $this->name </li>\n";
+ // print attributes
+ $ret .= '<li> <b> Attributes: </b>';
+ foreach ($this->attrs as $name => $value) {
+ $ret .= "$name => $value; ";
+ }
+ $ret .= " </li>\n";
+ // print children
+ foreach ($this->children as $child) {
+ if (is_string($child)) {
+ $ret .= "<li> $child </li>\n";
+ } else {
+ $ret .= $child->myPrint();
+ }
+ }
+ $ret .= "</ul>\n";
+ return $ret;
+ }
+}
+
+$ancStack = array (); // the stack with ancestral elements
+
+// START Three global functions needed for parsing, sorry guys
+/** @todo document */
+function wgXMLstartElement($parser, $name, $attrs) {
+ global $ancStack;
+
+ $newElem = new element;
+ $newElem->name = $name;
+ $newElem->attrs = $attrs;
+
+ array_push($ancStack, $newElem);
+}
+
+/** @todo document */
+function wgXMLendElement($parser, $name) {
+ global $ancStack, $rootElem;
+ // pop element off stack
+ $elem = array_pop($ancStack);
+ if (count($ancStack) == 0)
+ $rootElem = $elem;
+ else
+ // add it to its parent
+ array_push($ancStack[count($ancStack) - 1]->children, $elem);
+}
+
+/** @todo document */
+function wgXMLcharacterData($parser, $data) {
+ global $ancStack;
+ $data = trim($data); // Don't add blank lines, they're no use...
+ // add to parent if parent exists
+ if ($ancStack && $data != "") {
+ array_push($ancStack[count($ancStack) - 1]->children, $data);
+ }
+}
+// END Three global functions needed for parsing, sorry guys
+
+/**
+ * Here's the class that generates a nice tree
+ * @package MediaWiki
+ * @subpackage Experimental
+ */
+class xml2php {
+
+ /** @todo document */
+ function & scanFile($filename) {
+ global $ancStack, $rootElem;
+ $ancStack = array ();
+
+ $xml_parser = xml_parser_create();
+ xml_set_element_handler($xml_parser, 'wgXMLstartElement', 'wgXMLendElement');
+ xml_set_character_data_handler($xml_parser, 'wgXMLcharacterData');
+ if (!($fp = fopen($filename, 'r'))) {
+ die('could not open XML input');
+ }
+ while ($data = fread($fp, 4096)) {
+ if (!xml_parse($xml_parser, $data, feof($fp))) {
+ die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser)));
+ }
+ }
+ xml_parser_free($xml_parser);
+
+ // return the remaining root element we copied in the beginning
+ return $rootElem;
+ }
+
+ /** @todo document */
+ function scanString($input) {
+ global $ancStack, $rootElem;
+ $ancStack = array ();
+
+ $xml_parser = xml_parser_create();
+ xml_set_element_handler($xml_parser, 'wgXMLstartElement', 'wgXMLendElement');
+ xml_set_character_data_handler($xml_parser, 'wgXMLcharacterData');
+
+ if (!xml_parse($xml_parser, $input, true)) {
+ die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser)));
+ }
+ xml_parser_free($xml_parser);
+
+ // return the remaining root element we copied in the beginning
+ return $rootElem;
+ }
+
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ * @subpackage Experimental
+ */
+class ParserXML extends Parser {
+ /**#@+
+ * @private
+ */
+ # Persistent:
+ var $mTagHooks, $mListType;
+
+ # Cleared with clearState():
+ var $mOutput, $mAutonumber, $mDTopen, $mStripState = array ();
+ var $mVariables, $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+
+ # Temporary:
+ var $mOptions, $mTitle, $mOutputType, $mTemplates, // cache of already loaded templates, avoids
+ // multiple SQL queries for the same string
+ $mTemplatePath; // stores an unsorted hash of all the templates already loaded
+ // in this path. Used for loop detection.
+
+ var $nowikicount, $mCurrentTemplateOptions;
+
+ /**#@-*/
+
+ /**
+ * Constructor
+ *
+ * @public
+ */
+ function ParserXML() {
+ $this->mTemplates = array ();
+ $this->mTemplatePath = array ();
+ $this->mTagHooks = array ();
+ $this->clearState();
+ }
+
+ /**
+ * Clear Parser state
+ *
+ * @private
+ */
+ function clearState() {
+ $this->mOutput = new ParserOutput;
+ $this->mAutonumber = 0;
+ $this->mLastSection = "";
+ $this->mDTopen = false;
+ $this->mVariables = false;
+ $this->mIncludeCount = array ();
+ $this->mStripState = array ();
+ $this->mArgStack = array ();
+ $this->mInPre = false;
+ }
+
+ /**
+ * Turns the wikitext into XML by calling the external parser
+ *
+ */
+ function html2xml(& $text) {
+ global $wgWiki2xml;
+
+ # generating html2xml command path
+ $a = $wgWiki2xml;
+ $a = explode('/', $a);
+ array_pop($a);
+ $a[] = 'html2xml';
+ $html2xml = implode('/', $a);
+ $a = array ();
+
+ $tmpfname = tempnam( wfTempDir(), 'FOO' );
+ $handle = fopen($tmpfname, 'w');
+ fwrite($handle, utf8_encode($text));
+ fclose($handle);
+ exec($html2xml.' < '.$tmpfname, $a);
+ $text = utf8_decode(implode("\n", $a));
+ unlink($tmpfname);
+ }
+
+ /** @todo document */
+ function runXMLparser(& $text) {
+ global $wgWiki2xml;
+
+ $this->html2xml($text);
+
+ $tmpfname = tempnam( wfTempDir(), 'FOO');
+ $handle = fopen($tmpfname, 'w');
+ fwrite($handle, $text);
+ fclose($handle);
+ exec($wgWiki2xml.' < '.$tmpfname, $a);
+ $text = utf8_decode(implode("\n", $a));
+ unlink($tmpfname);
+ }
+
+ /** @todo document */
+ function plain_parse(& $text, $inline = false, $templateOptions = array ()) {
+ $this->runXMLparser($text);
+ $nowikicount = 0;
+ $w = new xml2php;
+ $result = $w->scanString($text);
+
+ $oldTemplateOptions = $this->mCurrentTemplateOptions;
+ $this->mCurrentTemplateOptions = $templateOptions;
+
+ if ($inline) { # Inline rendering off for templates
+ if (count($result->children) == 1)
+ $result->children[0]->name = 'IGNORE';
+ }
+
+ if (1)
+ $text = $result->makeXHTML($this); # No debugging info
+ else
+ $text = $result->makeXHTML($this).'<hr>'.$text.'<hr>'.$result->myPrint();
+ $this->mCurrentTemplateOptions = $oldTemplateOptions;
+ }
+
+ /** @todo document */
+ function parse($text, & $title, $options, $linestart = true, $clearState = true) {
+ $this->plain_parse($text);
+ $this->mOutput->setText($text);
+ return $this->mOutput;
+ }
+
+}
+?>
diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php
new file mode 100644
index 00000000..ed058c65
--- /dev/null
+++ b/includes/ProfilerSimple.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Simple profiler base class
+ * @package MediaWiki
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+require_once(dirname(__FILE__).'/Profiling.php');
+
+class ProfilerSimple extends Profiler {
+ function ProfilerSimple() {
+ global $wgRequestTime,$wgRUstart;
+ if (!empty($wgRequestTime) && !empty($wgRUstart)) {
+ $this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart));
+
+ $elapsedcpu = $this->getCpuTime() - $this->getCpuTime($wgRUstart);
+ $elapsedreal = microtime(true) - $wgRequestTime;
+
+ $entry =& $this->mCollated["-setup"];
+ if (!is_array($entry)) {
+ $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0);
+ $this->mCollated[$functionname] =& $entry;
+
+ }
+ $entry['cpu'] += $elapsedcpu;
+ $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu;
+ $entry['real'] += $elapsedreal;
+ $entry['real_sq'] += $elapsedreal*$elapsedreal;
+ $entry['count']++;
+ }
+ }
+
+ function profileIn($functionname) {
+ global $wgDebugFunctionEntry;
+ if ($wgDebugFunctionEntry) {
+ $this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n");
+ }
+ $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime());
+ }
+
+ function profileOut($functionname) {
+ $memory = memory_get_usage();
+
+ global $wgDebugFunctionEntry;
+
+ if ($wgDebugFunctionEntry) {
+ $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n");
+ }
+
+ list($ofname,$ocount,$ortime,$octime) = array_pop($this->mWorkStack);
+
+ if (!$ofname) {
+ $this->debug("Profiling error: $functionname\n");
+ } else {
+ if ($functionname == 'close') {
+ $message = "Profile section ended by close(): {$ofname}";
+ $functionname = $ofname;
+ $this->debug( "$message\n" );
+ }
+ elseif ($ofname != $functionname) {
+ $message = "Profiling error: in({$ofname}), out($functionname)";
+ $this->debug( "$message\n" );
+ }
+ $entry =& $this->mCollated[$functionname];
+ $elapsedcpu = $this->getCpuTime() - $octime;
+ $elapsedreal = microtime(true) - $ortime;
+ if (!is_array($entry)) {
+ $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0);
+ $this->mCollated[$functionname] =& $entry;
+
+ }
+ $entry['cpu'] += $elapsedcpu;
+ $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu;
+ $entry['real'] += $elapsedreal;
+ $entry['real_sq'] += $elapsedreal*$elapsedreal;
+ $entry['count']++;
+
+ }
+ }
+
+ function getFunctionReport() {
+ /* Implement in output subclasses */
+ }
+
+ function getCpuTime($ru=null) {
+ if ($ru==null)
+ $ru=getrusage();
+ return ($ru['ru_utime.tv_sec']+$ru['ru_stime.tv_sec']+($ru['ru_utime.tv_usec']+$ru['ru_stime.tv_usec'])*1e-6);
+ }
+
+ /* If argument is passed, it assumes that it is dual-format time string, returns proper float time value */
+ function getTime($time=null) {
+ if ($time==null)
+ return microtime(true);
+ list($a,$b)=explode(" ",$time);
+ return (float)($a+$b);
+ }
+
+ function debug( $s ) {
+ if (function_exists( 'wfDebug' ) ) {
+ wfDebug( $s );
+ }
+ }
+}
+?>
diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php
new file mode 100644
index 00000000..c395228b
--- /dev/null
+++ b/includes/ProfilerSimpleUDP.php
@@ -0,0 +1,34 @@
+<?php
+/* ProfilerSimpleUDP class, that sends out messages for 'udpprofile' daemon
+ (the one from wikipedia/udpprofile CVS )
+*/
+
+require_once(dirname(__FILE__).'/Profiling.php');
+require_once(dirname(__FILE__).'/ProfilerSimple.php');
+
+class ProfilerSimpleUDP extends ProfilerSimple {
+ function getFunctionReport() {
+ global $wgUDPProfilerHost;
+ global $wgUDPProfilerPort;
+ global $wgDBname;
+
+ $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+ $plength=0;
+ $packet="";
+ foreach ($this->mCollated as $entry=>$pfdata) {
+ $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", $wgDBname,"-",$pfdata['count'],
+ $pfdata['cpu'],$pfdata['cpu_sq'],$pfdata['real'],$pfdata['real_sq'],$entry);
+ $length=strlen($pfline);
+ /* printf("<!-- $pfline -->"); */
+ if ($length+$plength>1400) {
+ socket_sendto($sock,$packet,$plength,0,$wgUDPProfilerHost,$wgUDPProfilerPort);
+ $packet="";
+ $plength=0;
+ }
+ $packet.=$pfline;
+ $plength+=$length;
+ }
+ socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort);
+ }
+}
+?>
diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php
new file mode 100644
index 00000000..3bcdaab2
--- /dev/null
+++ b/includes/ProfilerStub.php
@@ -0,0 +1,26 @@
+<?php
+
+# Stub profiling functions
+
+$haveProctitle=function_exists("setproctitle");
+function wfProfileIn( $fn = '' ) {
+ global $hackwhere, $wgDBname, $haveProctitle;
+ if ($haveProctitle) {
+ $hackwhere[] = $fn;
+ setproctitle($fn . " [$wgDBname]");
+ }
+}
+function wfProfileOut( $fn = '' ) {
+ global $hackwhere, $wgDBname, $haveProctitle;
+ if (!$haveProctitle)
+ return;
+ if (count($hackwhere))
+ array_pop($hackwhere);
+ if (count($hackwhere))
+ setproctitle($hackwhere[count($hackwhere)-1] . " [$wgDBname]");
+}
+function wfGetProfilingOutput( $s, $e ) {}
+function wfProfileClose() {}
+function wfLogProfilingData() {}
+
+?>
diff --git a/includes/Profiling.php b/includes/Profiling.php
new file mode 100644
index 00000000..edecc4f3
--- /dev/null
+++ b/includes/Profiling.php
@@ -0,0 +1,353 @@
+<?php
+/**
+ * This file is only included if profiling is enabled
+ * @package MediaWiki
+ */
+
+/**
+ * @param $functioname name of the function we will profile
+ */
+function wfProfileIn($functionname) {
+ global $wgProfiler;
+ $wgProfiler->profileIn($functionname);
+}
+
+/**
+ * @param $functioname name of the function we have profiled
+ */
+function wfProfileOut($functionname = 'missing') {
+ global $wgProfiler;
+ $wgProfiler->profileOut($functionname);
+}
+
+function wfGetProfilingOutput($start, $elapsed) {
+ global $wgProfiler;
+ return $wgProfiler->getOutput($start, $elapsed);
+}
+
+function wfProfileClose() {
+ global $wgProfiler;
+ $wgProfiler->close();
+}
+
+if (!function_exists('memory_get_usage')) {
+ # Old PHP or --enable-memory-limit not compiled in
+ function memory_get_usage() {
+ return 0;
+ }
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class Profiler {
+ var $mStack = array (), $mWorkStack = array (), $mCollated = array ();
+ var $mCalls = array (), $mTotals = array ();
+
+ function Profiler()
+ {
+ // Push an entry for the pre-profile setup time onto the stack
+ global $wgRequestTime;
+ if ( !empty( $wgRequestTime ) ) {
+ $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, 0 );
+ $this->mStack[] = array( '-setup', 1, $wgRequestTime, 0, microtime(true), 0 );
+ } else {
+ $this->profileIn( '-total' );
+ }
+
+ }
+
+ function profileIn($functionname) {
+ global $wgDebugFunctionEntry;
+ if ($wgDebugFunctionEntry && function_exists('wfDebug')) {
+ wfDebug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n");
+ }
+ $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage());
+ }
+
+ function profileOut($functionname) {
+ $memory = memory_get_usage();
+ $time = $this->getTime();
+
+ global $wgDebugFunctionEntry;
+
+ if ($wgDebugFunctionEntry && function_exists('wfDebug')) {
+ wfDebug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n");
+ }
+
+ $bit = array_pop($this->mWorkStack);
+
+ if (!$bit) {
+ wfDebug("Profiling error, !\$bit: $functionname\n");
+ } else {
+ //if ($wgDebugProfiling) {
+ if ($functionname == 'close') {
+ $message = "Profile section ended by close(): {$bit[0]}";
+ wfDebug( "$message\n" );
+ $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
+ }
+ elseif ($bit[0] != $functionname) {
+ $message = "Profiling error: in({$bit[0]}), out($functionname)";
+ wfDebug( "$message\n" );
+ $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
+ }
+ //}
+ $bit[] = $time;
+ $bit[] = $memory;
+ $this->mStack[] = $bit;
+ }
+ }
+
+ function close() {
+ while (count($this->mWorkStack)) {
+ $this->profileOut('close');
+ }
+ }
+
+ function getOutput() {
+ global $wgDebugFunctionEntry;
+ $wgDebugFunctionEntry = false;
+
+ if (!count($this->mStack) && !count($this->mCollated)) {
+ return "No profiling output\n";
+ }
+ $this->close();
+
+ global $wgProfileCallTree;
+ if ($wgProfileCallTree) {
+ return $this->getCallTree();
+ } else {
+ return $this->getFunctionReport();
+ }
+ }
+
+ function getCallTree($start = 0) {
+ return implode('', array_map(array (& $this, 'getCallTreeLine'), $this->remapCallTree($this->mStack)));
+ }
+
+ function remapCallTree($stack) {
+ if (count($stack) < 2) {
+ return $stack;
+ }
+ $outputs = array ();
+ for ($max = count($stack) - 1; $max > 0;) {
+ /* Find all items under this entry */
+ $level = $stack[$max][1];
+ $working = array ();
+ for ($i = $max -1; $i >= 0; $i --) {
+ if ($stack[$i][1] > $level) {
+ $working[] = $stack[$i];
+ } else {
+ break;
+ }
+ }
+ $working = $this->remapCallTree(array_reverse($working));
+ $output = array ();
+ foreach ($working as $item) {
+ array_push($output, $item);
+ }
+ array_unshift($output, $stack[$max]);
+ $max = $i;
+
+ array_unshift($outputs, $output);
+ }
+ $final = array ();
+ foreach ($outputs as $output) {
+ foreach ($output as $item) {
+ $final[] = $item;
+ }
+ }
+ return $final;
+ }
+
+ function getCallTreeLine($entry) {
+ list ($fname, $level, $start, $x, $end) = $entry;
+ $delta = $end - $start;
+ $space = str_repeat(' ', $level);
+
+ # The ugly double sprintf is to work around a PHP bug,
+ # which has been fixed in recent releases.
+ return sprintf( "%10s %s %s\n",
+ trim( sprintf( "%7.3f", $delta * 1000.0 ) ),
+ $space, $fname );
+ }
+
+ function getTime() {
+ return microtime(true);
+ #return $this->getUserTime();
+ }
+
+ function getUserTime() {
+ $ru = getrusage();
+ return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6;
+ }
+
+ function getFunctionReport() {
+ $width = 140;
+ $nameWidth = $width - 65;
+ $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n";
+ $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
+ $prof = "\nProfiling data\n";
+ $prof .= sprintf($titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem');
+ $this->mCollated = array ();
+ $this->mCalls = array ();
+ $this->mMemory = array ();
+
+ # Estimate profiling overhead
+ $profileCount = count($this->mStack);
+ wfProfileIn('-overhead-total');
+ for ($i = 0; $i < $profileCount; $i ++) {
+ wfProfileIn('-overhead-internal');
+ wfProfileOut('-overhead-internal');
+ }
+ wfProfileOut('-overhead-total');
+
+ # First, subtract the overhead!
+ foreach ($this->mStack as $entry) {
+ $fname = $entry[0];
+ $thislevel = $entry[1];
+ $start = $entry[2];
+ $end = $entry[4];
+ $elapsed = $end - $start;
+ $memory = $entry[5] - $entry[3];
+
+ if ($fname == '-overhead-total') {
+ $overheadTotal[] = $elapsed;
+ $overheadMemory[] = $memory;
+ }
+ elseif ($fname == '-overhead-internal') {
+ $overheadInternal[] = $elapsed;
+ }
+ }
+ $overheadTotal = array_sum($overheadTotal) / count($overheadInternal);
+ $overheadMemory = array_sum($overheadMemory) / count($overheadInternal);
+ $overheadInternal = array_sum($overheadInternal) / count($overheadInternal);
+
+ # Collate
+ foreach ($this->mStack as $index => $entry) {
+ $fname = $entry[0];
+ $thislevel = $entry[1];
+ $start = $entry[2];
+ $end = $entry[4];
+ $elapsed = $end - $start;
+
+ $memory = $entry[5] - $entry[3];
+ $subcalls = $this->calltreeCount($this->mStack, $index);
+
+ if (!preg_match('/^-overhead/', $fname)) {
+ # Adjust for profiling overhead (except special values with elapsed=0
+ if ( $elapsed ) {
+ $elapsed -= $overheadInternal;
+ $elapsed -= ($subcalls * $overheadTotal);
+ $memory -= ($subcalls * $overheadMemory);
+ }
+ }
+
+ if (!array_key_exists($fname, $this->mCollated)) {
+ $this->mCollated[$fname] = 0;
+ $this->mCalls[$fname] = 0;
+ $this->mMemory[$fname] = 0;
+ $this->mMin[$fname] = 1 << 24;
+ $this->mMax[$fname] = 0;
+ $this->mOverhead[$fname] = 0;
+ }
+
+ $this->mCollated[$fname] += $elapsed;
+ $this->mCalls[$fname]++;
+ $this->mMemory[$fname] += $memory;
+ $this->mMin[$fname] = min($this->mMin[$fname], $elapsed);
+ $this->mMax[$fname] = max($this->mMax[$fname], $elapsed);
+ $this->mOverhead[$fname] += $subcalls;
+ }
+
+ $total = @ $this->mCollated['-total'];
+ $this->mCalls['-overhead-total'] = $profileCount;
+
+ # Output
+ asort($this->mCollated, SORT_NUMERIC);
+ foreach ($this->mCollated as $fname => $elapsed) {
+ $calls = $this->mCalls[$fname];
+ $percent = $total ? 100. * $elapsed / $total : 0;
+ $memory = $this->mMemory[$fname];
+ $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]);
+
+ global $wgProfileToDatabase;
+ if ($wgProfileToDatabase) {
+ Profiler :: logToDB($fname, (float) ($elapsed * 1000), $calls);
+ }
+ }
+ $prof .= "\nTotal: $total\n\n";
+
+ return $prof;
+ }
+
+ /**
+ * Counts the number of profiled function calls sitting under
+ * the given point in the call graph. Not the most efficient algo.
+ *
+ * @param $stack Array:
+ * @param $start Integer:
+ * @return Integer
+ * @private
+ */
+ function calltreeCount(& $stack, $start) {
+ $level = $stack[$start][1];
+ $count = 0;
+ for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) {
+ $count ++;
+ }
+ return $count;
+ }
+
+ /**
+ * @static
+ */
+ function logToDB($name, $timeSum, $eventCount) {
+ # Warning: $wguname is a live patch, it should be moved to Setup.php
+ global $wguname, $wgProfilePerHost;
+
+ $fname = 'Profiler::logToDB';
+ $dbw = & wfGetDB(DB_MASTER);
+ if (!is_object($dbw))
+ return false;
+ $errorState = $dbw->ignoreErrors( true );
+ $profiling = $dbw->tableName('profiling');
+
+ $name = substr($name, 0, 255);
+ $encname = $dbw->strencode($name);
+
+ if ($wgProfilePerHost) {
+ $pfhost = $wguname['nodename'];
+ } else {
+ $pfhost = '';
+ }
+
+ $sql = "UPDATE $profiling "."SET pf_count=pf_count+{$eventCount}, "."pf_time=pf_time + {$timeSum} ".
+ "WHERE pf_name='{$encname}' AND pf_server='{$pfhost}'";
+ $dbw->query($sql);
+
+ $rc = $dbw->affectedRows();
+ if ($rc == 0) {
+ $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount,
+ 'pf_time' => $timeSum, 'pf_server' => $pfhost ), $fname, array ('IGNORE'));
+ }
+ // When we upgrade to mysql 4.1, the insert+update
+ // can be merged into just a insert with this construct added:
+ // "ON DUPLICATE KEY UPDATE ".
+ // "pf_count=pf_count + VALUES(pf_count), ".
+ // "pf_time=pf_time + VALUES(pf_time)";
+ $dbw->ignoreErrors( $errorState );
+ }
+
+ /**
+ * Get the function name of the current profiling section
+ */
+ function getCurrentSection() {
+ $elt = end($this->mWorkStack);
+ return $elt[0];
+ }
+
+}
+
+?>
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
new file mode 100644
index 00000000..2a40a376
--- /dev/null
+++ b/includes/ProtectionForm.php
@@ -0,0 +1,244 @@
+<?php
+/**
+ * Copyright (C) 2005 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
+ */
+
+class ProtectionForm {
+ var $mRestrictions = array();
+ var $mReason = '';
+
+ function ProtectionForm( &$article ) {
+ global $wgRequest, $wgUser;
+ global $wgRestrictionTypes, $wgRestrictionLevels;
+ $this->mArticle =& $article;
+ $this->mTitle =& $article->mTitle;
+
+ if( $this->mTitle ) {
+ foreach( $wgRestrictionTypes as $action ) {
+ // Fixme: this form currently requires individual selections,
+ // but the db allows multiples separated by commas.
+ $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
+ }
+ }
+
+ // The form will be available in read-only to show levels.
+ $this->disabled = !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $wgUser->isBlocked();
+ $this->disabledAttrib = $this->disabled
+ ? array( 'disabled' => 'disabled' )
+ : array();
+
+ if( $wgRequest->wasPosted() ) {
+ $this->mReason = $wgRequest->getText( 'mwProtect-reason' );
+ foreach( $wgRestrictionTypes as $action ) {
+ $val = $wgRequest->getVal( "mwProtect-level-$action" );
+ if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
+ $this->mRestrictions[$action] = $val;
+ }
+ }
+ }
+ }
+
+ function show() {
+ global $wgOut;
+
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+ if( is_null( $this->mTitle ) ||
+ !$this->mTitle->exists() ||
+ $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ $wgOut->showFatalError( wfMsg( 'badarticleerror' ) );
+ return;
+ }
+
+ if( $this->save() ) {
+ $wgOut->redirect( $this->mTitle->getFullUrl() );
+ return;
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) );
+ $wgOut->setSubtitle( wfMsg( 'protectsub', $this->mTitle->getPrefixedText() ) );
+
+ $wgOut->addWikiText(
+ wfMsg( $this->disabled ? "protect-viewtext" : "protect-text",
+ $this->mTitle->getPrefixedText() ) );
+
+ $wgOut->addHTML( $this->buildForm() );
+
+ $this->showLogExtract( $wgOut );
+ }
+
+ function save() {
+ global $wgRequest, $wgUser, $wgOut;
+ if( !$wgRequest->wasPosted() ) {
+ return false;
+ }
+
+ if( $this->disabled ) {
+ return false;
+ }
+
+ $token = $wgRequest->getVal( 'wpEditToken' );
+ if( !$wgUser->matchEditToken( $token ) ) {
+ throw new FatalError( wfMsg( 'sessionfailure' ) );
+ }
+
+ $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason );
+ if( !$ok ) {
+ throw new FatalError( "Unknown error at restriction save time." );
+ }
+ return $ok;
+ }
+
+ function buildForm() {
+ global $wgUser;
+
+ $out = '';
+ if( !$this->disabled ) {
+ $out .= $this->buildScript();
+ // The submission needs to reenable the move permission selector
+ // if it's in locked mode, or some browsers won't submit the data.
+ $out .= wfOpenElement( 'form', array(
+ 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
+ 'method' => 'post',
+ 'onsubmit' => 'protectEnable(true)' ) );
+
+ $out .= wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $wgUser->editToken() ) );
+ }
+
+ $out .= "<table id='mwProtectSet'>";
+ $out .= "<tbody>";
+ $out .= "<tr>\n";
+ foreach( $this->mRestrictions as $action => $required ) {
+ /* Not all languages have V_x <-> N_x relation */
+ $out .= "<th>" . wfMsgHtml( 'restriction-' . $action ) . "</th>\n";
+ }
+ $out .= "</tr>\n";
+ $out .= "<tr>\n";
+ foreach( $this->mRestrictions as $action => $selected ) {
+ $out .= "<td>\n";
+ $out .= $this->buildSelector( $action, $selected );
+ $out .= "</td>\n";
+ }
+ $out .= "</tr>\n";
+
+ // JavaScript will add another row with a value-chaining checkbox
+
+ $out .= "</tbody>\n";
+ $out .= "</table>\n";
+
+ if( !$this->disabled ) {
+ $out .= "<table>\n";
+ $out .= "<tbody>\n";
+ $out .= "<tr><td>" . $this->buildReasonInput() . "</td></tr>\n";
+ $out .= "<tr><td></td><td>" . $this->buildSubmit() . "</td></tr>\n";
+ $out .= "</tbody>\n";
+ $out .= "</table>\n";
+ $out .= "</form>\n";
+ $out .= $this->buildCleanupScript();
+ }
+
+ return $out;
+ }
+
+ function buildSelector( $action, $selected ) {
+ global $wgRestrictionLevels;
+ $id = 'mwProtect-level-' . $action;
+ $attribs = array(
+ 'id' => $id,
+ 'name' => $id,
+ 'size' => count( $wgRestrictionLevels ),
+ 'onchange' => 'protectLevelsUpdate(this)',
+ ) + $this->disabledAttrib;
+
+ $out = wfOpenElement( 'select', $attribs );
+ foreach( $wgRestrictionLevels as $key ) {
+ $out .= $this->buildOption( $key, $selected );
+ }
+ $out .= "</select>\n";
+ return $out;
+ }
+
+ function buildOption( $key, $selected ) {
+ $text = ( $key == '' )
+ ? wfMsg( 'protect-default' )
+ : wfMsg( "protect-level-$key" );
+ $selectedAttrib = ($selected == $key)
+ ? array( 'selected' => 'selected' )
+ : array();
+ return wfElement( 'option',
+ array( 'value' => $key ) + $selectedAttrib,
+ $text );
+ }
+
+ function buildReasonInput() {
+ $id = 'mwProtect-reason';
+ return wfElement( 'label', array(
+ 'id' => "$id-label",
+ 'for' => $id ),
+ wfMsg( 'protectcomment' ) ) .
+ '</td><td>' .
+ wfElement( 'input', array(
+ 'size' => 60,
+ 'name' => $id,
+ 'id' => $id ) );
+ }
+
+ function buildSubmit() {
+ return wfElement( 'input', array(
+ 'type' => 'submit',
+ 'value' => wfMsg( 'confirm' ) ) );
+ }
+
+ function buildScript() {
+ global $wgStylePath;
+ return '<script type="text/javascript" src="' .
+ htmlspecialchars( $wgStylePath . "/common/protect.js" ) .
+ '"></script>';
+ }
+
+ function buildCleanupScript() {
+ return '<script type="text/javascript">protectInitialize("mwProtectSet","' .
+ wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")</script>';
+ }
+
+ /**
+ * @param OutputPage $out
+ * @access private
+ */
+ function showLogExtract( &$out ) {
+ # Show relevant lines from the deletion log:
+ $out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'protect' ) ) . "</h2>\n" );
+ require_once( 'SpecialLog.php' );
+ $logViewer = new LogViewer(
+ new LogReader(
+ new FauxRequest(
+ array( 'page' => $this->mTitle->getPrefixedText(),
+ 'type' => 'protect' ) ) ) );
+ $logViewer->showList( $out );
+ }
+}
+
+
+?>
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
new file mode 100644
index 00000000..bed79c10
--- /dev/null
+++ b/includes/ProxyTools.php
@@ -0,0 +1,233 @@
+<?php
+/**
+ * Functions for dealing with proxies
+ * @package MediaWiki
+ */
+
+function wfGetForwardedFor() {
+ if( function_exists( 'apache_request_headers' ) ) {
+ // More reliable than $_SERVER due to case and -/_ folding
+ $set = apache_request_headers();
+ $index = 'X-Forwarded-For';
+ } else {
+ // Subject to spoofing with headers like X_Forwarded_For
+ $set = $_SERVER;
+ $index = 'HTTP_X_FORWARDED_FOR';
+ }
+ if( isset( $set[$index] ) ) {
+ return $set[$index];
+ } else {
+ return null;
+ }
+}
+
+/** Work out the IP address based on various globals */
+function wfGetIP() {
+ global $wgSquidServers, $wgSquidServersNoPurge, $wgIP;
+
+ # Return cached result
+ if ( !empty( $wgIP ) ) {
+ return $wgIP;
+ }
+
+ /* collect the originating ips */
+ # Client connecting to this webserver
+ if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
+ $ipchain = array( $_SERVER['REMOTE_ADDR'] );
+ } else {
+ # Running on CLI?
+ $ipchain = array( '127.0.0.1' );
+ }
+ $ip = $ipchain[0];
+
+ # Get list of trusted proxies
+ # Flipped for quicker access
+ $trustedProxies = array_flip( array_merge( $wgSquidServers, $wgSquidServersNoPurge ) );
+ if ( count( $trustedProxies ) ) {
+ # Append XFF on to $ipchain
+ $forwardedFor = wfGetForwardedFor();
+ if ( isset( $forwardedFor ) ) {
+ $xff = array_map( 'trim', explode( ',', $forwardedFor ) );
+ $xff = array_reverse( $xff );
+ $ipchain = array_merge( $ipchain, $xff );
+ }
+ # Step through XFF list and find the last address in the list which is a trusted server
+ # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private)
+ foreach ( $ipchain as $i => $curIP ) {
+ if ( array_key_exists( $curIP, $trustedProxies ) ) {
+ if ( isset( $ipchain[$i + 1] ) && wfIsIPPublic( $ipchain[$i + 1] ) ) {
+ $ip = $ipchain[$i + 1];
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ wfDebug( "IP: $ip\n" );
+ $wgIP = $ip;
+ return $ip;
+}
+
+/**
+ * Given an IP address in dotted-quad notation, returns an unsigned integer.
+ * Like ip2long() except that it actually works and has a consistent error return value.
+ */
+function wfIP2Unsigned( $ip ) {
+ $n = ip2long( $ip );
+ if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version
+ $n = false;
+ } elseif ( $n < 0 ) {
+ $n += pow( 2, 32 );
+ }
+ return $n;
+}
+
+/**
+ * Return a zero-padded hexadecimal representation of an IP address
+ */
+function wfIP2Hex( $ip ) {
+ $n = wfIP2Unsigned( $ip );
+ if ( $n !== false ) {
+ $n = sprintf( '%08X', $n );
+ }
+ return $n;
+}
+
+/**
+ * Determine if an IP address really is an IP address, and if it is public,
+ * i.e. not RFC 1918 or similar
+ */
+function wfIsIPPublic( $ip ) {
+ $n = wfIP2Unsigned( $ip );
+ if ( !$n ) {
+ return false;
+ }
+
+ // ip2long accepts incomplete addresses, as well as some addresses
+ // followed by garbage characters. Check that it's really valid.
+ if( $ip != long2ip( $n ) ) {
+ return false;
+ }
+
+ static $privateRanges = false;
+ if ( !$privateRanges ) {
+ $privateRanges = array(
+ array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private)
+ array( '172.16.0.0', '172.31.255.255' ), # "
+ array( '192.168.0.0', '192.168.255.255' ), # "
+ array( '0.0.0.0', '0.255.255.255' ), # this network
+ array( '127.0.0.0', '127.255.255.255' ), # loopback
+ );
+ }
+
+ foreach ( $privateRanges as $r ) {
+ $start = wfIP2Unsigned( $r[0] );
+ $end = wfIP2Unsigned( $r[1] );
+ if ( $n >= $start && $n <= $end ) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Forks processes to scan the originating IP for an open proxy server
+ * MemCached can be used to skip IPs that have already been scanned
+ */
+function wfProxyCheck() {
+ global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath;
+ global $wgUseMemCached, $wgMemc, $wgDBname, $wgProxyMemcExpiry;
+ global $wgProxyKey;
+
+ if ( !$wgBlockOpenProxies ) {
+ return;
+ }
+
+ $ip = wfGetIP();
+
+ # Get MemCached key
+ $skip = false;
+ if ( $wgUseMemCached ) {
+ $mcKey = "$wgDBname:proxy:ip:$ip";
+ $mcValue = $wgMemc->get( $mcKey );
+ if ( $mcValue ) {
+ $skip = true;
+ }
+ }
+
+ # Fork the processes
+ if ( !$skip ) {
+ $title = Title::makeTitle( NS_SPECIAL, 'Blockme' );
+ $iphash = md5( $ip . $wgProxyKey );
+ $url = $title->getFullURL( 'ip='.$iphash );
+
+ foreach ( $wgProxyPorts as $port ) {
+ $params = implode( ' ', array(
+ escapeshellarg( $wgProxyScriptPath ),
+ escapeshellarg( $ip ),
+ escapeshellarg( $port ),
+ escapeshellarg( $url )
+ ));
+ exec( "php $params &>/dev/null &" );
+ }
+ # Set MemCached key
+ if ( $wgUseMemCached ) {
+ $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry );
+ }
+ }
+}
+
+/**
+ * Convert a network specification in CIDR notation to an integer network and a number of bits
+ */
+function wfParseCIDR( $range ) {
+ $parts = explode( '/', $range, 2 );
+ if ( count( $parts ) != 2 ) {
+ return array( false, false );
+ }
+ $network = wfIP2Unsigned( $parts[0] );
+ if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
+ $bits = $parts[1];
+ } else {
+ $network = false;
+ $bits = false;
+ }
+ return array( $network, $bits );
+}
+
+/**
+ * Check if an IP address is in the local proxy list
+ */
+function wfIsLocallyBlockedProxy( $ip ) {
+ global $wgProxyList;
+ $fname = 'wfIsLocallyBlockedProxy';
+
+ if ( !$wgProxyList ) {
+ return false;
+ }
+ wfProfileIn( $fname );
+
+ if ( !is_array( $wgProxyList ) ) {
+ # Load from the specified file
+ $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
+ }
+
+ if ( !is_array( $wgProxyList ) ) {
+ $ret = false;
+ } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
+ $ret = true;
+ } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
+ # Old-style flipped proxy list
+ $ret = true;
+ } else {
+ $ret = false;
+ }
+ wfProfileOut( $fname );
+ return $ret;
+}
+
+
+
+
+?>
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
new file mode 100644
index 00000000..53e17616
--- /dev/null
+++ b/includes/QueryPage.php
@@ -0,0 +1,483 @@
+<?php
+/**
+ * Contain a class for special pages
+ * @package MediaWiki
+ */
+
+/**
+ * List of query page classes and their associated special pages, for periodic update purposes
+ */
+global $wgQueryPages; // not redundant
+$wgQueryPages = array(
+// QueryPage subclass Special page name Limit (false for none, none for the default)
+//----------------------------------------------------------------------------
+ array( 'AncientPagesPage', 'Ancientpages' ),
+ array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
+ array( 'CategoriesPage', 'Categories' ),
+ array( 'DeadendPagesPage', 'Deadendpages' ),
+ array( 'DisambiguationsPage', 'Disambiguations' ),
+ array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
+ array( 'ListUsersPage', 'Listusers' ),
+ array( 'ListredirectsPage', 'Listredirects' ),
+ array( 'LonelyPagesPage', 'Lonelypages' ),
+ array( 'LongPagesPage', 'Longpages' ),
+ array( 'MostcategoriesPage', 'Mostcategories' ),
+ array( 'MostimagesPage', 'Mostimages' ),
+ array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
+ array( 'MostlinkedPage', 'Mostlinked' ),
+ array( 'MostrevisionsPage', 'Mostrevisions' ),
+ array( 'NewPagesPage', 'Newpages' ),
+ array( 'ShortPagesPage', 'Shortpages' ),
+ array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
+ array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
+ array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
+ array( 'UnusedCategoriesPage', 'Unusedcategories' ),
+ array( 'UnusedimagesPage', 'Unusedimages' ),
+ array( 'WantedCategoriesPage', 'Wantedcategories' ),
+ array( 'WantedPagesPage', 'Wantedpages' ),
+ array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
+ array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
+);
+wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
+
+global $wgDisableCounters;
+if ( !$wgDisableCounters )
+ $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
+
+
+/**
+ * This is a class for doing query pages; since they're almost all the same,
+ * we factor out some of the functionality into a superclass, and let
+ * subclasses derive from it.
+ *
+ * @package MediaWiki
+ */
+class QueryPage {
+ /**
+ * Whether or not we want plain listoutput rather than an ordered list
+ *
+ * @var bool
+ */
+ var $listoutput = false;
+
+ /**
+ * The offset and limit in use, as passed to the query() function
+ *
+ * @var integer
+ */
+ var $offset = 0;
+ var $limit = 0;
+
+ /**
+ * A mutator for $this->listoutput;
+ *
+ * @param bool $bool
+ */
+ function setListoutput( $bool ) {
+ $this->listoutput = $bool;
+ }
+
+ /**
+ * Subclasses return their name here. Make sure the name is also
+ * specified in SpecialPage.php and in Language.php as a language message
+ * param.
+ */
+ function getName() {
+ return '';
+ }
+
+ /**
+ * Return title object representing this page
+ *
+ * @return Title
+ */
+ function getTitle() {
+ return Title::makeTitle( NS_SPECIAL, $this->getName() );
+ }
+
+ /**
+ * Subclasses return an SQL query here.
+ *
+ * Note that the query itself should return the following four columns:
+ * 'type' (your special page's name), 'namespace', 'title', and 'value'
+ * *in that order*. 'value' is used for sorting.
+ *
+ * These may be stored in the querycache table for expensive queries,
+ * and that cached data will be returned sometimes, so the presence of
+ * extra fields can't be relied upon. The cached 'value' column will be
+ * an integer; non-numeric values are useful only for sorting the initial
+ * query.
+ *
+ * Don't include an ORDER or LIMIT clause, this will be added.
+ */
+ function getSQL() {
+ return "SELECT 'sample' as type, 0 as namespace, 'Sample result' as title, 42 as value";
+ }
+
+ /**
+ * Override to sort by increasing values
+ */
+ function sortDescending() {
+ return true;
+ }
+
+ function getOrder() {
+ return ' ORDER BY value ' .
+ ($this->sortDescending() ? 'DESC' : '');
+ }
+
+ /**
+ * Is this query expensive (for some definition of expensive)? Then we
+ * don't let it run in miser mode. $wgDisableQueryPages causes all query
+ * pages to be declared expensive. Some query pages are always expensive.
+ */
+ function isExpensive( ) {
+ global $wgDisableQueryPages;
+ return $wgDisableQueryPages;
+ }
+
+ /**
+ * Whether or not the output of the page in question is retrived from
+ * the database cache.
+ *
+ * @return bool
+ */
+ function isCached() {
+ global $wgMiserMode;
+
+ return $this->isExpensive() && $wgMiserMode;
+ }
+
+ /**
+ * Sometime we dont want to build rss / atom feeds.
+ */
+ function isSyndicated() {
+ return true;
+ }
+
+ /**
+ * Formats the results of the query for display. The skin is the current
+ * skin; you can use it for making links. The result is a single row of
+ * result data. You should be able to grab SQL results off of it.
+ * If the function return "false", the line output will be skipped.
+ */
+ function formatResult( $skin, $result ) {
+ return '';
+ }
+
+ /**
+ * The content returned by this function will be output before any result
+ */
+ function getPageHeader( ) {
+ return '';
+ }
+
+ /**
+ * If using extra form wheely-dealies, return a set of parameters here
+ * as an associative array. They will be encoded and added to the paging
+ * links (prev/next/lengths).
+ * @return array
+ */
+ function linkParameters() {
+ return array();
+ }
+
+ /**
+ * Some special pages (for example SpecialListusers) might not return the
+ * current object formatted, but return the previous one instead.
+ * Setting this to return true, will call one more time wfFormatResult to
+ * be sure that the very last result is formatted and shown.
+ */
+ function tryLastResult( ) {
+ return false;
+ }
+
+ /**
+ * Clear the cache and save new results
+ */
+ function recache( $limit, $ignoreErrors = true ) {
+ $fname = get_class($this) . '::recache';
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbr =& wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
+ if ( !$dbw || !$dbr ) {
+ return false;
+ }
+
+ $querycache = $dbr->tableName( 'querycache' );
+
+ if ( $ignoreErrors ) {
+ $ignoreW = $dbw->ignoreErrors( true );
+ $ignoreR = $dbr->ignoreErrors( true );
+ }
+
+ # Clear out any old cached data
+ $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
+ # Do query
+ $sql = $this->getSQL() . $this->getOrder();
+ if ($limit !== false)
+ $sql = $dbr->limitResult($sql, $limit, 0);
+ $res = $dbr->query($sql, $fname);
+ $num = false;
+ if ( $res ) {
+ $num = $dbr->numRows( $res );
+ # Fetch results
+ $insertSql = "INSERT INTO $querycache (qc_type,qc_namespace,qc_title,qc_value) VALUES ";
+ $first = true;
+ while ( $res && $row = $dbr->fetchObject( $res ) ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $insertSql .= ',';
+ }
+ if ( isset( $row->value ) ) {
+ $value = $row->value;
+ } else {
+ $value = '';
+ }
+
+ $insertSql .= '(' .
+ $dbw->addQuotes( $row->type ) . ',' .
+ $dbw->addQuotes( $row->namespace ) . ',' .
+ $dbw->addQuotes( $row->title ) . ',' .
+ $dbw->addQuotes( $value ) . ')';
+ }
+
+ # Save results into the querycache table on the master
+ if ( !$first ) {
+ if ( !$dbw->query( $insertSql, $fname ) ) {
+ // Set result to false to indicate error
+ $dbr->freeResult( $res );
+ $res = false;
+ }
+ }
+ if ( $res ) {
+ $dbr->freeResult( $res );
+ }
+ if ( $ignoreErrors ) {
+ $dbw->ignoreErrors( $ignoreW );
+ $dbr->ignoreErrors( $ignoreR );
+ }
+
+ # Update the querycache_info record for the page
+ $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
+ $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname );
+
+ }
+ return $num;
+ }
+
+ /**
+ * This is the actual workhorse. It does everything needed to make a
+ * real, honest-to-gosh query page.
+ *
+ * @param $offset database query offset
+ * @param $limit database query limit
+ * @param $shownavigation show navigation like "next 200"?
+ */
+ function doQuery( $offset, $limit, $shownavigation=true ) {
+ global $wgUser, $wgOut, $wgLang, $wgContLang;
+
+ $this->offset = $offset;
+ $this->limit = $limit;
+
+ $sname = $this->getName();
+ $fname = get_class($this) . '::doQuery';
+ $sql = $this->getSQL();
+ $dbr =& wfGetDB( DB_SLAVE );
+ $querycache = $dbr->tableName( 'querycache' );
+
+ $wgOut->setSyndicated( $this->isSyndicated() );
+
+ if ( $this->isCached() ) {
+ $type = $dbr->strencode( $sname );
+ $sql =
+ "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value
+ FROM $querycache WHERE qc_type='$type'";
+
+ if( !$this->listoutput ) {
+
+ # Fetch the timestamp of this update
+ $tRes = $dbr->select( 'querycache_info', array( 'qci_timestamp' ), array( 'qci_type' => $type ), $fname );
+ $tRow = $dbr->fetchObject( $tRes );
+
+ if( $tRow ) {
+ $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true );
+ $cacheNotice = wfMsg( 'perfcachedts', $updated );
+ $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp );
+ $wgOut->addScript( '<script language="JavaScript">var dataCacheTime = \'' . $tRow->qci_timestamp . '\';</script>' );
+ } else {
+ $cacheNotice = wfMsg( 'perfcached' );
+ }
+
+ $wgOut->addWikiText( $cacheNotice );
+ }
+
+ }
+
+ $sql .= $this->getOrder();
+ $sql = $dbr->limitResult($sql, $limit, $offset);
+ $res = $dbr->query( $sql );
+ $num = $dbr->numRows($res);
+
+ $this->preprocessResults( $dbr, $res );
+
+ $sk = $wgUser->getSkin( );
+
+ if($shownavigation) {
+ $wgOut->addHTML( $this->getPageHeader() );
+ $top = wfShowingResults( $offset, $num);
+ $wgOut->addHTML( "<p>{$top}\n" );
+
+ # often disable 'next' link when we reach the end
+ $atend = $num < $limit;
+
+ $sl = wfViewPrevNext( $offset, $limit ,
+ $wgContLang->specialPage( $sname ),
+ wfArrayToCGI( $this->linkParameters() ), $atend );
+ $wgOut->addHTML( "<br />{$sl}</p>\n" );
+ }
+ if ( $num > 0 ) {
+ $s = array();
+ if ( ! $this->listoutput )
+ $s[] = "<ol start='" . ( $offset + 1 ) . "' class='special'>";
+
+ # Only read at most $num rows, because $res may contain the whole 1000
+ for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) {
+ $format = $this->formatResult( $sk, $obj );
+ if ( $format ) {
+ $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol &&
+ $obj->patrolled == 0 ) ? ' class="not-patrolled"' : '';
+ $s[] = $this->listoutput ? $format : "<li{$attr}>{$format}</li>\n";
+ }
+ }
+
+ if($this->tryLastResult()) {
+ // flush the very last result
+ $obj = null;
+ $format = $this->formatResult( $sk, $obj );
+ if( $format ) {
+ $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol &&
+ $obj->patrolled == 0 ) ? ' class="not-patrolled"' : '';
+ $s[] = "<li{$attr}>{$format}</li>\n";
+ }
+ }
+
+ $dbr->freeResult( $res );
+ if ( ! $this->listoutput )
+ $s[] = '</ol>';
+ $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s );
+ $wgOut->addHTML( $str );
+ }
+ if($shownavigation) {
+ $wgOut->addHTML( "<p>{$sl}</p>\n" );
+ }
+ return $num;
+ }
+
+ /**
+ * Do any necessary preprocessing of the result object.
+ * You should pass this by reference: &$db , &$res
+ */
+ function preprocessResults( $db, $res ) {}
+
+ /**
+ * Similar to above, but packaging in a syndicated feed instead of a web page
+ */
+ function doFeed( $class = '', $limit = 50 ) {
+ global $wgFeedClasses;
+
+ if( isset($wgFeedClasses[$class]) ) {
+ $feed = new $wgFeedClasses[$class](
+ $this->feedTitle(),
+ $this->feedDesc(),
+ $this->feedUrl() );
+ $feed->outHeader();
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $sql = $this->getSQL() . $this->getOrder();
+ $sql = $dbr->limitResult( $sql, $limit, 0 );
+ $res = $dbr->query( $sql, 'QueryPage::doFeed' );
+ while( $obj = $dbr->fetchObject( $res ) ) {
+ $item = $this->feedResult( $obj );
+ if( $item ) $feed->outItem( $item );
+ }
+ $dbr->freeResult( $res );
+
+ $feed->outFooter();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Override for custom handling. If the titles/links are ok, just do
+ * feedItemDesc()
+ */
+ function feedResult( $row ) {
+ if( !isset( $row->title ) ) {
+ return NULL;
+ }
+ $title = Title::MakeTitle( intval( $row->namespace ), $row->title );
+ if( $title ) {
+ $date = isset( $row->timestamp ) ? $row->timestamp : '';
+ $comments = '';
+ if( $title ) {
+ $talkpage = $title->getTalkPage();
+ $comments = $talkpage->getFullURL();
+ }
+
+ return new FeedItem(
+ $title->getPrefixedText(),
+ $this->feedItemDesc( $row ),
+ $title->getFullURL(),
+ $date,
+ $this->feedItemAuthor( $row ),
+ $comments);
+ } else {
+ return NULL;
+ }
+ }
+
+ function feedItemDesc( $row ) {
+ return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
+ }
+
+ function feedItemAuthor( $row ) {
+ return isset( $row->user_text ) ? $row->user_text : '';
+ }
+
+ function feedTitle() {
+ global $wgLanguageCode, $wgSitename;
+ $page = SpecialPage::getPage( $this->getName() );
+ $desc = $page->getDescription();
+ return "$wgSitename - $desc [$wgLanguageCode]";
+ }
+
+ function feedDesc() {
+ return wfMsg( 'tagline' );
+ }
+
+ function feedUrl() {
+ $title = Title::MakeTitle( NS_SPECIAL, $this->getName() );
+ return $title->getFullURL();
+ }
+}
+
+/**
+ * This is a subclass for very simple queries that are just looking for page
+ * titles that match some criteria. It formats each result item as a link to
+ * that page.
+ *
+ * @package MediaWiki
+ */
+class PageQueryPage extends QueryPage {
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ return $skin->makeKnownLinkObj( $nt, htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ) );
+ }
+}
+
+?>
diff --git a/includes/RawPage.php b/includes/RawPage.php
new file mode 100644
index 00000000..3cdabfd9
--- /dev/null
+++ b/includes/RawPage.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Copyright (C) 2004 Gabriel Wicke <wicke@wikidev.net>
+ * http://wikidev.net/
+ * Based on PageHistory and SpecialExport
+ *
+ * License: GPL (http://www.gnu.org/copyleft/gpl.html)
+ *
+ * @author Gabriel Wicke <wicke@wikidev.net>
+ * @package MediaWiki
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ */
+class RawPage {
+ var $mArticle, $mTitle, $mRequest;
+ var $mOldId, $mGen, $mCharset;
+ var $mSmaxage, $mMaxage;
+ var $mContentType, $mExpandTemplates;
+
+ function RawPage( &$article, $request = false ) {
+ global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType;
+
+ $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit');
+ $this->mArticle =& $article;
+ $this->mTitle =& $article->mTitle;
+
+ if ( $request === false ) {
+ $this->mRequest =& $wgRequest;
+ } else {
+ $this->mRequest = $request;
+ }
+
+ $ctype = $this->mRequest->getVal( 'ctype' );
+ $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage );
+ $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage );
+ $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand';
+
+ $oldid = $this->mRequest->getInt( 'oldid' );
+ switch ( $wgRequest->getText( 'direction' ) ) {
+ case 'next':
+ # output next revision, or nothing if there isn't one
+ if ( $oldid ) {
+ $oldid = $this->mTitle->getNextRevisionId( $oldid );
+ }
+ $oldid = $oldid ? $oldid : -1;
+ break;
+ case 'prev':
+ # output previous revision, or nothing if there isn't one
+ if ( ! $oldid ) {
+ # get the current revision so we can get the penultimate one
+ $this->mArticle->getTouched();
+ $oldid = $this->mArticle->mLatest;
+ }
+ $prev = $this->mTitle->getPreviousRevisionId( $oldid );
+ $oldid = $prev ? $prev : -1 ;
+ break;
+ case 'cur':
+ $oldid = 0;
+ break;
+ }
+ $this->mOldId = $oldid;
+
+ # special case for 'generated' raw things: user css/js
+ $gen = $this->mRequest->getVal( 'gen' );
+
+ if($gen == 'css') {
+ $this->mGen = $gen;
+ if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage;
+ if($ctype == '') $ctype = 'text/css';
+ } elseif ($gen == 'js') {
+ $this->mGen = $gen;
+ if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage;
+ if($ctype == '') $ctype = $wgJsMimeType;
+ } else {
+ $this->mGen = false;
+ }
+ $this->mCharset = $wgInputEncoding;
+ $this->mSmaxage = intval( $smaxage );
+ $this->mMaxage = $maxage;
+ if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) {
+ $this->mContentType = 'text/x-wiki';
+ } else {
+ $this->mContentType = $ctype;
+ }
+ }
+
+ function view() {
+ global $wgOut, $wgScript;
+
+ if( isset( $_SERVER['SCRIPT_URL'] ) ) {
+ # Normally we use PHP_SELF to get the URL to the script
+ # as it was called, minus the query string.
+ #
+ # Some sites use Apache rewrite rules to handle subdomains,
+ # and have PHP set up in a weird way that causes PHP_SELF
+ # to contain the rewritten URL instead of the one that the
+ # outside world sees.
+ #
+ # If in this mode, use SCRIPT_URL instead, which mod_rewrite
+ # provides containing the "before" URL.
+ $url = $_SERVER['SCRIPT_URL'];
+ } else {
+ $url = $_SERVER['PHP_SELF'];
+ }
+
+ $ua = @$_SERVER['HTTP_USER_AGENT'];
+ if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) {
+ # Internet Explorer will ignore the Content-Type header if it
+ # thinks it sees a file extension it recognizes. Make sure that
+ # all raw requests are done through the script node, which will
+ # have eg '.php' and should remain safe.
+ #
+ # We used to redirect to a canonical-form URL as a general
+ # backwards-compatibility / good-citizen nice thing. However
+ # a lot of servers are set up in buggy ways, resulting in
+ # redirect loops which hang the browser until the CSS load
+ # times out.
+ #
+ # Just return a 403 Forbidden and get it over with.
+ wfHttpError( 403, 'Forbidden',
+ 'Raw pages must be accessed through the primary script entry point.' );
+ return;
+ }
+
+ header( "Content-type: ".$this->mContentType.'; charset='.$this->mCharset );
+ # allow the client to cache this for 24 hours
+ header( 'Cache-Control: s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage );
+ echo $this->getRawText();
+ $wgOut->disable();
+ }
+
+ function getRawText() {
+ global $wgUser, $wgOut;
+ if($this->mGen) {
+ $sk = $wgUser->getSkin();
+ $sk->initPage($wgOut);
+ if($this->mGen == 'css') {
+ return $sk->getUserStylesheet();
+ } else if($this->mGen == 'js') {
+ return $sk->getUserJs();
+ }
+ } else {
+ return $this->getArticleText();
+ }
+ }
+
+ function getArticleText() {
+ $found = false;
+ $text = '';
+ if( $this->mTitle ) {
+ // If it's a MediaWiki message we can just hit the message cache
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ $key = $this->mTitle->getDBkey();
+ $text = wfMsgForContentNoTrans( $key );
+ # If the message doesn't exist, return a blank
+ if( $text == '&lt;' . $key . '&gt;' )
+ $text = '';
+ $found = true;
+ } else {
+ // Get it from the DB
+ $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId );
+ if ( $rev ) {
+ $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
+ header( "Last-modified: $lastmod" );
+ $text = $rev->getText();
+ $found = true;
+ }
+ }
+ }
+
+ # Bad title or page does not exist
+ if( !$found && $this->mContentType == 'text/x-wiki' ) {
+ # Don't return a 404 response for CSS or JavaScript;
+ # 404s aren't generally cached and it would create
+ # extra hits when user CSS/JS are on and the user doesn't
+ # have the pages.
+ header( "HTTP/1.0 404 Not Found" );
+ }
+
+ return $this->parseArticleText( $text );
+ }
+
+ function parseArticleText( $text ) {
+ if ( $text === '' )
+ return '';
+ else
+ if ( $this->mExpandTemplates ) {
+ global $wgTitle;
+
+ $parser = new Parser();
+ $parser->Options( new ParserOptions() ); // We don't want this to be user-specific
+ $parser->Title( $wgTitle );
+ $parser->OutputType( OT_HTML );
+
+ return $parser->replaceVariables( $text );
+ } else
+ return $text;
+ }
+}
+?>
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
new file mode 100644
index 00000000..f320a47a
--- /dev/null
+++ b/includes/RecentChange.php
@@ -0,0 +1,509 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Utility class for creating new RC entries
+ * mAttribs:
+ * rc_id id of the row in the recentchanges table
+ * rc_timestamp time the entry was made
+ * rc_cur_time timestamp on the cur row
+ * rc_namespace namespace #
+ * rc_title non-prefixed db key
+ * rc_type is new entry, used to determine whether updating is necessary
+ * rc_minor is minor
+ * rc_cur_id page_id of associated page entry
+ * rc_user user id who made the entry
+ * rc_user_text user name who made the entry
+ * rc_comment edit summary
+ * rc_this_oldid rev_id associated with this entry (or zero)
+ * rc_last_oldid rev_id associated with the entry before this one (or zero)
+ * rc_bot is bot, hidden
+ * rc_ip IP address of the user in dotted quad notation
+ * rc_new obsolete, use rc_type==RC_NEW
+ * rc_patrolled boolean whether or not someone has marked this edit as patrolled
+ *
+ * mExtra:
+ * prefixedDBkey prefixed db key, used by external app via msg queue
+ * lastTimestamp timestamp of previous entry, used in WHERE clause during update
+ * lang the interwiki prefix, automatically set in save()
+ * oldSize text size before the change
+ * newSize text size after the change
+ *
+ * temporary: not stored in the database
+ * notificationtimestamp
+ * numberofWatchingusers
+ *
+ * @todo document functions and variables
+ * @package MediaWiki
+ */
+class RecentChange
+{
+ var $mAttribs = array(), $mExtra = array();
+ var $mTitle = false, $mMovedToTitle = false;
+ var $numberofWatchingusers = 0 ; # Dummy to prevent error message in SpecialRecentchangeslinked
+
+ # Factory methods
+
+ /* static */ function newFromRow( $row )
+ {
+ $rc = new RecentChange;
+ $rc->loadFromRow( $row );
+ return $rc;
+ }
+
+ /* static */ function newFromCurRow( $row, $rc_this_oldid = 0 )
+ {
+ $rc = new RecentChange;
+ $rc->loadFromCurRow( $row, $rc_this_oldid );
+ $rc->notificationtimestamp = false;
+ $rc->numberofWatchingusers = false;
+ return $rc;
+ }
+
+ # Accessors
+
+ function setAttribs( $attribs )
+ {
+ $this->mAttribs = $attribs;
+ }
+
+ function setExtra( $extra )
+ {
+ $this->mExtra = $extra;
+ }
+
+ function &getTitle()
+ {
+ if ( $this->mTitle === false ) {
+ $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
+ }
+ return $this->mTitle;
+ }
+
+ function getMovedToTitle()
+ {
+ if ( $this->mMovedToTitle === false ) {
+ $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'],
+ $this->mAttribs['rc_moved_to_title'] );
+ }
+ return $this->mMovedToTitle;
+ }
+
+ # Writes the data in this object to the database
+ function save()
+ {
+ global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix, $wgUseRCPatrol;
+ $fname = 'RecentChange::save';
+
+ $dbw =& wfGetDB( DB_MASTER );
+ if ( !is_array($this->mExtra) ) {
+ $this->mExtra = array();
+ }
+ $this->mExtra['lang'] = $wgLocalInterwiki;
+
+ if ( !$wgPutIPinRC ) {
+ $this->mAttribs['rc_ip'] = '';
+ }
+
+ # Fixup database timestamps
+ $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']);
+ $this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']);
+ $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' );
+
+ # Insert new row
+ $dbw->insert( 'recentchanges', $this->mAttribs, $fname );
+
+ # Set the ID
+ $this->mAttribs['rc_id'] = $dbw->insertId();
+
+ # Update old rows, if necessary
+ if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
+ $lastTime = $this->mExtra['lastTimestamp'];
+ #$now = $this->mAttribs['rc_timestamp'];
+ #$curId = $this->mAttribs['rc_cur_id'];
+
+ # Don't bother looking for entries that have probably
+ # been purged, it just locks up the indexes needlessly.
+ global $wgRCMaxAge;
+ $age = time() - wfTimestamp( TS_UNIX, $lastTime );
+ if( $age < $wgRCMaxAge ) {
+ # live hack, will commit once tested - kate
+ # Update rc_this_oldid for the entries which were current
+ #
+ #$oldid = $this->mAttribs['rc_last_oldid'];
+ #$ns = $this->mAttribs['rc_namespace'];
+ #$title = $this->mAttribs['rc_title'];
+ #
+ #$dbw->update( 'recentchanges',
+ # array( /* SET */
+ # 'rc_this_oldid' => $oldid
+ # ), array( /* WHERE */
+ # 'rc_namespace' => $ns,
+ # 'rc_title' => $title,
+ # 'rc_timestamp' => $dbw->timestamp( $lastTime )
+ # ), $fname
+ #);
+ }
+
+ # Update rc_cur_time
+ #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ),
+ # array( 'rc_cur_id' => $curId ), $fname );
+ }
+
+ # Notify external application via UDP
+ if ( $wgRC2UDPAddress ) {
+ $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+ if ( $conn ) {
+ $line = $wgRC2UDPPrefix . $this->getIRCLine();
+ socket_sendto( $conn, $line, strlen($line), 0, $wgRC2UDPAddress, $wgRC2UDPPort );
+ socket_close( $conn );
+ }
+ }
+
+ // E-mail notifications
+ global $wgUseEnotif;
+ if( $wgUseEnotif ) {
+ # this would be better as an extension hook
+ include_once( "UserMailer.php" );
+ $enotif = new EmailNotification();
+ $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
+ $enotif->notifyOnPageChange( $title,
+ $this->mAttribs['rc_timestamp'],
+ $this->mAttribs['rc_comment'],
+ $this->mAttribs['rc_minor'],
+ $this->mAttribs['rc_last_oldid'] );
+ }
+
+ }
+
+ # Marks a certain row as patrolled
+ function markPatrolled( $rcid )
+ {
+ $fname = 'RecentChange::markPatrolled';
+
+ $dbw =& wfGetDB( DB_MASTER );
+
+ $dbw->update( 'recentchanges',
+ array( /* SET */
+ 'rc_patrolled' => 1
+ ), array( /* WHERE */
+ 'rc_id' => $rcid
+ ), $fname
+ );
+ }
+
+ # Makes an entry in the database corresponding to an edit
+ /*static*/ function notifyEdit( $timestamp, &$title, $minor, &$user, $comment,
+ $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0,
+ $newId = 0)
+ {
+ if ( $bot == 'default' ) {
+ $bot = $user->isBot();
+ }
+
+ if ( !$ip ) {
+ $ip = wfGetIP();
+ if ( !$ip ) {
+ $ip = '';
+ }
+ }
+
+ $rc = new RecentChange;
+ $rc->mAttribs = array(
+ 'rc_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $title->getNamespace(),
+ 'rc_title' => $title->getDBkey(),
+ 'rc_type' => RC_EDIT,
+ 'rc_minor' => $minor ? 1 : 0,
+ 'rc_cur_id' => $title->getArticleID(),
+ 'rc_user' => $user->getID(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => $newId,
+ 'rc_last_oldid' => $oldId,
+ 'rc_bot' => $bot ? 1 : 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => $ip,
+ 'rc_patrolled' => 0,
+ 'rc_new' => 0 # obsolete
+ );
+
+ $rc->mExtra = array(
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
+ 'lastTimestamp' => $lastTimestamp,
+ 'oldSize' => $oldSize,
+ 'newSize' => $newSize,
+ );
+ $rc->save();
+ return( $rc->mAttribs['rc_id'] );
+ }
+
+ # Makes an entry in the database corresponding to page creation
+ # Note: the title object must be loaded with the new id using resetArticleID()
+ /*static*/ function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default",
+ $ip='', $size = 0, $newId = 0 )
+ {
+ if ( !$ip ) {
+ $ip = wfGetIP();
+ if ( !$ip ) {
+ $ip = '';
+ }
+ }
+ if ( $bot == 'default' ) {
+ $bot = $user->isBot();
+ }
+
+ $rc = new RecentChange;
+ $rc->mAttribs = array(
+ 'rc_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $title->getNamespace(),
+ 'rc_title' => $title->getDBkey(),
+ 'rc_type' => RC_NEW,
+ 'rc_minor' => $minor ? 1 : 0,
+ 'rc_cur_id' => $title->getArticleID(),
+ 'rc_user' => $user->getID(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => $newId,
+ 'rc_last_oldid' => 0,
+ 'rc_bot' => $bot ? 1 : 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => $ip,
+ 'rc_patrolled' => 0,
+ 'rc_new' => 1 # obsolete
+ );
+
+ $rc->mExtra = array(
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
+ 'lastTimestamp' => 0,
+ 'oldSize' => 0,
+ 'newSize' => $size
+ );
+ $rc->save();
+ return( $rc->mAttribs['rc_id'] );
+ }
+
+ # Makes an entry in the database corresponding to a rename
+ /*static*/ function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
+ {
+ if ( !$ip ) {
+ $ip = wfGetIP();
+ if ( !$ip ) {
+ $ip = '';
+ }
+ }
+
+ $rc = new RecentChange;
+ $rc->mAttribs = array(
+ 'rc_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $oldTitle->getNamespace(),
+ 'rc_title' => $oldTitle->getDBkey(),
+ 'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE,
+ 'rc_minor' => 0,
+ 'rc_cur_id' => $oldTitle->getArticleID(),
+ 'rc_user' => $user->getID(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_bot' => $user->isBot() ? 1 : 0,
+ 'rc_moved_to_ns' => $newTitle->getNamespace(),
+ 'rc_moved_to_title' => $newTitle->getDBkey(),
+ 'rc_ip' => $ip,
+ 'rc_new' => 0, # obsolete
+ 'rc_patrolled' => 1
+ );
+
+ $rc->mExtra = array(
+ 'prefixedDBkey' => $oldTitle->getPrefixedDBkey(),
+ 'lastTimestamp' => 0,
+ 'prefixedMoveTo' => $newTitle->getPrefixedDBkey()
+ );
+ $rc->save();
+ }
+
+ /* static */ function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
+ RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false );
+ }
+
+ /* static */ function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
+ RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
+ }
+
+ # A log entry is different to an edit in that previous revisions are
+ # not kept
+ /*static*/ function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
+ $type, $action, $target, $logComment, $params )
+ {
+ if ( !$ip ) {
+ $ip = wfGetIP();
+ if ( !$ip ) {
+ $ip = '';
+ }
+ }
+
+ $rc = new RecentChange;
+ $rc->mAttribs = array(
+ 'rc_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $title->getNamespace(),
+ 'rc_title' => $title->getDBkey(),
+ 'rc_type' => RC_LOG,
+ 'rc_minor' => 0,
+ 'rc_cur_id' => $title->getArticleID(),
+ 'rc_user' => $user->getID(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_bot' => $user->isBot() ? 1 : 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => $ip,
+ 'rc_patrolled' => 1,
+ 'rc_new' => 0 # obsolete
+ );
+ $rc->mExtra = array(
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
+ 'lastTimestamp' => 0,
+ 'logType' => $type,
+ 'logAction' => $action,
+ 'logComment' => $logComment,
+ 'logTarget' => $target,
+ 'logParams' => $params
+ );
+ $rc->save();
+ }
+
+ # Initialises the members of this object from a mysql row object
+ function loadFromRow( $row )
+ {
+ $this->mAttribs = get_object_vars( $row );
+ $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]);
+ $this->mExtra = array();
+ }
+
+ # Makes a pseudo-RC entry from a cur row, for watchlists and things
+ function loadFromCurRow( $row )
+ {
+ $this->mAttribs = array(
+ 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp),
+ 'rc_cur_time' => $row->rev_timestamp,
+ 'rc_user' => $row->rev_user,
+ 'rc_user_text' => $row->rev_user_text,
+ 'rc_namespace' => $row->page_namespace,
+ 'rc_title' => $row->page_title,
+ 'rc_comment' => $row->rev_comment,
+ 'rc_minor' => $row->rev_minor_edit ? 1 : 0,
+ 'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
+ 'rc_cur_id' => $row->page_id,
+ 'rc_this_oldid' => $row->rev_id,
+ 'rc_last_oldid' => isset($row->rc_last_oldid) ? $row->rc_last_oldid : 0,
+ 'rc_bot' => 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => '',
+ 'rc_id' => $row->rc_id,
+ 'rc_patrolled' => $row->rc_patrolled,
+ 'rc_new' => $row->page_is_new # obsolete
+ );
+
+ $this->mExtra = array();
+ }
+
+
+ /**
+ * Gets the end part of the diff URL associated with this object
+ * Blank if no diff link should be displayed
+ */
+ function diffLinkTrail( $forceCur )
+ {
+ if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
+ $trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) .
+ "&oldid=" . (int)($this->mAttribs['rc_last_oldid']);
+ if ( $forceCur ) {
+ $trail .= '&diff=0' ;
+ } else {
+ $trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']);
+ }
+ } else {
+ $trail = '';
+ }
+ return $trail;
+ }
+
+ function cleanupForIRC( $text ) {
+ return str_replace(array("\n", "\r"), array("", ""), $text);
+ }
+
+ function getIRCLine() {
+ global $wgUseRCPatrol;
+
+ extract($this->mAttribs);
+ extract($this->mExtra);
+
+ $titleObj =& $this->getTitle();
+ if ( $rc_type == RC_LOG ) {
+ $title = Namespace::getCanonicalName( $titleObj->getNamespace() ) . $titleObj->getText();
+ } else {
+ $title = $titleObj->getPrefixedText();
+ }
+ $title = $this->cleanupForIRC( $title );
+
+ $bad = array("\n", "\r");
+ $empty = array("", "");
+ $title = $titleObj->getPrefixedText();
+ $title = str_replace($bad, $empty, $title);
+
+ // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26
+ if ( $rc_type == RC_LOG ) {
+ $url = '';
+ } elseif ( $rc_new && $wgUseRCPatrol ) {
+ $url = $titleObj->getInternalURL("rcid=$rc_id");
+ } else if ( $rc_new ) {
+ $url = $titleObj->getInternalURL();
+ } else if ( $wgUseRCPatrol ) {
+ $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid&rcid=$rc_id");
+ } else {
+ $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid");
+ }
+
+ if ( isset( $oldSize ) && isset( $newSize ) ) {
+ $szdiff = $newSize - $oldSize;
+ if ($szdiff < -500) {
+ $szdiff = "\002$szdiff\002";
+ } elseif ($szdiff >= 0) {
+ $szdiff = '+' . $szdiff ;
+ }
+ $szdiff = '(' . $szdiff . ')' ;
+ } else {
+ $szdiff = '';
+ }
+
+ $user = $this->cleanupForIRC( $rc_user_text );
+
+ if ( $rc_type == RC_LOG ) {
+ $logTargetText = $logTarget->getPrefixedText();
+ $comment = $this->cleanupForIRC( str_replace( $logTargetText, "\00302$logTargetText\00310", $rc_comment ) );
+ $flag = $logAction;
+ } else {
+ $comment = $this->cleanupForIRC( $rc_comment );
+ $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : "");
+ }
+ # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
+ # no colour (\003) switches back to the term default
+ $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " .
+ "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
+ return $fullString;
+ }
+
+}
+?>
diff --git a/includes/Revision.php b/includes/Revision.php
new file mode 100644
index 00000000..653bacb8
--- /dev/null
+++ b/includes/Revision.php
@@ -0,0 +1,799 @@
+<?php
+/**
+ * @package MediaWiki
+ * @todo document
+ */
+
+/** */
+require_once( 'Database.php' );
+
+/**
+ * @package MediaWiki
+ * @todo document
+ */
+class Revision {
+ const DELETED_TEXT = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+
+ /**
+ * Load a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * @param int $id
+ * @static
+ * @access public
+ */
+ function newFromId( $id ) {
+ return Revision::newFromConds(
+ array( 'page_id=rev_page',
+ 'rev_id' => intval( $id ) ) );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given title. If not attached
+ * to that title, will return null.
+ *
+ * @param Title $title
+ * @param int $id
+ * @return Revision
+ * @access public
+ * @static
+ */
+ function newFromTitle( &$title, $id = 0 ) {
+ if( $id ) {
+ $matchId = intval( $id );
+ } else {
+ $matchId = 'page_latest';
+ }
+ return Revision::newFromConds(
+ array( "rev_id=$matchId",
+ 'page_id=rev_page',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbkey() ) );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page. If not attached
+ * to that page, will return null.
+ *
+ * @param Database $db
+ * @param int $pageid
+ * @param int $id
+ * @return Revision
+ * @access public
+ */
+ function loadFromPageId( &$db, $pageid, $id = 0 ) {
+ $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
+ if( $id ) {
+ $conds['rev_id']=intval($id);
+ } else {
+ $conds[]='rev_id=page_latest';
+ }
+ return Revision::loadFromConds( $db, $conds );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page. If not attached
+ * to that page, will return null.
+ *
+ * @param Database $db
+ * @param Title $title
+ * @param int $id
+ * @return Revision
+ * @access public
+ */
+ function loadFromTitle( &$db, $title, $id = 0 ) {
+ if( $id ) {
+ $matchId = intval( $id );
+ } else {
+ $matchId = 'page_latest';
+ }
+ return Revision::loadFromConds(
+ $db,
+ array( "rev_id=$matchId",
+ 'page_id=rev_page',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbkey() ) );
+ }
+
+ /**
+ * Load the revision for the given title with the given timestamp.
+ * WARNING: Timestamps may in some circumstances not be unique,
+ * so this isn't the best key to use.
+ *
+ * @param Database $db
+ * @param Title $title
+ * @param string $timestamp
+ * @return Revision
+ * @access public
+ * @static
+ */
+ function loadFromTimestamp( &$db, &$title, $timestamp ) {
+ return Revision::loadFromConds(
+ $db,
+ array( 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_id=rev_page',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbkey() ) );
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision.
+ *
+ * @param array $conditions
+ * @return Revision
+ * @static
+ * @access private
+ */
+ function newFromConds( $conditions ) {
+ $db =& wfGetDB( DB_SLAVE );
+ $row = Revision::loadFromConds( $db, $conditions );
+ if( is_null( $row ) ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $row = Revision::loadFromConds( $dbw, $conditions );
+ }
+ return $row;
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision from
+ * the given database connection.
+ *
+ * @param Database $db
+ * @param array $conditions
+ * @return Revision
+ * @static
+ * @access private
+ */
+ function loadFromConds( &$db, $conditions ) {
+ $res = Revision::fetchFromConds( $db, $conditions );
+ if( $res ) {
+ $row = $res->fetchObject();
+ $res->free();
+ if( $row ) {
+ $ret = new Revision( $row );
+ return $ret;
+ }
+ }
+ $ret = null;
+ return $ret;
+ }
+
+ /**
+ * Return a wrapper for a series of database rows to
+ * fetch all of a given page's revisions in turn.
+ * Each row can be fed to the constructor to get objects.
+ *
+ * @param Title $title
+ * @return ResultWrapper
+ * @static
+ * @access public
+ */
+ function fetchAllRevisions( &$title ) {
+ return Revision::fetchFromConds(
+ wfGetDB( DB_SLAVE ),
+ array( 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbkey(),
+ 'page_id=rev_page' ) );
+ }
+
+ /**
+ * Return a wrapper for a series of database rows to
+ * fetch all of a given page's revisions in turn.
+ * Each row can be fed to the constructor to get objects.
+ *
+ * @param Title $title
+ * @return ResultWrapper
+ * @static
+ * @access public
+ */
+ function fetchRevision( &$title ) {
+ return Revision::fetchFromConds(
+ wfGetDB( DB_SLAVE ),
+ array( 'rev_id=page_latest',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbkey(),
+ 'page_id=rev_page' ) );
+ }
+
+ /**
+ * Given a set of conditions, return a ResultWrapper
+ * which will return matching database rows with the
+ * fields necessary to build Revision objects.
+ *
+ * @param Database $db
+ * @param array $conditions
+ * @return ResultWrapper
+ * @static
+ * @access private
+ */
+ function fetchFromConds( &$db, $conditions ) {
+ $res = $db->select(
+ array( 'page', 'revision' ),
+ array( 'page_namespace',
+ 'page_title',
+ 'page_latest',
+ 'rev_id',
+ 'rev_page',
+ 'rev_text_id',
+ 'rev_comment',
+ 'rev_user_text',
+ 'rev_user',
+ 'rev_minor_edit',
+ 'rev_timestamp',
+ 'rev_deleted' ),
+ $conditions,
+ 'Revision::fetchRow',
+ array( 'LIMIT' => 1 ) );
+ $ret = $db->resultObject( $res );
+ return $ret;
+ }
+
+ /**
+ * @param object $row
+ * @access private
+ */
+ function Revision( $row ) {
+ if( is_object( $row ) ) {
+ $this->mId = intval( $row->rev_id );
+ $this->mPage = intval( $row->rev_page );
+ $this->mTextId = intval( $row->rev_text_id );
+ $this->mComment = $row->rev_comment;
+ $this->mUserText = $row->rev_user_text;
+ $this->mUser = intval( $row->rev_user );
+ $this->mMinorEdit = intval( $row->rev_minor_edit );
+ $this->mTimestamp = $row->rev_timestamp;
+ $this->mDeleted = intval( $row->rev_deleted );
+
+ if( isset( $row->page_latest ) ) {
+ $this->mCurrent = ( $row->rev_id == $row->page_latest );
+ $this->mTitle = Title::makeTitle( $row->page_namespace,
+ $row->page_title );
+ } else {
+ $this->mCurrent = false;
+ $this->mTitle = null;
+ }
+
+ if( isset( $row->old_text ) ) {
+ $this->mText = $this->getRevisionText( $row );
+ } else {
+ $this->mText = null;
+ }
+ } elseif( is_array( $row ) ) {
+ // Build a new revision to be saved...
+ global $wgUser;
+
+ $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+ $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
+ $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
+ $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
+ $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
+ $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
+ $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
+ $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
+
+ // Enforce spacing trimming on supplied text
+ $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
+ $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+
+ $this->mTitle = null; # Load on demand if needed
+ $this->mCurrent = false;
+ } else {
+ throw new MWException( 'Revision constructor passed invalid row format.' );
+ }
+ }
+
+ /**#@+
+ * @access public
+ */
+
+ /**
+ * @return int
+ */
+ function getId() {
+ return $this->mId;
+ }
+
+ /**
+ * @return int
+ */
+ function getTextId() {
+ return $this->mTextId;
+ }
+
+ /**
+ * Returns the title of the page associated with this entry.
+ * @return Title
+ */
+ function getTitle() {
+ if( isset( $this->mTitle ) ) {
+ return $this->mTitle;
+ }
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow(
+ array( 'page', 'revision' ),
+ array( 'page_namespace', 'page_title' ),
+ array( 'page_id=rev_page',
+ 'rev_id' => $this->mId ),
+ 'Revision::getTitle' );
+ if( $row ) {
+ $this->mTitle = Title::makeTitle( $row->page_namespace,
+ $row->page_title );
+ }
+ return $this->mTitle;
+ }
+
+ /**
+ * Set the title of the revision
+ * @param Title $title
+ */
+ function setTitle( $title ) {
+ $this->mTitle = $title;
+ }
+
+ /**
+ * @return int
+ */
+ function getPage() {
+ return $this->mPage;
+ }
+
+ /**
+ * Fetch revision's user id if it's available to all users
+ * @return int
+ */
+ function getUser() {
+ if( $this->isDeleted( self::DELETED_USER ) ) {
+ return 0;
+ } else {
+ return $this->mUser;
+ }
+ }
+
+ /**
+ * Fetch revision's user id without regard for the current user's permissions
+ * @return string
+ */
+ function getRawUser() {
+ return $this->mUser;
+ }
+
+ /**
+ * Fetch revision's username if it's available to all users
+ * @return string
+ */
+ function getUserText() {
+ if( $this->isDeleted( self::DELETED_USER ) ) {
+ return "";
+ } else {
+ return $this->mUserText;
+ }
+ }
+
+ /**
+ * Fetch revision's username without regard for view restrictions
+ * @return string
+ */
+ function getRawUserText() {
+ return $this->mUserText;
+ }
+
+ /**
+ * Fetch revision comment if it's available to all users
+ * @return string
+ */
+ function getComment() {
+ if( $this->isDeleted( self::DELETED_COMMENT ) ) {
+ return "";
+ } else {
+ return $this->mComment;
+ }
+ }
+
+ /**
+ * Fetch revision comment without regard for the current user's permissions
+ * @return string
+ */
+ function getRawComment() {
+ return $this->mComment;
+ }
+
+ /**
+ * @return bool
+ */
+ function isMinor() {
+ return (bool)$this->mMinorEdit;
+ }
+
+ /**
+ * int $field one of DELETED_* bitfield constants
+ * @return bool
+ */
+ function isDeleted( $field ) {
+ return ($this->mDeleted & $field) == $field;
+ }
+
+ /**
+ * Fetch revision text if it's available to all users
+ * @return string
+ */
+ function getText() {
+ if( $this->isDeleted( self::DELETED_TEXT ) ) {
+ return "";
+ } else {
+ return $this->getRawText();
+ }
+ }
+
+ /**
+ * Fetch revision text without regard for view restrictions
+ * @return string
+ */
+ function getRawText() {
+ if( is_null( $this->mText ) ) {
+ // Revision text is immutable. Load on demand:
+ $this->mText = $this->loadText();
+ }
+ return $this->mText;
+ }
+
+ /**
+ * @return string
+ */
+ function getTimestamp() {
+ return wfTimestamp(TS_MW, $this->mTimestamp);
+ }
+
+ /**
+ * @return bool
+ */
+ function isCurrent() {
+ return $this->mCurrent;
+ }
+
+ /**
+ * @return Revision
+ */
+ function getPrevious() {
+ $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
+ if ( $prev ) {
+ return Revision::newFromTitle( $this->mTitle, $prev );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return Revision
+ */
+ function getNext() {
+ $next = $this->mTitle->getNextRevisionID( $this->mId );
+ if ( $next ) {
+ return Revision::newFromTitle( $this->mTitle, $next );
+ } else {
+ return null;
+ }
+ }
+ /**#@-*/
+
+ /**
+ * Get revision text associated with an old or archive row
+ * $row is usually an object from wfFetchRow(), both the flags and the text
+ * field must be included
+ * @static
+ * @param integer $row Id of a row
+ * @param string $prefix table prefix (default 'old_')
+ * @return string $text|false the text requested
+ */
+ function getRevisionText( $row, $prefix = 'old_' ) {
+ $fname = 'Revision::getRevisionText';
+ wfProfileIn( $fname );
+
+ # Get data
+ $textField = $prefix . 'text';
+ $flagsField = $prefix . 'flags';
+
+ if( isset( $row->$flagsField ) ) {
+ $flags = explode( ',', $row->$flagsField );
+ } else {
+ $flags = array();
+ }
+
+ if( isset( $row->$textField ) ) {
+ $text = $row->$textField;
+ } else {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ # Use external methods for external objects, text in table is URL-only then
+ if ( in_array( 'external', $flags ) ) {
+ $url=$text;
+ @list($proto,$path)=explode('://',$url,2);
+ if ($path=="") {
+ wfProfileOut( $fname );
+ return false;
+ }
+ require_once('ExternalStore.php');
+ $text=ExternalStore::fetchFromURL($url);
+ }
+
+ // If the text was fetched without an error, convert it
+ if ( $text !== false ) {
+ if( in_array( 'gzip', $flags ) ) {
+ # Deal with optional compression of archived pages.
+ # This can be done periodically via maintenance/compressOld.php, and
+ # as pages are saved if $wgCompressRevisions is set.
+ $text = gzinflate( $text );
+ }
+
+ if( in_array( 'object', $flags ) ) {
+ # Generic compressed storage
+ $obj = unserialize( $text );
+ if ( !is_object( $obj ) ) {
+ // Invalid object
+ wfProfileOut( $fname );
+ return false;
+ }
+ $text = $obj->getText();
+ }
+
+ global $wgLegacyEncoding;
+ if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
+ # Old revisions kept around in a legacy encoding?
+ # Upconvert on demand.
+ global $wgInputEncoding, $wgContLang;
+ $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
+ }
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * If $wgCompressRevisions is enabled, we will compress data.
+ * The input string is modified in place.
+ * Return value is the flags field: contains 'gzip' if the
+ * data is compressed, and 'utf-8' if we're saving in UTF-8
+ * mode.
+ *
+ * @static
+ * @param mixed $text reference to a text
+ * @return string
+ */
+ function compressRevisionText( &$text ) {
+ global $wgCompressRevisions;
+ $flags = array();
+
+ # Revisions not marked this way will be converted
+ # on load if $wgLegacyCharset is set in the future.
+ $flags[] = 'utf-8';
+
+ if( $wgCompressRevisions ) {
+ if( function_exists( 'gzdeflate' ) ) {
+ $text = gzdeflate( $text );
+ $flags[] = 'gzip';
+ } else {
+ wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
+ }
+ }
+ return implode( ',', $flags );
+ }
+
+ /**
+ * Insert a new revision into the database, returning the new revision ID
+ * number on success and dies horribly on failure.
+ *
+ * @param Database $dbw
+ * @return int
+ */
+ function insertOn( &$dbw ) {
+ global $wgDefaultExternalStore;
+
+ $fname = 'Revision::insertOn';
+ wfProfileIn( $fname );
+
+ $data = $this->mText;
+ $flags = Revision::compressRevisionText( $data );
+
+ # Write to external storage if required
+ if ( $wgDefaultExternalStore ) {
+ if ( is_array( $wgDefaultExternalStore ) ) {
+ // Distribute storage across multiple clusters
+ $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
+ } else {
+ $store = $wgDefaultExternalStore;
+ }
+ require_once('ExternalStore.php');
+ // Store and get the URL
+ $data = ExternalStore::insert( $store, $data );
+ if ( !$data ) {
+ # This should only happen in the case of a configuration error, where the external store is not valid
+ throw new MWException( "Unable to store text to external storage $store" );
+ }
+ if ( $flags ) {
+ $flags .= ',';
+ }
+ $flags .= 'external';
+ }
+
+ # Record the text (or external storage URL) to the text table
+ if( !isset( $this->mTextId ) ) {
+ $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
+ $dbw->insert( 'text',
+ array(
+ 'old_id' => $old_id,
+ 'old_text' => $data,
+ 'old_flags' => $flags,
+ ), $fname
+ );
+ $this->mTextId = $dbw->insertId();
+ }
+
+ # Record the edit in revisions
+ $rev_id = isset( $this->mId )
+ ? $this->mId
+ : $dbw->nextSequenceValue( 'rev_rev_id_val' );
+ $dbw->insert( 'revision',
+ array(
+ 'rev_id' => $rev_id,
+ 'rev_page' => $this->mPage,
+ 'rev_text_id' => $this->mTextId,
+ 'rev_comment' => $this->mComment,
+ 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+ 'rev_user' => $this->mUser,
+ 'rev_user_text' => $this->mUserText,
+ 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
+ 'rev_deleted' => $this->mDeleted,
+ ), $fname
+ );
+
+ $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
+ wfProfileOut( $fname );
+ return $this->mId;
+ }
+
+ /**
+ * Lazy-load the revision's text.
+ * Currently hardcoded to the 'text' table storage engine.
+ *
+ * @return string
+ * @access private
+ */
+ function loadText() {
+ $fname = 'Revision::loadText';
+ wfProfileIn( $fname );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $this->getTextId() ),
+ $fname);
+
+ if( !$row ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $row = $dbw->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $this->getTextId() ),
+ $fname);
+ }
+
+ $text = Revision::getRevisionText( $row );
+ wfProfileOut( $fname );
+
+ return $text;
+ }
+
+ /**
+ * Create a new null-revision for insertion into a page's
+ * history. This will not re-save the text, but simply refer
+ * to the text from the previous version.
+ *
+ * Such revisions can for instance identify page rename
+ * operations and other such meta-modifications.
+ *
+ * @param Database $dbw
+ * @param int $pageId ID number of the page to read from
+ * @param string $summary
+ * @param bool $minor
+ * @return Revision
+ */
+ function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
+ $fname = 'Revision::newNullRevision';
+ wfProfileIn( $fname );
+
+ $current = $dbw->selectRow(
+ array( 'page', 'revision' ),
+ array( 'page_latest', 'rev_text_id' ),
+ array(
+ 'page_id' => $pageId,
+ 'page_latest=rev_id',
+ ),
+ $fname );
+
+ if( $current ) {
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'comment' => $summary,
+ 'minor_edit' => $minor,
+ 'text_id' => $current->rev_text_id,
+ ) );
+ } else {
+ $revision = null;
+ }
+
+ wfProfileOut( $fname );
+ return $revision;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ * @param int $field one of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @return bool
+ */
+ function userCan( $field ) {
+ if( ( $this->mDeleted & $field ) == $field ) {
+ global $wgUser;
+ $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
+ ? 'hiderevision'
+ : 'deleterevision';
+ wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+ return $wgUser->isAllowed( $permission );
+ } else {
+ return true;
+ }
+ }
+
+
+ /**
+ * Get rev_timestamp from rev_id, without loading the rest of the row
+ * @param integer $id
+ */
+ static function getTimestampFromID( $id ) {
+ $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_id' => $id ), __METHOD__ );
+ if ( $timestamp === false ) {
+ # Not in slave, try master
+ $dbw =& wfGetDB( DB_MASTER );
+ $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_id' => $id ), __METHOD__ );
+ }
+ return $timestamp;
+ }
+
+ static function countByPageId( $db, $id ) {
+ $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
+ array( 'rev_page' => $id ), __METHOD__ );
+ if( $row ) {
+ return $row->revCount;
+ }
+ return 0;
+ }
+
+ static function countByTitle( $db, $title ) {
+ $id = $title->getArticleId();
+ if( $id ) {
+ return Revision::countByPageId( $db, $id );
+ }
+ return 0;
+ }
+}
+
+/**
+ * Aliases for backwards compatibility with 1.6
+ */
+define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
+define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
+define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
+define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );
+
+
+?>
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
new file mode 100644
index 00000000..f5a24dfa
--- /dev/null
+++ b/includes/Sanitizer.php
@@ -0,0 +1,1184 @@
+<?php
+/**
+ * XHTML sanitizer for MediaWiki
+ *
+ * Copyright (C) 2002-2005 Brion Vibber <brion@pobox.com> et al
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @package MediaWiki
+ * @subpackage Parser
+ */
+
+/**
+ * Regular expression to match various types of character references in
+ * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
+ */
+define( 'MW_CHAR_REFS_REGEX',
+ '/&([A-Za-z0-9]+);
+ |&\#([0-9]+);
+ |&\#x([0-9A-Za-z]+);
+ |&\#X([0-9A-Za-z]+);
+ |(&)/x' );
+
+/**
+ * Regular expression to match HTML/XML attribute pairs within a tag.
+ * Allows some... latitude.
+ * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes
+ */
+$attrib = '[A-Za-z0-9]';
+$space = '[\x09\x0a\x0d\x20]';
+define( 'MW_ATTRIBS_REGEX',
+ "/(?:^|$space)($attrib+)
+ ($space*=$space*
+ (?:
+ # The attribute value: quoted or alone
+ \"([^<\"]*)\"
+ | '([^<']*)'
+ | ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+)
+ | (\#[0-9a-fA-F]+) # Technically wrong, but lots of
+ # colors are specified like this.
+ # We'll be normalizing it.
+ )
+ )?(?=$space|\$)/sx" );
+
+/**
+ * List of all named character entities defined in HTML 4.01
+ * http://www.w3.org/TR/html4/sgml/entities.html
+ * @private
+ */
+global $wgHtmlEntities;
+$wgHtmlEntities = array(
+ 'Aacute' => 193,
+ 'aacute' => 225,
+ 'Acirc' => 194,
+ 'acirc' => 226,
+ 'acute' => 180,
+ 'AElig' => 198,
+ 'aelig' => 230,
+ 'Agrave' => 192,
+ 'agrave' => 224,
+ 'alefsym' => 8501,
+ 'Alpha' => 913,
+ 'alpha' => 945,
+ 'amp' => 38,
+ 'and' => 8743,
+ 'ang' => 8736,
+ 'Aring' => 197,
+ 'aring' => 229,
+ 'asymp' => 8776,
+ 'Atilde' => 195,
+ 'atilde' => 227,
+ 'Auml' => 196,
+ 'auml' => 228,
+ 'bdquo' => 8222,
+ 'Beta' => 914,
+ 'beta' => 946,
+ 'brvbar' => 166,
+ 'bull' => 8226,
+ 'cap' => 8745,
+ 'Ccedil' => 199,
+ 'ccedil' => 231,
+ 'cedil' => 184,
+ 'cent' => 162,
+ 'Chi' => 935,
+ 'chi' => 967,
+ 'circ' => 710,
+ 'clubs' => 9827,
+ 'cong' => 8773,
+ 'copy' => 169,
+ 'crarr' => 8629,
+ 'cup' => 8746,
+ 'curren' => 164,
+ 'dagger' => 8224,
+ 'Dagger' => 8225,
+ 'darr' => 8595,
+ 'dArr' => 8659,
+ 'deg' => 176,
+ 'Delta' => 916,
+ 'delta' => 948,
+ 'diams' => 9830,
+ 'divide' => 247,
+ 'Eacute' => 201,
+ 'eacute' => 233,
+ 'Ecirc' => 202,
+ 'ecirc' => 234,
+ 'Egrave' => 200,
+ 'egrave' => 232,
+ 'empty' => 8709,
+ 'emsp' => 8195,
+ 'ensp' => 8194,
+ 'Epsilon' => 917,
+ 'epsilon' => 949,
+ 'equiv' => 8801,
+ 'Eta' => 919,
+ 'eta' => 951,
+ 'ETH' => 208,
+ 'eth' => 240,
+ 'Euml' => 203,
+ 'euml' => 235,
+ 'euro' => 8364,
+ 'exist' => 8707,
+ 'fnof' => 402,
+ 'forall' => 8704,
+ 'frac12' => 189,
+ 'frac14' => 188,
+ 'frac34' => 190,
+ 'frasl' => 8260,
+ 'Gamma' => 915,
+ 'gamma' => 947,
+ 'ge' => 8805,
+ 'gt' => 62,
+ 'harr' => 8596,
+ 'hArr' => 8660,
+ 'hearts' => 9829,
+ 'hellip' => 8230,
+ 'Iacute' => 205,
+ 'iacute' => 237,
+ 'Icirc' => 206,
+ 'icirc' => 238,
+ 'iexcl' => 161,
+ 'Igrave' => 204,
+ 'igrave' => 236,
+ 'image' => 8465,
+ 'infin' => 8734,
+ 'int' => 8747,
+ 'Iota' => 921,
+ 'iota' => 953,
+ 'iquest' => 191,
+ 'isin' => 8712,
+ 'Iuml' => 207,
+ 'iuml' => 239,
+ 'Kappa' => 922,
+ 'kappa' => 954,
+ 'Lambda' => 923,
+ 'lambda' => 955,
+ 'lang' => 9001,
+ 'laquo' => 171,
+ 'larr' => 8592,
+ 'lArr' => 8656,
+ 'lceil' => 8968,
+ 'ldquo' => 8220,
+ 'le' => 8804,
+ 'lfloor' => 8970,
+ 'lowast' => 8727,
+ 'loz' => 9674,
+ 'lrm' => 8206,
+ 'lsaquo' => 8249,
+ 'lsquo' => 8216,
+ 'lt' => 60,
+ 'macr' => 175,
+ 'mdash' => 8212,
+ 'micro' => 181,
+ 'middot' => 183,
+ 'minus' => 8722,
+ 'Mu' => 924,
+ 'mu' => 956,
+ 'nabla' => 8711,
+ 'nbsp' => 160,
+ 'ndash' => 8211,
+ 'ne' => 8800,
+ 'ni' => 8715,
+ 'not' => 172,
+ 'notin' => 8713,
+ 'nsub' => 8836,
+ 'Ntilde' => 209,
+ 'ntilde' => 241,
+ 'Nu' => 925,
+ 'nu' => 957,
+ 'Oacute' => 211,
+ 'oacute' => 243,
+ 'Ocirc' => 212,
+ 'ocirc' => 244,
+ 'OElig' => 338,
+ 'oelig' => 339,
+ 'Ograve' => 210,
+ 'ograve' => 242,
+ 'oline' => 8254,
+ 'Omega' => 937,
+ 'omega' => 969,
+ 'Omicron' => 927,
+ 'omicron' => 959,
+ 'oplus' => 8853,
+ 'or' => 8744,
+ 'ordf' => 170,
+ 'ordm' => 186,
+ 'Oslash' => 216,
+ 'oslash' => 248,
+ 'Otilde' => 213,
+ 'otilde' => 245,
+ 'otimes' => 8855,
+ 'Ouml' => 214,
+ 'ouml' => 246,
+ 'para' => 182,
+ 'part' => 8706,
+ 'permil' => 8240,
+ 'perp' => 8869,
+ 'Phi' => 934,
+ 'phi' => 966,
+ 'Pi' => 928,
+ 'pi' => 960,
+ 'piv' => 982,
+ 'plusmn' => 177,
+ 'pound' => 163,
+ 'prime' => 8242,
+ 'Prime' => 8243,
+ 'prod' => 8719,
+ 'prop' => 8733,
+ 'Psi' => 936,
+ 'psi' => 968,
+ 'quot' => 34,
+ 'radic' => 8730,
+ 'rang' => 9002,
+ 'raquo' => 187,
+ 'rarr' => 8594,
+ 'rArr' => 8658,
+ 'rceil' => 8969,
+ 'rdquo' => 8221,
+ 'real' => 8476,
+ 'reg' => 174,
+ 'rfloor' => 8971,
+ 'Rho' => 929,
+ 'rho' => 961,
+ 'rlm' => 8207,
+ 'rsaquo' => 8250,
+ 'rsquo' => 8217,
+ 'sbquo' => 8218,
+ 'Scaron' => 352,
+ 'scaron' => 353,
+ 'sdot' => 8901,
+ 'sect' => 167,
+ 'shy' => 173,
+ 'Sigma' => 931,
+ 'sigma' => 963,
+ 'sigmaf' => 962,
+ 'sim' => 8764,
+ 'spades' => 9824,
+ 'sub' => 8834,
+ 'sube' => 8838,
+ 'sum' => 8721,
+ 'sup' => 8835,
+ 'sup1' => 185,
+ 'sup2' => 178,
+ 'sup3' => 179,
+ 'supe' => 8839,
+ 'szlig' => 223,
+ 'Tau' => 932,
+ 'tau' => 964,
+ 'there4' => 8756,
+ 'Theta' => 920,
+ 'theta' => 952,
+ 'thetasym' => 977,
+ 'thinsp' => 8201,
+ 'THORN' => 222,
+ 'thorn' => 254,
+ 'tilde' => 732,
+ 'times' => 215,
+ 'trade' => 8482,
+ 'Uacute' => 218,
+ 'uacute' => 250,
+ 'uarr' => 8593,
+ 'uArr' => 8657,
+ 'Ucirc' => 219,
+ 'ucirc' => 251,
+ 'Ugrave' => 217,
+ 'ugrave' => 249,
+ 'uml' => 168,
+ 'upsih' => 978,
+ 'Upsilon' => 933,
+ 'upsilon' => 965,
+ 'Uuml' => 220,
+ 'uuml' => 252,
+ 'weierp' => 8472,
+ 'Xi' => 926,
+ 'xi' => 958,
+ 'Yacute' => 221,
+ 'yacute' => 253,
+ 'yen' => 165,
+ 'Yuml' => 376,
+ 'yuml' => 255,
+ 'Zeta' => 918,
+ 'zeta' => 950,
+ 'zwj' => 8205,
+ 'zwnj' => 8204 );
+
+/** @package MediaWiki */
+class Sanitizer {
+ /**
+ * Cleans up HTML, removes dangerous tags and attributes, and
+ * removes HTML comments
+ * @private
+ * @param string $text
+ * @param callback $processCallback to do any variable or parameter replacements in HTML attribute values
+ * @param array $args for the processing callback
+ * @return string
+ */
+ function removeHTMLtags( $text, $processCallback = null, $args = array() ) {
+ global $wgUseTidy, $wgUserHtml;
+ $fname = 'Parser::removeHTMLtags';
+ wfProfileIn( $fname );
+
+ if( $wgUserHtml ) {
+ $htmlpairs = array( # Tags that must be closed
+ 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
+ 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
+ 'strike', 'strong', 'tt', 'var', 'div', 'center',
+ 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
+ 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u'
+ );
+ $htmlsingle = array(
+ 'br', 'hr', 'li', 'dt', 'dd'
+ );
+ $htmlsingleonly = array( # Elements that cannot have close tags
+ 'br', 'hr'
+ );
+ $htmlnest = array( # Tags that can be nested--??
+ 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
+ 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span'
+ );
+ $tabletags = array( # Can only appear inside table
+ 'td', 'th', 'tr',
+ );
+ $htmllist = array( # Tags used by list
+ 'ul','ol',
+ );
+ $listtags = array( # Tags that can appear in a list
+ 'li',
+ );
+
+ } else {
+ $htmlpairs = array();
+ $htmlsingle = array();
+ $htmlnest = array();
+ $tabletags = array();
+ }
+
+ $htmlsingleallowed = array_merge( $htmlsingle, $tabletags );
+ $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest );
+
+ # Remove HTML comments
+ $text = Sanitizer::removeHTMLcomments( $text );
+ $bits = explode( '<', $text );
+ $text = array_shift( $bits );
+ if(!$wgUseTidy) {
+ $tagstack = array(); $tablestack = array();
+ foreach ( $bits as $x ) {
+ $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
+ preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/',
+ $x, $regs );
+ list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
+ error_reporting( $prev );
+
+ $badtag = 0 ;
+ if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
+ # Check our stack
+ if ( $slash ) {
+ # Closing a tag...
+ if( in_array( $t, $htmlsingleonly ) ) {
+ $badtag = 1;
+ } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) {
+ if ( in_array($ot, $htmlsingleallowed) ) {
+ # Pop all elements with an optional close tag
+ # and see if we find a match below them
+ $optstack = array();
+ array_push ($optstack, $ot);
+ while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) &&
+ in_array($ot, $htmlsingleallowed) ) {
+ array_push ($optstack, $ot);
+ }
+ if ( $t != $ot ) {
+ # No match. Push the optinal elements back again
+ $badtag = 1;
+ while ( $ot = @array_pop( $optstack ) ) {
+ array_push( $tagstack, $ot );
+ }
+ }
+ } else {
+ @array_push( $tagstack, $ot );
+ # <li> can be nested in <ul> or <ol>, skip those cases:
+ if(!(in_array($ot, $htmllist) && in_array($t, $listtags) )) {
+ $badtag = 1;
+ }
+ }
+ } else {
+ if ( $t == 'table' ) {
+ $tagstack = array_pop( $tablestack );
+ }
+ }
+ $newparams = '';
+ } else {
+ # Keep track for later
+ if ( in_array( $t, $tabletags ) &&
+ ! in_array( 'table', $tagstack ) ) {
+ $badtag = 1;
+ } else if ( in_array( $t, $tagstack ) &&
+ ! in_array ( $t , $htmlnest ) ) {
+ $badtag = 1 ;
+ # Is it a self closed htmlpair ? (bug 5487)
+ } else if( $brace == '/>' &&
+ in_array($t, $htmlpairs) ) {
+ $badtag = 1;
+ } elseif( in_array( $t, $htmlsingleonly ) ) {
+ # Hack to force empty tag for uncloseable elements
+ $brace = '/>';
+ } else if( in_array( $t, $htmlsingle ) ) {
+ # Hack to not close $htmlsingle tags
+ $brace = NULL;
+ } else {
+ if ( $t == 'table' ) {
+ array_push( $tablestack, $tagstack );
+ $tagstack = array();
+ }
+ array_push( $tagstack, $t );
+ }
+
+ # Replace any variables or template parameters with
+ # plaintext results.
+ if( is_callable( $processCallback ) ) {
+ call_user_func_array( $processCallback, array( &$params, $args ) );
+ }
+
+ # Strip non-approved attributes from the tag
+ $newparams = Sanitizer::fixTagAttributes( $params, $t );
+ }
+ if ( ! $badtag ) {
+ $rest = str_replace( '>', '&gt;', $rest );
+ $close = ( $brace == '/>' ) ? ' /' : '';
+ $text .= "<$slash$t$newparams$close>$rest";
+ continue;
+ }
+ }
+ $text .= '&lt;' . str_replace( '>', '&gt;', $x);
+ }
+ # Close off any remaining tags
+ while ( is_array( $tagstack ) && ($t = array_pop( $tagstack )) ) {
+ $text .= "</$t>\n";
+ if ( $t == 'table' ) { $tagstack = array_pop( $tablestack ); }
+ }
+ } else {
+ # this might be possible using tidy itself
+ foreach ( $bits as $x ) {
+ preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/',
+ $x, $regs );
+ @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
+ if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
+ if( is_callable( $processCallback ) ) {
+ call_user_func_array( $processCallback, array( &$params, $args ) );
+ }
+ $newparams = Sanitizer::fixTagAttributes( $params, $t );
+ $rest = str_replace( '>', '&gt;', $rest );
+ $text .= "<$slash$t$newparams$brace$rest";
+ } else {
+ $text .= '&lt;' . str_replace( '>', '&gt;', $x);
+ }
+ }
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Remove '<!--', '-->', and everything between.
+ * To avoid leaving blank lines, when a comment is both preceded
+ * and followed by a newline (ignoring spaces), trim leading and
+ * trailing spaces and one of the newlines.
+ *
+ * @private
+ * @param string $text
+ * @return string
+ */
+ function removeHTMLcomments( $text ) {
+ $fname='Parser::removeHTMLcomments';
+ wfProfileIn( $fname );
+ while (($start = strpos($text, '<!--')) !== false) {
+ $end = strpos($text, '-->', $start + 4);
+ if ($end === false) {
+ # Unterminated comment; bail out
+ break;
+ }
+
+ $end += 3;
+
+ # Trim space and newline if the comment is both
+ # preceded and followed by a newline
+ $spaceStart = max($start - 1, 0);
+ $spaceLen = $end - $spaceStart;
+ while (substr($text, $spaceStart, 1) === ' ' && $spaceStart > 0) {
+ $spaceStart--;
+ $spaceLen++;
+ }
+ while (substr($text, $spaceStart + $spaceLen, 1) === ' ')
+ $spaceLen++;
+ if (substr($text, $spaceStart, 1) === "\n" and substr($text, $spaceStart + $spaceLen, 1) === "\n") {
+ # Remove the comment, leading and trailing
+ # spaces, and leave only one newline.
+ $text = substr_replace($text, "\n", $spaceStart, $spaceLen + 1);
+ }
+ else {
+ # Remove just the comment.
+ $text = substr_replace($text, '', $start, $end - $start);
+ }
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * Take an array of attribute names and values and normalize or discard
+ * illegal values for the given element type.
+ *
+ * - Discards attributes not on a whitelist for the given element
+ * - Unsafe style attributes are discarded
+ *
+ * @param array $attribs
+ * @param string $element
+ * @return array
+ *
+ * @todo Check for legal values where the DTD limits things.
+ * @todo Check for unique id attribute :P
+ */
+ function validateTagAttributes( $attribs, $element ) {
+ $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) );
+ $out = array();
+ foreach( $attribs as $attribute => $value ) {
+ if( !isset( $whitelist[$attribute] ) ) {
+ continue;
+ }
+ # Strip javascript "expression" from stylesheets.
+ # http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
+ if( $attribute == 'style' ) {
+ $value = Sanitizer::checkCss( $value );
+ if( $value === false ) {
+ # haxx0r
+ continue;
+ }
+ }
+
+ if ( $attribute === 'id' )
+ $value = Sanitizer::escapeId( $value );
+
+ // If this attribute was previously set, override it.
+ // Output should only have one attribute of each name.
+ $out[$attribute] = $value;
+ }
+ return $out;
+ }
+
+ /**
+ * Pick apart some CSS and check it for forbidden or unsafe structures.
+ * Returns a sanitized string, or false if it was just too evil.
+ *
+ * Currently URL references, 'expression', 'tps' are forbidden.
+ *
+ * @param string $value
+ * @return mixed
+ */
+ static function checkCss( $value ) {
+ $stripped = Sanitizer::decodeCharReferences( $value );
+
+ // Remove any comments; IE gets token splitting wrong
+ $stripped = preg_replace( '!/\\*.*?\\*/!S', ' ', $stripped );
+ $value = $stripped;
+
+ // ... and continue checks
+ $stripped = preg_replace( '!\\\\([0-9A-Fa-f]{1,6})[ \\n\\r\\t\\f]?!e',
+ 'codepointToUtf8(hexdec("$1"))', $stripped );
+ $stripped = str_replace( '\\', '', $stripped );
+ if( preg_match( '/(expression|tps*:\/\/|url\\s*\().*/is',
+ $stripped ) ) {
+ # haxx0r
+ return false;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Take a tag soup fragment listing an HTML element's attributes
+ * and normalize it to well-formed XML, discarding unwanted attributes.
+ * Output is safe for further wikitext processing, with escaping of
+ * values that could trigger problems.
+ *
+ * - Normalizes attribute names to lowercase
+ * - Discards attributes not on a whitelist for the given element
+ * - Turns broken or invalid entities into plaintext
+ * - Double-quotes all attribute values
+ * - Attributes without values are given the name as attribute
+ * - Double attributes are discarded
+ * - Unsafe style attributes are discarded
+ * - Prepends space if there are attributes.
+ *
+ * @param string $text
+ * @param string $element
+ * @return string
+ */
+ function fixTagAttributes( $text, $element ) {
+ if( trim( $text ) == '' ) {
+ return '';
+ }
+
+ $stripped = Sanitizer::validateTagAttributes(
+ Sanitizer::decodeTagAttributes( $text ), $element );
+
+ $attribs = array();
+ foreach( $stripped as $attribute => $value ) {
+ $encAttribute = htmlspecialchars( $attribute );
+ $encValue = Sanitizer::safeEncodeAttribute( $value );
+
+ $attribs[] = "$encAttribute=\"$encValue\"";
+ }
+ return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
+ }
+
+ /**
+ * Encode an attribute value for HTML output.
+ * @param $text
+ * @return HTML-encoded text fragment
+ */
+ function encodeAttribute( $text ) {
+ $encValue = htmlspecialchars( $text );
+
+ // Whitespace is normalized during attribute decoding,
+ // so if we've been passed non-spaces we must encode them
+ // ahead of time or they won't be preserved.
+ $encValue = strtr( $encValue, array(
+ "\n" => '&#10;',
+ "\r" => '&#13;',
+ "\t" => '&#9;',
+ ) );
+
+ return $encValue;
+ }
+
+ /**
+ * Encode an attribute value for HTML tags, with extra armoring
+ * against further wiki processing.
+ * @param $text
+ * @return HTML-encoded text fragment
+ */
+ function safeEncodeAttribute( $text ) {
+ $encValue = Sanitizer::encodeAttribute( $text );
+
+ # Templates and links may be expanded in later parsing,
+ # creating invalid or dangerous output. Suppress this.
+ $encValue = strtr( $encValue, array(
+ '<' => '&lt;', // This should never happen,
+ '>' => '&gt;', // we've received invalid input
+ '"' => '&quot;', // which should have been escaped.
+ '{' => '&#123;',
+ '[' => '&#91;',
+ "''" => '&#39;&#39;',
+ 'ISBN' => '&#73;SBN',
+ 'RFC' => '&#82;FC',
+ 'PMID' => '&#80;MID',
+ '|' => '&#124;',
+ '__' => '&#95;_',
+ ) );
+
+ # Stupid hack
+ $encValue = preg_replace_callback(
+ '/(' . wfUrlProtocols() . ')/',
+ array( 'Sanitizer', 'armorLinksCallback' ),
+ $encValue );
+ return $encValue;
+ }
+
+ /**
+ * Given a value escape it so that it can be used in an id attribute and
+ * return it, this does not validate the value however (see first link)
+ *
+ * @link http://www.w3.org/TR/html401/types.html#type-name Valid characters
+ * in the id and
+ * name attributes
+ * @link http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
+ *
+ * @bug 4461
+ *
+ * @static
+ *
+ * @param string $id
+ * @return string
+ */
+ function escapeId( $id ) {
+ static $replace = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+
+ $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
+
+ return str_replace( array_keys( $replace ), array_values( $replace ), $id );
+ }
+
+ /**
+ * Regex replace callback for armoring links against further processing.
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function armorLinksCallback( $matches ) {
+ return str_replace( ':', '&#58;', $matches[1] );
+ }
+
+ /**
+ * Return an associative array of attribute names and values from
+ * a partial tag string. Attribute names are forces to lowercase,
+ * character references are decoded to UTF-8 text.
+ *
+ * @param string
+ * @return array
+ */
+ function decodeTagAttributes( $text ) {
+ $attribs = array();
+
+ if( trim( $text ) == '' ) {
+ return $attribs;
+ }
+
+ $pairs = array();
+ if( !preg_match_all(
+ MW_ATTRIBS_REGEX,
+ $text,
+ $pairs,
+ PREG_SET_ORDER ) ) {
+ return $attribs;
+ }
+
+ foreach( $pairs as $set ) {
+ $attribute = strtolower( $set[1] );
+ $value = Sanitizer::getTagAttributeCallback( $set );
+
+ // Normalize whitespace
+ $value = preg_replace( '/[\t\r\n ]+/', ' ', $value );
+ $value = trim( $value );
+
+ // Decode character references
+ $attribs[$attribute] = Sanitizer::decodeCharReferences( $value );
+ }
+ return $attribs;
+ }
+
+ /**
+ * Pick the appropriate attribute value from a match set from the
+ * MW_ATTRIBS_REGEX matches.
+ *
+ * @param array $set
+ * @return string
+ * @private
+ */
+ function getTagAttributeCallback( $set ) {
+ if( isset( $set[6] ) ) {
+ # Illegal #XXXXXX color with no quotes.
+ return $set[6];
+ } elseif( isset( $set[5] ) ) {
+ # No quotes.
+ return $set[5];
+ } elseif( isset( $set[4] ) ) {
+ # Single-quoted
+ return $set[4];
+ } elseif( isset( $set[3] ) ) {
+ # Double-quoted
+ return $set[3];
+ } elseif( !isset( $set[2] ) ) {
+ # In XHTML, attributes must have a value.
+ # For 'reduced' form, return explicitly the attribute name here.
+ return $set[1];
+ } else {
+ throw new MWException( "Tag conditions not met. This should never happen and is a bug." );
+ }
+ }
+
+ /**
+ * Normalize whitespace and character references in an XML source-
+ * encoded text for an attribute value.
+ *
+ * See http://www.w3.org/TR/REC-xml/#AVNormalize for background,
+ * but note that we're not returning the value, but are returning
+ * XML source fragments that will be slapped into output.
+ *
+ * @param string $text
+ * @return string
+ * @private
+ */
+ function normalizeAttributeValue( $text ) {
+ return str_replace( '"', '&quot;',
+ preg_replace(
+ '/\r\n|[\x20\x0d\x0a\x09]/',
+ ' ',
+ Sanitizer::normalizeCharReferences( $text ) ) );
+ }
+
+ /**
+ * Ensure that any entities and character references are legal
+ * for XML and XHTML specifically. Any stray bits will be
+ * &amp;-escaped to result in a valid text fragment.
+ *
+ * a. any named char refs must be known in XHTML
+ * b. any numeric char refs must be legal chars, not invalid or forbidden
+ * c. use &#x, not &#X
+ * d. fix or reject non-valid attributes
+ *
+ * @param string $text
+ * @return string
+ * @private
+ */
+ function normalizeCharReferences( $text ) {
+ return preg_replace_callback(
+ MW_CHAR_REFS_REGEX,
+ array( 'Sanitizer', 'normalizeCharReferencesCallback' ),
+ $text );
+ }
+ /**
+ * @param string $matches
+ * @return string
+ */
+ function normalizeCharReferencesCallback( $matches ) {
+ $ret = null;
+ if( $matches[1] != '' ) {
+ $ret = Sanitizer::normalizeEntity( $matches[1] );
+ } elseif( $matches[2] != '' ) {
+ $ret = Sanitizer::decCharReference( $matches[2] );
+ } elseif( $matches[3] != '' ) {
+ $ret = Sanitizer::hexCharReference( $matches[3] );
+ } elseif( $matches[4] != '' ) {
+ $ret = Sanitizer::hexCharReference( $matches[4] );
+ }
+ if( is_null( $ret ) ) {
+ return htmlspecialchars( $matches[0] );
+ } else {
+ return $ret;
+ }
+ }
+
+ /**
+ * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
+ * return the named entity reference as is. Otherwise, returns
+ * HTML-escaped text of pseudo-entity source (eg &amp;foo;)
+ *
+ * @param string $name
+ * @return string
+ */
+ function normalizeEntity( $name ) {
+ global $wgHtmlEntities;
+ if( isset( $wgHtmlEntities[$name] ) ) {
+ return "&$name;";
+ } else {
+ return "&amp;$name;";
+ }
+ }
+
+ function decCharReference( $codepoint ) {
+ $point = intval( $codepoint );
+ if( Sanitizer::validateCodepoint( $point ) ) {
+ return sprintf( '&#%d;', $point );
+ } else {
+ return null;
+ }
+ }
+
+ function hexCharReference( $codepoint ) {
+ $point = hexdec( $codepoint );
+ if( Sanitizer::validateCodepoint( $point ) ) {
+ return sprintf( '&#x%x;', $point );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if a given Unicode codepoint is a valid character in XML.
+ * @param int $codepoint
+ * @return bool
+ */
+ function validateCodepoint( $codepoint ) {
+ return ($codepoint == 0x09)
+ || ($codepoint == 0x0a)
+ || ($codepoint == 0x0d)
+ || ($codepoint >= 0x20 && $codepoint <= 0xd7ff)
+ || ($codepoint >= 0xe000 && $codepoint <= 0xfffd)
+ || ($codepoint >= 0x10000 && $codepoint <= 0x10ffff);
+ }
+
+ /**
+ * Decode any character references, numeric or named entities,
+ * in the text and return a UTF-8 string.
+ *
+ * @param string $text
+ * @return string
+ * @public
+ */
+ function decodeCharReferences( $text ) {
+ return preg_replace_callback(
+ MW_CHAR_REFS_REGEX,
+ array( 'Sanitizer', 'decodeCharReferencesCallback' ),
+ $text );
+ }
+
+ /**
+ * @param string $matches
+ * @return string
+ */
+ function decodeCharReferencesCallback( $matches ) {
+ if( $matches[1] != '' ) {
+ return Sanitizer::decodeEntity( $matches[1] );
+ } elseif( $matches[2] != '' ) {
+ return Sanitizer::decodeChar( intval( $matches[2] ) );
+ } elseif( $matches[3] != '' ) {
+ return Sanitizer::decodeChar( hexdec( $matches[3] ) );
+ } elseif( $matches[4] != '' ) {
+ return Sanitizer::decodeChar( hexdec( $matches[4] ) );
+ }
+ # Last case should be an ampersand by itself
+ return $matches[0];
+ }
+
+ /**
+ * Return UTF-8 string for a codepoint if that is a valid
+ * character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
+ * @param int $codepoint
+ * @return string
+ * @private
+ */
+ function decodeChar( $codepoint ) {
+ if( Sanitizer::validateCodepoint( $codepoint ) ) {
+ return codepointToUtf8( $codepoint );
+ } else {
+ return UTF8_REPLACEMENT;
+ }
+ }
+
+ /**
+ * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
+ * return the UTF-8 encoding of that character. Otherwise, returns
+ * pseudo-entity source (eg &foo;)
+ *
+ * @param string $name
+ * @return string
+ */
+ function decodeEntity( $name ) {
+ global $wgHtmlEntities;
+ if( isset( $wgHtmlEntities[$name] ) ) {
+ return codepointToUtf8( $wgHtmlEntities[$name] );
+ } else {
+ return "&$name;";
+ }
+ }
+
+ /**
+ * Fetch the whitelist of acceptable attributes for a given
+ * element name.
+ *
+ * @param string $element
+ * @return array
+ */
+ function attributeWhitelist( $element ) {
+ static $list;
+ if( !isset( $list ) ) {
+ $list = Sanitizer::setupAttributeWhitelist();
+ }
+ return isset( $list[$element] )
+ ? $list[$element]
+ : array();
+ }
+
+ /**
+ * @return array
+ */
+ function setupAttributeWhitelist() {
+ $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
+ $block = array_merge( $common, array( 'align' ) );
+ $tablealign = array( 'align', 'char', 'charoff', 'valign' );
+ $tablecell = array( 'abbr',
+ 'axis',
+ 'headers',
+ 'scope',
+ 'rowspan',
+ 'colspan',
+ 'nowrap', # deprecated
+ 'width', # deprecated
+ 'height', # deprecated
+ 'bgcolor' # deprecated
+ );
+
+ # Numbers refer to sections in HTML 4.01 standard describing the element.
+ # See: http://www.w3.org/TR/html4/
+ $whitelist = array (
+ # 7.5.4
+ 'div' => $block,
+ 'center' => $common, # deprecated
+ 'span' => $block, # ??
+
+ # 7.5.5
+ 'h1' => $block,
+ 'h2' => $block,
+ 'h3' => $block,
+ 'h4' => $block,
+ 'h5' => $block,
+ 'h6' => $block,
+
+ # 7.5.6
+ # address
+
+ # 8.2.4
+ # bdo
+
+ # 9.2.1
+ 'em' => $common,
+ 'strong' => $common,
+ 'cite' => $common,
+ # dfn
+ 'code' => $common,
+ # samp
+ # kbd
+ 'var' => $common,
+ # abbr
+ # acronym
+
+ # 9.2.2
+ 'blockquote' => array_merge( $common, array( 'cite' ) ),
+ # q
+
+ # 9.2.3
+ 'sub' => $common,
+ 'sup' => $common,
+
+ # 9.3.1
+ 'p' => $block,
+
+ # 9.3.2
+ 'br' => array( 'id', 'class', 'title', 'style', 'clear' ),
+
+ # 9.3.4
+ 'pre' => array_merge( $common, array( 'width' ) ),
+
+ # 9.4
+ 'ins' => array_merge( $common, array( 'cite', 'datetime' ) ),
+ 'del' => array_merge( $common, array( 'cite', 'datetime' ) ),
+
+ # 10.2
+ 'ul' => array_merge( $common, array( 'type' ) ),
+ 'ol' => array_merge( $common, array( 'type', 'start' ) ),
+ 'li' => array_merge( $common, array( 'type', 'value' ) ),
+
+ # 10.3
+ 'dl' => $common,
+ 'dd' => $common,
+ 'dt' => $common,
+
+ # 11.2.1
+ 'table' => array_merge( $common,
+ array( 'summary', 'width', 'border', 'frame',
+ 'rules', 'cellspacing', 'cellpadding',
+ 'align', 'bgcolor', 'frame', 'rules',
+ 'border' ) ),
+
+ # 11.2.2
+ 'caption' => array_merge( $common, array( 'align' ) ),
+
+ # 11.2.3
+ 'thead' => array_merge( $common, $tablealign ),
+ 'tfoot' => array_merge( $common, $tablealign ),
+ 'tbody' => array_merge( $common, $tablealign ),
+
+ # 11.2.4
+ 'colgroup' => array_merge( $common, array( 'span', 'width' ), $tablealign ),
+ 'col' => array_merge( $common, array( 'span', 'width' ), $tablealign ),
+
+ # 11.2.5
+ 'tr' => array_merge( $common, array( 'bgcolor' ), $tablealign ),
+
+ # 11.2.6
+ 'td' => array_merge( $common, $tablecell, $tablealign ),
+ 'th' => array_merge( $common, $tablecell, $tablealign ),
+
+ # 15.2.1
+ 'tt' => $common,
+ 'b' => $common,
+ 'i' => $common,
+ 'big' => $common,
+ 'small' => $common,
+ 'strike' => $common,
+ 's' => $common,
+ 'u' => $common,
+
+ # 15.2.2
+ 'font' => array_merge( $common, array( 'size', 'color', 'face' ) ),
+ # basefont
+
+ # 15.3
+ 'hr' => array_merge( $common, array( 'noshade', 'size', 'width' ) ),
+
+ # XHTML Ruby annotation text module, simple ruby only.
+ # http://www.w3c.org/TR/ruby/
+ 'ruby' => $common,
+ # rbc
+ # rtc
+ 'rb' => $common,
+ 'rt' => $common, #array_merge( $common, array( 'rbspan' ) ),
+ 'rp' => $common,
+ );
+ return $whitelist;
+ }
+
+ /**
+ * Take a fragment of (potentially invalid) HTML and return
+ * a version with any tags removed, encoded suitably for literal
+ * inclusion in an attribute value.
+ *
+ * @param string $text HTML fragment
+ * @return string
+ */
+ function stripAllTags( $text ) {
+ # Actual <tags>
+ $text = preg_replace( '/ < .*? > /x', '', $text );
+
+ # Normalize &entities and whitespace
+ $text = Sanitizer::normalizeAttributeValue( $text );
+
+ # Will be placed into "double-quoted" attributes,
+ # make sure remaining bits are safe.
+ $text = str_replace(
+ array('<', '>', '"'),
+ array('&lt;', '&gt;', '&quot;'),
+ $text );
+
+ return $text;
+ }
+
+ /**
+ * Hack up a private DOCTYPE with HTML's standard entity declarations.
+ * PHP 4 seemed to know these if you gave it an HTML doctype, but
+ * PHP 5.1 doesn't.
+ *
+ * Use for passing XHTML fragments to PHP's XML parsing functions
+ *
+ * @return string
+ * @static
+ */
+ function hackDocType() {
+ global $wgHtmlEntities;
+ $out = "<!DOCTYPE html [\n";
+ foreach( $wgHtmlEntities as $entity => $codepoint ) {
+ $out .= "<!ENTITY $entity \"&#$codepoint;\">";
+ }
+ $out .= "]>\n";
+ return $out;
+ }
+
+}
+
+?>
diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php
new file mode 100644
index 00000000..c3b38519
--- /dev/null
+++ b/includes/SearchEngine.php
@@ -0,0 +1,345 @@
+<?php
+/**
+ * Contain a class for special pages
+ * @package MediaWiki
+ * @subpackage Search
+ */
+
+/**
+ * @package MediaWiki
+ */
+class SearchEngine {
+ var $limit = 10;
+ var $offset = 0;
+ var $searchTerms = array();
+ var $namespaces = array( NS_MAIN );
+ var $showRedirects = false;
+
+ /**
+ * Perform a full text search query and return a result set.
+ * If title searches are not supported or disabled, return null.
+ *
+ * @param string $term - Raw search term
+ * @return SearchResultSet
+ * @access public
+ * @abstract
+ */
+ function searchText( $term ) {
+ return null;
+ }
+
+ /**
+ * Perform a title-only search query and return a result set.
+ * If title searches are not supported or disabled, return null.
+ *
+ * @param string $term - Raw search term
+ * @return SearchResultSet
+ * @access public
+ * @abstract
+ */
+ function searchTitle( $term ) {
+ return null;
+ }
+
+ /**
+ * If an exact title match can be find, or a very slightly close match,
+ * return the title. If no match, returns NULL.
+ *
+ * @static
+ * @param string $term
+ * @return Title
+ * @private
+ */
+ function getNearMatch( $term ) {
+ # Exact match? No need to look further.
+ $title = Title::newFromText( $term );
+ if (is_null($title))
+ return NULL;
+
+ if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) {
+ return $title;
+ }
+
+ # Now try all lower case (i.e. first letter capitalized)
+ #
+ $title = Title::newFromText( strtolower( $term ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+
+ # Now try capitalized string
+ #
+ $title = Title::newFromText( ucwords( strtolower( $term ) ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+
+ # Now try all upper case
+ #
+ $title = Title::newFromText( strtoupper( $term ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+
+ # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc
+ $title = Title::newFromText( preg_replace_callback(
+ '/\b([\w\x80-\xff]+)\b/',
+ create_function( '$matches', '
+ global $wgContLang;
+ return $wgContLang->ucfirst($matches[1]);
+ ' ),
+ $term ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+
+ global $wgCapitalLinks, $wgContLang;
+ if( !$wgCapitalLinks ) {
+ // Catch differs-by-first-letter-case-only
+ $title = Title::newFromText( $wgContLang->ucfirst( $term ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+ $title = Title::newFromText( $wgContLang->lcfirst( $term ) );
+ if ( $title->exists() ) {
+ return $title;
+ }
+ }
+
+ $title = Title::newFromText( $term );
+
+ # Entering an IP address goes to the contributions page
+ if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) )
+ || User::isIP( trim( $term ) ) ) {
+ return Title::makeTitle( NS_SPECIAL, "Contributions/" . $title->getDbkey() );
+ }
+
+
+ # Entering a user goes to the user page whether it's there or not
+ if ( $title->getNamespace() == NS_USER ) {
+ return $title;
+ }
+
+ # Quoted term? Try without the quotes...
+ if( preg_match( '/^"([^"]+)"$/', $term, $matches ) ) {
+ return SearchEngine::getNearMatch( $matches[1] );
+ }
+
+ return NULL;
+ }
+
+ function legalSearchChars() {
+ return "A-Za-z_'0-9\\x80-\\xFF\\-";
+ }
+
+ /**
+ * Set the maximum number of results to return
+ * and how many to skip before returning the first.
+ *
+ * @param int $limit
+ * @param int $offset
+ * @access public
+ */
+ function setLimitOffset( $limit, $offset = 0 ) {
+ $this->limit = intval( $limit );
+ $this->offset = intval( $offset );
+ }
+
+ /**
+ * Set which namespaces the search should include.
+ * Give an array of namespace index numbers.
+ *
+ * @param array $namespaces
+ * @access public
+ */
+ function setNamespaces( $namespaces ) {
+ $this->namespaces = $namespaces;
+ }
+
+ /**
+ * Make a list of searchable namespaces and their canonical names.
+ * @return array
+ * @access public
+ */
+ function searchableNamespaces() {
+ global $wgContLang;
+ $arr = array();
+ foreach( $wgContLang->getNamespaces() as $ns => $name ) {
+ if( $ns >= NS_MAIN ) {
+ $arr[$ns] = $name;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Return a 'cleaned up' search string
+ *
+ * @return string
+ * @access public
+ */
+ function filter( $text ) {
+ $lc = $this->legalSearchChars();
+ return trim( preg_replace( "/[^{$lc}]/", " ", $text ) );
+ }
+ /**
+ * Load up the appropriate search engine class for the currently
+ * active database backend, and return a configured instance.
+ *
+ * @return SearchEngine
+ * @private
+ */
+ function create() {
+ global $wgDBtype, $wgSearchType;
+ if( $wgSearchType ) {
+ $class = $wgSearchType;
+ } elseif( $wgDBtype == 'mysql' ) {
+ $class = 'SearchMySQL4';
+ } else if ( $wgDBtype == 'postgres' ) {
+ $class = 'SearchPostgres';
+ } else {
+ $class = 'SearchEngineDummy';
+ }
+ $search = new $class( wfGetDB( DB_SLAVE ) );
+ $search->setLimitOffset(0,0);
+ return $search;
+ }
+
+ /**
+ * Create or update the search index record for the given page.
+ * Title and text should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ * @param string $text
+ * @abstract
+ */
+ function update( $id, $title, $text ) {
+ // no-op
+ }
+
+ /**
+ * Update a search index record's title only.
+ * Title should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ * @abstract
+ */
+ function updateTitle( $id, $title ) {
+ // no-op
+ }
+}
+
+/** @package MediaWiki */
+class SearchResultSet {
+ /**
+ * Fetch an array of regular expression fragments for matching
+ * the search terms as parsed by this engine in a text extract.
+ *
+ * @return array
+ * @access public
+ * @abstract
+ */
+ function termMatches() {
+ return array();
+ }
+
+ function numRows() {
+ return 0;
+ }
+
+ /**
+ * Return true if results are included in this result set.
+ * @return bool
+ * @abstract
+ */
+ function hasResults() {
+ return false;
+ }
+
+ /**
+ * Some search modes return a total hit count for the query
+ * in the entire article database. This may include pages
+ * in namespaces that would not be matched on the given
+ * settings.
+ *
+ * Return null if no total hits number is supported.
+ *
+ * @return int
+ * @access public
+ */
+ function getTotalHits() {
+ return null;
+ }
+
+ /**
+ * Some search modes return a suggested alternate term if there are
+ * no exact hits. Returns true if there is one on this set.
+ *
+ * @return bool
+ * @access public
+ */
+ function hasSuggestion() {
+ return false;
+ }
+
+ /**
+ * Some search modes return a suggested alternate term if there are
+ * no exact hits. Check hasSuggestion() first.
+ *
+ * @return string
+ * @access public
+ */
+ function getSuggestion() {
+ return '';
+ }
+
+ /**
+ * Fetches next search result, or false.
+ * @return SearchResult
+ * @access public
+ * @abstract
+ */
+ function next() {
+ return false;
+ }
+}
+
+/** @package MediaWiki */
+class SearchResult {
+ function SearchResult( $row ) {
+ $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+ }
+
+ /**
+ * @return Title
+ * @access public
+ */
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ /**
+ * @return double or null if not supported
+ */
+ function getScore() {
+ return null;
+ }
+}
+
+/**
+ * @package MediaWiki
+ */
+class SearchEngineDummy {
+ function search( $term ) {
+ return null;
+ }
+ function setLimitOffset($l, $o) {}
+ function legalSearchChars() {}
+ function update() {}
+ function setnamespaces() {}
+ function searchtitle() {}
+ function searchtext() {}
+}
+?>
diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php
new file mode 100644
index 00000000..15515952
--- /dev/null
+++ b/includes/SearchMySQL.php
@@ -0,0 +1,206 @@
+<?php
+# 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
+
+/**
+ * Search engine hook base class for MySQL.
+ * Specific bits for MySQL 3 and 4 variants are in child classes.
+ * @package MediaWiki
+ * @subpackage Search
+ */
+
+/** @package MediaWiki */
+class SearchMySQL extends SearchEngine {
+ /**
+ * Perform a full text search query and return a result set.
+ *
+ * @param string $term - Raw search term
+ * @return MySQLSearchResultSet
+ * @access public
+ */
+ function searchText( $term ) {
+ $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), true ) ) );
+ return new MySQLSearchResultSet( $resultSet, $this->searchTerms );
+ }
+
+ /**
+ * Perform a title-only search query and return a result set.
+ *
+ * @param string $term - Raw search term
+ * @return MySQLSearchResultSet
+ * @access public
+ */
+ function searchTitle( $term ) {
+ $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) );
+ return new MySQLSearchResultSet( $resultSet, $this->searchTerms );
+ }
+
+
+ /**
+ * Return a partial WHERE clause to exclude redirects, if so set
+ * @return string
+ * @private
+ */
+ function queryRedirect() {
+ if( $this->showRedirects ) {
+ return '';
+ } else {
+ return 'AND page_is_redirect=0';
+ }
+ }
+
+ /**
+ * Return a partial WHERE clause to limit the search to the given namespaces
+ * @return string
+ * @private
+ */
+ function queryNamespaces() {
+ $namespaces = implode( ',', $this->namespaces );
+ if ($namespaces == '') {
+ $namespaces = '0';
+ }
+ return 'AND page_namespace IN (' . $namespaces . ')';
+ }
+
+ /**
+ * Return a LIMIT clause to limit results on the query.
+ * @return string
+ * @private
+ */
+ function queryLimit() {
+ return $this->db->limitResult( '', $this->limit, $this->offset );
+ }
+
+ /**
+ * Does not do anything for generic search engine
+ * subclasses may define this though
+ * @return string
+ * @private
+ */
+ function queryRanking( $filteredTerm, $fulltext ) {
+ return '';
+ }
+
+ /**
+ * Construct the full SQL query to do the search.
+ * The guts shoulds be constructed in queryMain()
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @private
+ */
+ function getQuery( $filteredTerm, $fulltext ) {
+ return $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
+ $this->queryRedirect() . ' ' .
+ $this->queryNamespaces() . ' ' .
+ $this->queryRanking( $filteredTerm, $fulltext ) . ' ' .
+ $this->queryLimit();
+ }
+
+
+ /**
+ * Picks which field to index on, depending on what type of query.
+ * @param bool $fulltext
+ * @return string
+ */
+ function getIndexField( $fulltext ) {
+ return $fulltext ? 'si_text' : 'si_title';
+ }
+
+ /**
+ * Get the base part of the search query.
+ * The actual match syntax will depend on the server
+ * version; MySQL 3 and MySQL 4 have different capabilities
+ * in their fulltext search indexes.
+ *
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
+ * @private
+ */
+ function queryMain( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $page = $this->db->tableName( 'page' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return 'SELECT page_id, page_namespace, page_title ' .
+ "FROM $page,$searchindex " .
+ 'WHERE page_id=si_page AND ' . $match;
+ }
+
+ /**
+ * Create or update the search index record for the given page.
+ * Title and text should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ * @param string $text
+ */
+ function update( $id, $title, $text ) {
+ $dbw=& wfGetDB( DB_MASTER );
+ $dbw->replace( 'searchindex',
+ array( 'si_page' ),
+ array(
+ 'si_page' => $id,
+ 'si_title' => $title,
+ 'si_text' => $text
+ ), 'SearchMySQL4::update' );
+ }
+
+ /**
+ * Update a search index record's title only.
+ * Title should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ */
+ function updateTitle( $id, $title ) {
+ $dbw =& wfGetDB( DB_MASTER );
+
+ $dbw->update( 'searchindex',
+ array( 'si_title' => $title ),
+ array( 'si_page' => $id ),
+ 'SearchMySQL4::updateTitle',
+ array( $dbw->lowPriorityOption() ) );
+ }
+}
+
+/** @package MediaWiki */
+class MySQLSearchResultSet extends SearchResultSet {
+ function MySQLSearchResultSet( $resultSet, $terms ) {
+ $this->mResultSet = $resultSet;
+ $this->mTerms = $terms;
+ }
+
+ function termMatches() {
+ return $this->mTerms;
+ }
+
+ function numRows() {
+ return $this->mResultSet->numRows();
+ }
+
+ function next() {
+ $row = $this->mResultSet->fetchObject();
+ if( $row === false ) {
+ return false;
+ } else {
+ return new SearchResult( $row );
+ }
+ }
+}
+
+?>
diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php
new file mode 100644
index 00000000..dcc1f685
--- /dev/null
+++ b/includes/SearchMySQL4.php
@@ -0,0 +1,73 @@
+<?php
+# 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
+
+/**
+ * Search engine hook for MySQL 4+
+ * @package MediaWiki
+ * @subpackage Search
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage Search
+ */
+class SearchMySQL4 extends SearchMySQL {
+ var $strictMatching = true;
+
+ /** @todo document */
+ function SearchMySQL4( &$db ) {
+ $this->db =& $db;
+ }
+
+ /** @todo document */
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars();
+ $searchon = '';
+ $this->searchTerms = array();
+
+ # FIXME: This doesn't handle parenthetical expressions.
+ if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach( $m as $terms ) {
+ if( $searchon !== '' ) $searchon .= ' ';
+ if( $this->strictMatching && ($terms[1] == '') ) {
+ $terms[1] = '+';
+ }
+ $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
+ if( !empty( $terms[3] ) ) {
+ $regexp = preg_quote( $terms[3], '/' );
+ if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
+ } else {
+ $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
+ }
+ $this->searchTerms[] = $regexp;
+ }
+ wfDebug( "Would search with '$searchon'\n" );
+ wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" );
+ } else {
+ wfDebug( "Can't understand search query '{$filteredText}'\n" );
+ }
+
+ $searchon = $this->db->strencode( $searchon );
+ $field = $this->getIndexField( $fulltext );
+ return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
+ }
+}
+?>
diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php
new file mode 100644
index 00000000..8e36b0b5
--- /dev/null
+++ b/includes/SearchPostgres.php
@@ -0,0 +1,156 @@
+<?php
+# Copyright (C) 2006 Greg Sabino Mullane <greg@turnstep.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
+
+## XXX Better catching of SELECT to_tsquery('the')
+
+/**
+ * Search engine hook base class for Postgres
+ * @package MediaWiki
+ * @subpackage Search
+ */
+
+/** @package MediaWiki */
+class SearchPostgres extends SearchEngine {
+
+ function SearchPostgres( &$db ) {
+ $this->db =& $db;
+ }
+
+ /**
+ * Perform a full text search query via tsearch2 and return a result set.
+ * Currently searches a page's current title (p.page_title) and text (t.old_text)
+ *
+ * @param string $term - Raw search term
+ * @return PostgresSearchResultSet
+ * @access public
+ */
+ function searchText( $term ) {
+ $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term, 'textvector' ) ) );
+ return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
+ }
+ function searchTitle( $term ) {
+ $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term , 'titlevector' ) ) );
+ return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
+ }
+
+
+ /*
+ * Transform the user's search string into a better form for tsearch2
+ */
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars();
+ $searchon = '';
+ $this->searchTerms = array();
+
+ # FIXME: This doesn't handle parenthetical expressions.
+ if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach( $m as $terms ) {
+ if( $searchon !== '' ) $searchon .= ' ';
+ if($terms[1] == '') {
+ $terms[1] = '+';
+ }
+ $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
+ if( !empty( $terms[3] ) ) {
+ $regexp = preg_quote( $terms[3], '/' );
+ if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
+ } else {
+ $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
+ }
+ $this->searchTerms[] = $regexp;
+ }
+ wfDebug( "Would search with '$searchon'\n" );
+ wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" );
+ } else {
+ wfDebug( "Can't understand search query '{$this->filteredText}'\n" );
+ }
+
+ $searchon = preg_replace('/(\s+)/','&',$searchon);
+ $searchon = $this->db->strencode( $searchon );
+ return $searchon;
+ }
+
+ /**
+ * Construct the full SQL query to do the search.
+ * @param string $filteredTerm
+ * @param string $fulltext
+ * @private
+ */
+ function searchQuery( $filteredTerm, $fulltext ) {
+
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+
+ $query = "SELECT page_id, page_namespace, page_title, old_text AS page_text ".
+ "FROM page p, revision r, text t WHERE p.page_latest = r.rev_id " .
+ "AND r.rev_text_id = t.old_id AND $fulltext @@ to_tsquery('$match')";
+
+ ## Redirects
+ if (! $this->showRedirects)
+ $query .= ' AND page_is_redirect = 0'; ## IS FALSE
+
+ ## Namespaces - defaults to 0
+ if ( count($this->namespaces) < 1)
+ $query .= ' AND page_namespace = 0';
+ else {
+ $namespaces = implode( ',', $this->namespaces );
+ $query .= " AND page_namespace IN ($namespaces)";
+ }
+
+ $query .= " ORDER BY rank($fulltext, to_tsquery('$fulltext')) DESC";
+
+ $query .= $this->db->limitResult( '', $this->limit, $this->offset );
+
+ return $query;
+ }
+
+ ## These two functions are done automatically via triggers
+
+ function update( $id, $title, $text ) { return true; }
+ function updateTitle( $id, $title ) { return true; }
+
+} ## end of the SearchPostgres class
+
+
+/** @package MediaWiki */
+class PostgresSearchResultSet extends SearchResultSet {
+ function PostgresSearchResultSet( $resultSet, $terms ) {
+ $this->mResultSet = $resultSet;
+ $this->mTerms = $terms;
+ }
+
+ function termMatches() {
+ return $this->mTerms;
+ }
+
+ function numRows() {
+ return $this->mResultSet->numRows();
+ }
+
+ function next() {
+ $row = $this->mResultSet->fetchObject();
+ if( $row === false ) {
+ return false;
+ } else {
+ return new SearchResult( $row );
+ }
+ }
+}
+
+?>
diff --git a/includes/SearchTsearch2.php b/includes/SearchTsearch2.php
new file mode 100644
index 00000000..a8f354b3
--- /dev/null
+++ b/includes/SearchTsearch2.php
@@ -0,0 +1,123 @@
+<?php
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>, Domas Mituzas <domas.mituzas@gmail.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
+
+/**
+ * Search engine hook for PostgreSQL / Tsearch2
+ * @package MediaWiki
+ * @subpackage Search
+ */
+
+/**
+ * @todo document
+ * @package MediaWiki
+ * @subpackage Search
+ */
+class SearchTsearch2 extends SearchEngine {
+ var $strictMatching = false;
+
+ function SearchTsearch2( &$db ) {
+ $this->db =& $db;
+ $this->mRanking = true;
+ }
+
+ function getIndexField( $fulltext ) {
+ return $fulltext ? 'si_text' : 'si_title';
+ }
+
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars();
+ $searchon = '';
+ $this->searchTerms = array();
+
+ # FIXME: This doesn't handle parenthetical expressions.
+ if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach( $m as $terms ) {
+ if( $searchon !== '' ) $searchon .= ' ';
+ if( $this->strictMatching && ($terms[1] == '') ) {
+ $terms[1] = '+';
+ }
+ $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
+ if( !empty( $terms[3] ) ) {
+ $regexp = preg_quote( $terms[3], '/' );
+ if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
+ } else {
+ $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
+ }
+ $this->searchTerms[] = $regexp;
+ }
+ wfDebug( "Would search with '$searchon'\n" );
+ wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" );
+ } else {
+ wfDebug( "Can't understand search query '{$this->filteredText}'\n" );
+ }
+
+ $searchon = preg_replace('/(\s+)/','&',$searchon);
+ $searchon = $this->db->strencode( $searchon );
+ return $searchon;
+ }
+
+ function queryRanking($filteredTerm, $fulltext) {
+ $field = $this->getIndexField( $fulltext );
+ $searchon = $this->parseQuery($filteredTerm,$fulltext);
+ if ($this->mRanking)
+ return " ORDER BY rank($field,to_tsquery('$searchon')) DESC";
+ else
+ return "";
+ }
+
+
+ function queryMain( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $field = $this->getIndexField( $fulltext );
+ $cur = $this->db->tableName( 'cur' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return 'SELECT cur_id, cur_namespace, cur_title, cur_text ' .
+ "FROM $cur,$searchindex " .
+ 'WHERE cur_id=si_page AND ' .
+ " $field @@ to_tsquery ('$match') " ;
+ }
+
+ function update( $id, $title, $text ) {
+ $dbw=& wfGetDB(DB_MASTER);
+ $searchindex = $dbw->tableName( 'searchindex' );
+ $sql = "DELETE FROM $searchindex WHERE si_page={$id}";
+ $dbw->query($sql,"SearchTsearch2:update");
+ $sql = "INSERT INTO $searchindex (si_page,si_title,si_text) ".
+ " VALUES ( $id, to_tsvector('".
+ $dbw->strencode($title).
+ "'),to_tsvector('".
+ $dbw->strencode( $text)."')) ";
+ $dbw->query($sql,"SearchTsearch2:update");
+ }
+
+ function updateTitle($id,$title) {
+ $dbw=& wfGetDB(DB_MASTER);
+ $searchindex = $dbw->tableName( 'searchindex' );
+ $sql = "UPDATE $searchindex SET si_title=to_tsvector('" .
+ $db->strencode( $title ) .
+ "') WHERE si_page={$id}";
+
+ $dbw->query( $sql, "SearchMySQL4::updateTitle" );
+ }
+
+}
+
+?>
diff --git a/includes/SearchUpdate.php b/includes/SearchUpdate.php
new file mode 100644
index 00000000..37981a67
--- /dev/null
+++ b/includes/SearchUpdate.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * See deferred.txt
+ * @package MediaWiki
+ */
+
+/**
+ *
+ * @package MediaWiki
+ */
+class SearchUpdate {
+
+ /* private */ var $mId = 0, $mNamespace, $mTitle, $mText;
+ /* private */ var $mTitleWords;
+
+ function SearchUpdate( $id, $title, $text = false ) {
+ $nt = Title::newFromText( $title );
+ if( $nt ) {
+ $this->mId = $id;
+ $this->mText = $text;
+
+ $this->mNamespace = $nt->getNamespace();
+ $this->mTitle = $nt->getText(); # Discard namespace
+
+ $this->mTitleWords = $this->mTextWords = array();
+ } else {
+ wfDebug( "SearchUpdate object created with invalid title '$title'\n" );
+ }
+ }
+
+ function doUpdate() {
+ global $wgContLang, $wgDisableSearchUpdate;
+
+ if( $wgDisableSearchUpdate || !$this->mId ) {
+ return false;
+ }
+ $fname = 'SearchUpdate::doUpdate';
+ wfProfileIn( $fname );
+
+ $search = SearchEngine::create();
+ $lc = $search->legalSearchChars() . '&#;';
+
+ if( $this->mText === false ) {
+ $search->updateTitle($this->mId,
+ Title::indexTitle( $this->mNamespace, $this->mTitle ));
+ wfProfileOut( $fname );
+ return;
+ }
+
+ # Language-specific strip/conversion
+ $text = $wgContLang->stripForSearch( $this->mText );
+
+ wfProfileIn( $fname.'-regexps' );
+ $text = preg_replace( "/<\\/?\\s*[A-Za-z][A-Za-z0-9]*\\s*([^>]*?)>/",
+ ' ', strtolower( " " . $text /*$this->mText*/ . " " ) ); # Strip HTML markup
+ $text = preg_replace( "/(^|\\n)==\\s*([^\\n]+)\\s*==(\\s)/sD",
+ "\\1\\2 \\2 \\2\\3", $text ); # Emphasize headings
+
+ # Strip external URLs
+ $uc = "A-Za-z0-9_\\/:.,~%\\-+&;#?!=()@\\xA0-\\xFF";
+ $protos = "http|https|ftp|mailto|news|gopher";
+ $pat = "/(^|[^\\[])({$protos}):[{$uc}]+([^{$uc}]|$)/";
+ $text = preg_replace( $pat, "\\1 \\3", $text );
+
+ $p1 = "/([^\\[])\\[({$protos}):[{$uc}]+]/";
+ $p2 = "/([^\\[])\\[({$protos}):[{$uc}]+\\s+([^\\]]+)]/";
+ $text = preg_replace( $p1, "\\1 ", $text );
+ $text = preg_replace( $p2, "\\1 \\3 ", $text );
+
+ # Internal image links
+ $pat2 = "/\\[\\[image:([{$uc}]+)\\.(gif|png|jpg|jpeg)([^{$uc}])/i";
+ $text = preg_replace( $pat2, " \\1 \\3", $text );
+
+ $text = preg_replace( "/([^{$lc}])([{$lc}]+)]]([a-z]+)/",
+ "\\1\\2 \\2\\3", $text ); # Handle [[game]]s
+
+ # Strip all remaining non-search characters
+ $text = preg_replace( "/[^{$lc}]+/", " ", $text );
+
+ # Handle 's, s'
+ #
+ # $text = preg_replace( "/([{$lc}]+)'s /", "\\1 \\1's ", $text );
+ # $text = preg_replace( "/([{$lc}]+)s' /", "\\1s ", $text );
+ #
+ # These tail-anchored regexps are insanely slow. The worst case comes
+ # when Japanese or Chinese text (ie, no word spacing) is written on
+ # a wiki configured for Western UTF-8 mode. The Unicode characters are
+ # expanded to hex codes and the "words" are very long paragraph-length
+ # monstrosities. On a large page the above regexps may take over 20
+ # seconds *each* on a 1GHz-level processor.
+ #
+ # Following are reversed versions which are consistently fast
+ # (about 3 milliseconds on 1GHz-level processor).
+ #
+ $text = strrev( preg_replace( "/ s'([{$lc}]+)/", " s'\\1 \\1", strrev( $text ) ) );
+ $text = strrev( preg_replace( "/ 's([{$lc}]+)/", " s\\1", strrev( $text ) ) );
+
+ # Strip wiki '' and '''
+ $text = preg_replace( "/''[']*/", " ", $text );
+ wfProfileOut( "$fname-regexps" );
+ $search->update($this->mId, Title::indexTitle( $this->mNamespace, $this->mTitle ),
+ $text);
+ wfProfileOut( $fname );
+ }
+}
+
+/**
+ * Placeholder class
+ * @package MediaWiki
+ */
+class SearchUpdateMyISAM extends SearchUpdate {
+ # Inherits everything
+}
+
+?>
diff --git a/includes/Setup.php b/includes/Setup.php
new file mode 100644
index 00000000..1ef83cc7
--- /dev/null
+++ b/includes/Setup.php
@@ -0,0 +1,330 @@
+<?php
+/**
+ * Include most things that's need to customize the site
+ * @package MediaWiki
+ */
+
+/**
+ * This file is not a valid entry point, perform no further processing unless
+ * MEDIAWIKI is defined
+ */
+if( defined( 'MEDIAWIKI' ) ) {
+
+# The main wiki script and things like database
+# conversion and maintenance scripts all share a
+# common setup of including lots of classes and
+# setting up a few globals.
+#
+
+// Check to see if we are at the file scope
+if ( !isset( $wgVersion ) ) {
+ echo "Error, Setup.php must be included from the file scope, after DefaultSettings.php\n";
+ die( 1 );
+}
+
+if( !isset( $wgProfiling ) )
+ $wgProfiling = false;
+
+require_once( "$IP/includes/AutoLoader.php" );
+
+if ( function_exists( 'wfProfileIn' ) ) {
+ /* nada, everything should be done already */
+} elseif ( $wgProfiling and (0 == rand() % $wgProfileSampleRate ) ) {
+ $wgProfiling = true;
+ if ($wgProfilerType == "") {
+ $wgProfiler = new Profiler();
+ } else {
+ $prclass="Profiler{$wgProfilerType}";
+ require_once( $prclass.".php" );
+ $wgProfiler = new $prclass();
+ }
+} else {
+ require_once( "$IP/includes/ProfilerStub.php" );
+}
+
+$fname = 'Setup.php';
+wfProfileIn( $fname );
+
+wfProfileIn( $fname.'-exception' );
+require_once( "$IP/includes/Exception.php" );
+wfInstallExceptionHandler();
+wfProfileOut( $fname.'-exception' );
+
+wfProfileIn( $fname.'-includes' );
+
+require_once( "$IP/includes/GlobalFunctions.php" );
+require_once( "$IP/includes/Hooks.php" );
+require_once( "$IP/includes/Namespace.php" );
+require_once( "$IP/includes/User.php" );
+require_once( "$IP/includes/OutputPage.php" );
+require_once( "$IP/includes/MagicWord.php" );
+require_once( "$IP/includes/MessageCache.php" );
+require_once( "$IP/includes/Parser.php" );
+require_once( "$IP/includes/LoadBalancer.php" );
+require_once( "$IP/includes/ProxyTools.php" );
+require_once( "$IP/includes/ObjectCache.php" );
+require_once( "$IP/includes/ImageFunctions.php" );
+
+if ( $wgUseDynamicDates ) {
+ require_once( "$IP/includes/DateFormatter.php" );
+}
+
+wfProfileOut( $fname.'-includes' );
+wfProfileIn( $fname.'-misc1' );
+
+$wgIP = false; # Load on demand
+$wgRequest = new WebRequest();
+if ( function_exists( 'posix_uname' ) ) {
+ $wguname = posix_uname();
+ $wgNodeName = $wguname['nodename'];
+} else {
+ $wgNodeName = '';
+}
+
+# Useful debug output
+if ( $wgCommandLineMode ) {
+ # wfDebug( '"' . implode( '" "', $argv ) . '"' );
+} elseif ( function_exists( 'getallheaders' ) ) {
+ wfDebug( "\n\nStart request\n" );
+ wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" );
+ $headers = getallheaders();
+ foreach ($headers as $name => $value) {
+ wfDebug( "$name: $value\n" );
+ }
+ wfDebug( "\n" );
+} elseif( isset( $_SERVER['REQUEST_URI'] ) ) {
+ wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" );
+}
+
+if ( $wgSkipSkin ) {
+ $wgSkipSkins[] = $wgSkipSkin;
+}
+
+$wgUseEnotif = $wgEnotifUserTalk || $wgEnotifWatchlist;
+
+wfProfileOut( $fname.'-misc1' );
+wfProfileIn( $fname.'-memcached' );
+
+$wgMemc =& wfGetMainCache();
+$messageMemc =& wfGetMessageCacheStorage();
+$parserMemc =& wfGetParserCacheStorage();
+
+wfDebug( 'Main cache: ' . get_class( $wgMemc ) .
+ "\nMessage cache: " . get_class( $messageMemc ) .
+ "\nParser cache: " . get_class( $parserMemc ) . "\n" );
+
+wfProfileOut( $fname.'-memcached' );
+wfProfileIn( $fname.'-SetupSession' );
+
+if ( $wgDBprefix ) {
+ $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix;
+} elseif ( $wgSharedDB ) {
+ $wgCookiePrefix = $wgSharedDB;
+} else {
+ $wgCookiePrefix = $wgDBname;
+}
+
+# If session.auto_start is there, we can't touch session name
+#
+if( !ini_get( 'session.auto_start' ) )
+ session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
+
+if( !$wgCommandLineMode && ( isset( $_COOKIE[session_name()] ) || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) {
+ wfIncrStats( 'request_with_session' );
+ User::SetupSession();
+ $wgSessionStarted = true;
+} else {
+ wfIncrStats( 'request_without_session' );
+ $wgSessionStarted = false;
+}
+
+wfProfileOut( $fname.'-SetupSession' );
+wfProfileIn( $fname.'-database' );
+
+if ( !$wgDBservers ) {
+ $wgDBservers = array(array(
+ 'host' => $wgDBserver,
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'dbname' => $wgDBname,
+ 'type' => $wgDBtype,
+ 'load' => 1,
+ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+ ));
+}
+$wgLoadBalancer = LoadBalancer::newFromParams( $wgDBservers, false, $wgMasterWaitTimeout );
+$wgLoadBalancer->loadMasterPos();
+
+wfProfileOut( $fname.'-database' );
+wfProfileIn( $fname.'-language1' );
+
+require_once( "$IP/languages/Language.php" );
+
+function setupLangObj($langclass) {
+ global $IP;
+
+ if( ! class_exists( $langclass ) ) {
+ # Default to English/UTF-8
+ $baseclass = 'LanguageUtf8';
+ require_once( "$IP/languages/$baseclass.php" );
+ $lc = strtolower(substr($langclass, 8));
+ $snip = "
+ class $langclass extends $baseclass {
+ function getVariants() {
+ return array(\"$lc\");
+ }
+
+ }";
+ eval($snip);
+ }
+
+ $lang = new $langclass();
+
+ return $lang;
+}
+
+# $wgLanguageCode may be changed later to fit with user preference.
+# The content language will remain fixed as per the configuration,
+# so let's keep it.
+$wgContLanguageCode = $wgLanguageCode;
+$wgContLangClass = 'Language' . str_replace( '-', '_', ucfirst( $wgContLanguageCode ) );
+
+$wgContLang = setupLangObj( $wgContLangClass );
+$wgContLang->initEncoding();
+
+wfProfileOut( $fname.'-language1' );
+wfProfileIn( $fname.'-User' );
+
+# Skin setup functions
+# Entries can be added to this variable during the inclusion
+# of the extension file. Skins can then perform any necessary initialisation.
+#
+foreach ( $wgSkinExtensionFunctions as $func ) {
+ call_user_func( $func );
+}
+
+if( !is_object( $wgAuth ) ) {
+ require_once( 'AuthPlugin.php' );
+ $wgAuth = new AuthPlugin();
+}
+
+if( $wgCommandLineMode ) {
+ # Used for some maintenance scripts; user session cookies can screw things up
+ # when the database is in an in-between state.
+ $wgUser = new User();
+ # Prevent loading User settings from the DB.
+ $wgUser->setLoaded( true );
+} else {
+ $wgUser = null;
+ wfRunHooks('AutoAuthenticate',array(&$wgUser));
+ if ($wgUser === null) {
+ $wgUser = User::loadFromSession();
+ }
+}
+
+wfProfileOut( $fname.'-User' );
+wfProfileIn( $fname.'-language2' );
+
+// wgLanguageCode now specifically means the UI language
+$wgLanguageCode = $wgRequest->getText('uselang', '');
+if ($wgLanguageCode == '')
+ $wgLanguageCode = $wgUser->getOption('language');
+# Validate $wgLanguageCode, which will soon be sent to an eval()
+if( empty( $wgLanguageCode ) || !preg_match( '/^[a-z]+(-[a-z]+)?$/', $wgLanguageCode ) ) {
+ $wgLanguageCode = $wgContLanguageCode;
+}
+
+$wgLangClass = 'Language'. str_replace( '-', '_', ucfirst( $wgLanguageCode ) );
+
+if( $wgLangClass == $wgContLangClass ) {
+ $wgLang = &$wgContLang;
+} else {
+ wfSuppressWarnings();
+ // Preload base classes to work around APC/PHP5 bug
+ include_once("$IP/languages/$wgLangClass.deps.php");
+ include_once("$IP/languages/$wgLangClass.php");
+ wfRestoreWarnings();
+
+ $wgLang = setupLangObj( $wgLangClass );
+}
+
+wfProfileOut( $fname.'-language2' );
+wfProfileIn( $fname.'-MessageCache' );
+
+$wgMessageCache = new MessageCache( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgDBname);
+
+wfProfileOut( $fname.'-MessageCache' );
+
+#
+# I guess the warning about UI switching might still apply...
+#
+# FIXME: THE ABOVE MIGHT BREAK NAMESPACES, VARIABLES,
+# SEARCH INDEX UPDATES, AND MANY MANY THINGS.
+# DO NOT USE THIS MODE EXCEPT FOR TESTING RIGHT NOW.
+#
+# To disable it, the easiest thing could be to uncomment the
+# following; they should effectively disable the UI switch functionality
+#
+# $wgLangClass = $wgContLangClass;
+# $wgLanguageCode = $wgContLanguageCode;
+# $wgLang = $wgContLang;
+#
+# TODO: Need to change reference to $wgLang to $wgContLang at proper
+# places, including namespaces, dates in signatures, magic words,
+# and links
+#
+# TODO: Need to look at the issue of input/output encoding
+#
+
+
+wfProfileIn( $fname.'-OutputPage' );
+
+$wgOut = new OutputPage();
+
+wfProfileOut( $fname.'-OutputPage' );
+wfProfileIn( $fname.'-misc2' );
+
+$wgDeferredUpdateList = array();
+$wgPostCommitUpdateList = array();
+
+$wgMagicWords = array();
+
+if ( $wgUseXMLparser ) {
+ require_once( 'ParserXML.php' );
+ $wgParser = new ParserXML();
+} else {
+ $wgParser = new Parser();
+}
+$wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) );
+$wgMsgParserOptions = ParserOptions::newFromUser($wgUser);
+wfSeedRandom();
+
+# Placeholders in case of DB error
+$wgTitle = Title::makeTitle( NS_SPECIAL, 'Error' );
+$wgArticle = new Article($wgTitle);
+
+wfProfileOut( $fname.'-misc2' );
+wfProfileIn( $fname.'-extensions' );
+
+# Extension setup functions for extensions other than skins
+# Entries should be added to this variable during the inclusion
+# of the extension file. This allows the extension to perform
+# any necessary initialisation in the fully initialised environment
+foreach ( $wgExtensionFunctions as $func ) {
+ call_user_func( $func );
+}
+
+// For compatibility
+wfRunHooks( 'LogPageValidTypes', array( &$wgLogTypes ) );
+wfRunHooks( 'LogPageLogName', array( &$wgLogNames ) );
+wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) );
+wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) );
+
+
+wfDebug( "\n" );
+$wgFullyInitialised = true;
+wfProfileOut( $fname.'-extensions' );
+wfProfileOut( $fname );
+
+}
+?>
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
new file mode 100644
index 00000000..8fd5d6b6
--- /dev/null
+++ b/includes/SiteConfiguration.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * This is a class used to hold configuration settings, particularly for multi-wiki sites.
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * The include paths change after this file is included from commandLine.inc,
+ * meaning that require_once() fails to detect that it is including the same
+ * file again. We use DIY C-style protection as a workaround.
+ */
+if (!defined('SITE_CONFIGURATION')) {
+define('SITE_CONFIGURATION', 1);
+
+/** @package MediaWiki */
+class SiteConfiguration {
+ var $suffixes = array();
+ var $wikis = array();
+ var $settings = array();
+ var $localVHosts = array();
+
+ /** */
+ function get( $setting, $wiki, $suffix, $params = array() ) {
+ if ( array_key_exists( $setting, $this->settings ) ) {
+ if ( array_key_exists( $wiki, $this->settings[$setting] ) ) {
+ $retval = $this->settings[$setting][$wiki];
+ } elseif ( array_key_exists( $suffix, $this->settings[$setting] ) ) {
+ $retval = $this->settings[$setting][$suffix];
+ } elseif ( array_key_exists( 'default', $this->settings[$setting] ) ) {
+ $retval = $this->settings[$setting]['default'];
+ } else {
+ $retval = NULL;
+ }
+ } else {
+ $retval = NULL;
+ }
+
+ if ( !is_null( $retval ) && count( $params ) ) {
+ foreach ( $params as $key => $value ) {
+ $retval = str_replace( '$' . $key, $value, $retval );
+ }
+ }
+ return $retval;
+ }
+
+ /** */
+ function getAll( $wiki, $suffix, $params ) {
+ $localSettings = array();
+ foreach ( $this->settings as $varname => $stuff ) {
+ $value = $this->get( $varname, $wiki, $suffix, $params );
+ if ( !is_null( $value ) ) {
+ $localSettings[$varname] = $value;
+ }
+ }
+ return $localSettings;
+ }
+
+ /** */
+ function getBool( $setting, $wiki, $suffix ) {
+ return (bool)($this->get( $setting, $wiki, $suffix ));
+ }
+
+ /** */
+ function &getLocalDatabases() {
+ return $this->wikis;
+ }
+
+ /** */
+ function initialise() {
+ }
+
+ /** */
+ function extractVar( $setting, $wiki, $suffix, &$var, $params ) {
+ $value = $this->get( $setting, $wiki, $suffix, $params );
+ if ( !is_null( $value ) ) {
+ $var = $value;
+ }
+ }
+
+ /** */
+ function extractGlobal( $setting, $wiki, $suffix, $params ) {
+ $value = $this->get( $setting, $wiki, $suffix, $params );
+ if ( !is_null( $value ) ) {
+ $GLOBALS[$setting] = $value;
+ }
+ }
+
+ /** */
+ function extractAllGlobals( $wiki, $suffix, $params ) {
+ foreach ( $this->settings as $varName => $setting ) {
+ $this->extractGlobal( $varName, $wiki, $suffix, $params );
+ }
+ }
+
+ /**
+ * Work out the site and language name from a database name
+ * @param $db
+ */
+ function siteFromDB( $db ) {
+ $site = NULL;
+ $lang = NULL;
+ foreach ( $this->suffixes as $suffix ) {
+ if ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
+ $site = $suffix == 'wiki' ? 'wikipedia' : $suffix;
+ $lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) );
+ break;
+ }
+ }
+ $lang = str_replace( '_', '-', $lang );
+ return array( $site, $lang );
+ }
+
+ /** */
+ function isLocalVHost( $vhost ) {
+ return in_array( $vhost, $this->localVHosts );
+ }
+}
+}
+
+?>
diff --git a/includes/SiteStatsUpdate.php b/includes/SiteStatsUpdate.php
new file mode 100644
index 00000000..1b6d3804
--- /dev/null
+++ b/includes/SiteStatsUpdate.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * See deferred.txt
+ *
+ * @package MediaWiki
+ */
+
+/**
+ *
+ * @package MediaWiki
+ */
+class SiteStatsUpdate {
+
+ var $mViews, $mEdits, $mGood, $mPages, $mUsers;
+
+ function SiteStatsUpdate( $views, $edits, $good, $pages = 0, $users = 0 ) {
+ $this->mViews = $views;
+ $this->mEdits = $edits;
+ $this->mGood = $good;
+ $this->mPages = $pages;
+ $this->mUsers = $users;
+ }
+
+ function appendUpdate( &$sql, $field, $delta ) {
+ if ( $delta ) {
+ if ( $sql ) {
+ $sql .= ',';
+ }
+ if ( $delta < 0 ) {
+ $sql .= "$field=$field-1";
+ } else {
+ $sql .= "$field=$field+1";
+ }
+ }
+ }
+
+ function doUpdate() {
+ $fname = 'SiteStatsUpdate::doUpdate';
+ $dbw =& wfGetDB( DB_MASTER );
+
+ # First retrieve the row just to find out which schema we're in
+ $row = $dbw->selectRow( 'site_stats', '*', false, $fname );
+
+ $updates = '';
+
+ $this->appendUpdate( $updates, 'ss_total_views', $this->mViews );
+ $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits );
+ $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood );
+
+ if ( isset( $row->ss_total_pages ) ) {
+ # Update schema if required
+ if ( $row->ss_total_pages == -1 && !$this->mViews ) {
+ $dbr =& wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') );
+ extract( $dbr->tableNames( 'page', 'user' ) );
+
+ $sql = "SELECT COUNT(page_namespace) AS total FROM $page";
+ $res = $dbr->query( $sql, $fname );
+ $pageRow = $dbr->fetchObject( $res );
+ $pages = $pageRow->total + $this->mPages;
+
+ $sql = "SELECT COUNT(user_id) AS total FROM $user";
+ $res = $dbr->query( $sql, $fname );
+ $userRow = $dbr->fetchObject( $res );
+ $users = $userRow->total + $this->mUsers;
+
+ if ( $updates ) {
+ $updates .= ',';
+ }
+ $updates .= "ss_total_pages=$pages, ss_users=$users";
+ } else {
+ $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages );
+ $this->appendUpdate( $updates, 'ss_users', $this->mUsers );
+ }
+ }
+ if ( $updates ) {
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1);
+ $dbw->query( $sql, $fname );
+ }
+ }
+}
+?>
diff --git a/includes/Skin.php b/includes/Skin.php
new file mode 100644
index 00000000..8a03f461
--- /dev/null
+++ b/includes/Skin.php
@@ -0,0 +1,1499 @@
+<?php
+if ( ! defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage Skins
+ */
+
+# See skin.txt
+
+/**
+ * The main skin class that provide methods and properties for all other skins.
+ * This base class is also the "Standard" skin.
+ * @package MediaWiki
+ */
+class Skin extends Linker {
+ /**#@+
+ * @private
+ */
+ var $lastdate, $lastline;
+ var $rc_cache ; # Cache for Enhanced Recent Changes
+ var $rcCacheIndex ; # Recent Changes Cache Counter for visibility toggle
+ var $rcMoveIndex;
+ /**#@-*/
+
+ /** Constructor, call parent constructor */
+ function Skin() { parent::Linker(); }
+
+ /**
+ * Fetch the set of available skins.
+ * @return array of strings
+ * @static
+ */
+ function &getSkinNames() {
+ global $wgValidSkinNames;
+ static $skinsInitialised = false;
+ if ( !$skinsInitialised ) {
+ # Get a list of available skins
+ # Build using the regular expression '^(.*).php$'
+ # Array keys are all lower case, array value keep the case used by filename
+ #
+ wfProfileIn( __METHOD__ . '-init' );
+ global $wgStyleDirectory;
+ $skinDir = dir( $wgStyleDirectory );
+
+ # while code from www.php.net
+ while (false !== ($file = $skinDir->read())) {
+ // Skip non-PHP files, hidden files, and '.dep' includes
+ if(preg_match('/^([^.]*)\.php$/',$file, $matches)) {
+ $aSkin = $matches[1];
+ $wgValidSkinNames[strtolower($aSkin)] = $aSkin;
+ }
+ }
+ $skinDir->close();
+ $skinsInitialised = true;
+ wfProfileOut( __METHOD__ . '-init' );
+ }
+ return $wgValidSkinNames;
+ }
+
+ /**
+ * Normalize a skin preference value to a form that can be loaded.
+ * If a skin can't be found, it will fall back to the configured
+ * default (or the old 'Classic' skin if that's broken).
+ * @param string $key
+ * @return string
+ * @static
+ */
+ function normalizeKey( $key ) {
+ global $wgDefaultSkin;
+ $skinNames = Skin::getSkinNames();
+
+ if( $key == '' ) {
+ // Don't return the default immediately;
+ // in a misconfiguration we need to fall back.
+ $key = $wgDefaultSkin;
+ }
+
+ if( isset( $skinNames[$key] ) ) {
+ return $key;
+ }
+
+ // Older versions of the software used a numeric setting
+ // in the user preferences.
+ $fallback = array(
+ 0 => $wgDefaultSkin,
+ 1 => 'nostalgia',
+ 2 => 'cologneblue' );
+
+ if( isset( $fallback[$key] ) ){
+ $key = $fallback[$key];
+ }
+
+ if( isset( $skinNames[$key] ) ) {
+ return $key;
+ } else {
+ // The old built-in skin
+ return 'standard';
+ }
+ }
+
+ /**
+ * Factory method for loading a skin of a given type
+ * @param string $key 'monobook', 'standard', etc
+ * @return Skin
+ * @static
+ */
+ function &newFromKey( $key ) {
+ global $wgStyleDirectory;
+
+ $key = Skin::normalizeKey( $key );
+
+ $skinNames = Skin::getSkinNames();
+ $skinName = $skinNames[$key];
+
+ # Grab the skin class and initialise it.
+ wfSuppressWarnings();
+ // Preload base classes to work around APC/PHP5 bug
+ include_once( "{$wgStyleDirectory}/{$skinName}.deps.php" );
+ wfRestoreWarnings();
+ require_once( "{$wgStyleDirectory}/{$skinName}.php" );
+
+ # Check if we got if not failback to default skin
+ $className = 'Skin'.$skinName;
+ if( !class_exists( $className ) ) {
+ # DO NOT die if the class isn't found. This breaks maintenance
+ # scripts and can cause a user account to be unrecoverable
+ # except by SQL manipulation if a previously valid skin name
+ # is no longer valid.
+ wfDebug( "Skin class does not exist: $className\n" );
+ $className = 'SkinStandard';
+ require_once( "{$wgStyleDirectory}/Standard.php" );
+ }
+ $skin =& new $className;
+ return $skin;
+ }
+
+ /** @return string path to the skin stylesheet */
+ function getStylesheet() {
+ return 'common/wikistandard.css?1';
+ }
+
+ /** @return string skin name */
+ function getSkinName() {
+ return 'standard';
+ }
+
+ function qbSetting() {
+ global $wgOut, $wgUser;
+
+ if ( $wgOut->isQuickbarSuppressed() ) { return 0; }
+ $q = $wgUser->getOption( 'quickbar' );
+ if ( '' == $q ) { $q = 0; }
+ return $q;
+ }
+
+ function initPage( &$out ) {
+ global $wgFavicon;
+
+ $fname = 'Skin::initPage';
+ wfProfileIn( $fname );
+
+ if( false !== $wgFavicon ) {
+ $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
+ }
+
+ $this->addMetadataLinks($out);
+
+ $this->mRevisionId = $out->mRevisionId;
+
+ $this->preloadExistence();
+
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Preload the existence of three commonly-requested pages in a single query
+ */
+ function preloadExistence() {
+ global $wgUser, $wgTitle;
+
+ if ( $wgTitle->isTalkPage() ) {
+ $otherTab = $wgTitle->getSubjectPage();
+ } else {
+ $otherTab = $wgTitle->getTalkPage();
+ }
+ $lb = new LinkBatch( array(
+ $wgUser->getUserPage(),
+ $wgUser->getTalkPage(),
+ $otherTab
+ ));
+ $lb->execute();
+ }
+
+ function addMetadataLinks( &$out ) {
+ global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf;
+ global $wgRightsPage, $wgRightsUrl;
+
+ if( $out->isArticleRelated() ) {
+ # note: buggy CC software only reads first "meta" link
+ if( $wgEnableCreativeCommonsRdf ) {
+ $out->addMetadataLink( array(
+ 'title' => 'Creative Commons',
+ 'type' => 'application/rdf+xml',
+ 'href' => $wgTitle->getLocalURL( 'action=creativecommons') ) );
+ }
+ if( $wgEnableDublinCoreRdf ) {
+ $out->addMetadataLink( array(
+ 'title' => 'Dublin Core',
+ 'type' => 'application/rdf+xml',
+ 'href' => $wgTitle->getLocalURL( 'action=dublincore' ) ) );
+ }
+ }
+ $copyright = '';
+ if( $wgRightsPage ) {
+ $copy = Title::newFromText( $wgRightsPage );
+ if( $copy ) {
+ $copyright = $copy->getLocalURL();
+ }
+ }
+ if( !$copyright && $wgRightsUrl ) {
+ $copyright = $wgRightsUrl;
+ }
+ if( $copyright ) {
+ $out->addLink( array(
+ 'rel' => 'copyright',
+ 'href' => $copyright ) );
+ }
+ }
+
+ function outputPage( &$out ) {
+ global $wgDebugComments;
+
+ wfProfileIn( 'Skin::outputPage' );
+ $this->initPage( $out );
+
+ $out->out( $out->headElement() );
+
+ $out->out( "\n<body" );
+ $ops = $this->getBodyOptions();
+ foreach ( $ops as $name => $val ) {
+ $out->out( " $name='$val'" );
+ }
+ $out->out( ">\n" );
+ if ( $wgDebugComments ) {
+ $out->out( "<!-- Wiki debugging output:\n" .
+ $out->mDebugtext . "-->\n" );
+ }
+
+ $out->out( $this->beforeContent() );
+
+ $out->out( $out->mBodytext . "\n" );
+
+ $out->out( $this->afterContent() );
+
+ $out->out( $out->reportTime() );
+
+ $out->out( "\n</body></html>" );
+ }
+
+ function getHeadScripts() {
+ global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType;
+ $r = "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js\"></script>\n";
+ if( $wgAllowUserJs && $wgUser->isLoggedIn() ) {
+ $userpage = $wgUser->getUserPage();
+ $userjs = htmlspecialchars( $this->makeUrl(
+ $userpage->getPrefixedText().'/'.$this->getSkinName().'.js',
+ 'action=raw&ctype='.$wgJsMimeType));
+ $r .= '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>\n";
+ }
+ return $r;
+ }
+
+ /**
+ * To make it harder for someone to slip a user a fake
+ * user-JavaScript or user-CSS preview, a random token
+ * is associated with the login session. If it's not
+ * passed back with the preview request, we won't render
+ * the code.
+ *
+ * @param string $action
+ * @return bool
+ * @private
+ */
+ function userCanPreview( $action ) {
+ global $wgTitle, $wgRequest, $wgUser;
+
+ if( $action != 'submit' )
+ return false;
+ if( !$wgRequest->wasPosted() )
+ return false;
+ if( !$wgTitle->userCanEditCssJsSubpage() )
+ return false;
+ return $wgUser->matchEditToken(
+ $wgRequest->getVal( 'wpEditToken' ) );
+ }
+
+ # get the user/site-specific stylesheet, SkinTemplate loads via RawPage.php (settings are cached that way)
+ function getUserStylesheet() {
+ global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage;
+ $sheet = $this->getStylesheet();
+ $action = $wgRequest->getText('action');
+ $s = "@import \"$wgStylePath/$sheet\";\n";
+ if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css\";\n";
+
+ $query = "action=raw&ctype=text/css&smaxage=$wgSquidMaxage";
+ $s .= '@import "' . $this->makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" .
+ '@import "'.$this->makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n";
+
+ $s .= $this->doGetUserStyles();
+ return $s."\n";
+ }
+
+ /**
+ * placeholder, returns generated js in monobook
+ */
+ function getUserJs() { return; }
+
+ /**
+ * Return html code that include User stylesheets
+ */
+ function getUserStyles() {
+ $s = "<style type='text/css'>\n";
+ $s .= "/*/*/ /*<![CDATA[*/\n"; # <-- Hide the styles from Netscape 4 without hiding them from IE/Mac
+ $s .= $this->getUserStylesheet();
+ $s .= "/*]]>*/ /* */\n";
+ $s .= "</style>\n";
+ return $s;
+ }
+
+ /**
+ * Some styles that are set by user through the user settings interface.
+ */
+ function doGetUserStyles() {
+ global $wgUser, $wgUser, $wgRequest, $wgTitle, $wgAllowUserCss;
+
+ $s = '';
+
+ if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { # logged in
+ if($wgTitle->isCssSubpage() && $this->userCanPreview( $wgRequest->getText( 'action' ) ) ) {
+ $s .= $wgRequest->getText('wpTextbox1');
+ } else {
+ $userpage = $wgUser->getUserPage();
+ $s.= '@import "'.$this->makeUrl(
+ $userpage->getPrefixedText().'/'.$this->getSkinName().'.css',
+ 'action=raw&ctype=text/css').'";'."\n";
+ }
+ }
+
+ return $s . $this->reallyDoGetUserStyles();
+ }
+
+ function reallyDoGetUserStyles() {
+ global $wgUser;
+ $s = '';
+ if (($undopt = $wgUser->getOption("underline")) != 2) {
+ $underline = $undopt ? 'underline' : 'none';
+ $s .= "a { text-decoration: $underline; }\n";
+ }
+ if( $wgUser->getOption( 'highlightbroken' ) ) {
+ $s .= "a.new, #quickbar a.new { color: #CC2200; }\n";
+ } else {
+ $s .= <<<END
+a.new, #quickbar a.new,
+a.stub, #quickbar a.stub {
+ color: inherit;
+ text-decoration: inherit;
+}
+a.new:after, #quickbar a.new:after {
+ content: "?";
+ color: #CC2200;
+ text-decoration: $underline;
+}
+a.stub:after, #quickbar a.stub:after {
+ content: "!";
+ color: #772233;
+ text-decoration: $underline;
+}
+END;
+ }
+ if( $wgUser->getOption( 'justify' ) ) {
+ $s .= "#article, #bodyContent { text-align: justify; }\n";
+ }
+ if( !$wgUser->getOption( 'showtoc' ) ) {
+ $s .= "#toc { display: none; }\n";
+ }
+ if( !$wgUser->getOption( 'editsection' ) ) {
+ $s .= ".editsection { display: none; }\n";
+ }
+ return $s;
+ }
+
+ function getBodyOptions() {
+ global $wgUser, $wgTitle, $wgOut, $wgRequest;
+
+ extract( $wgRequest->getValues( 'oldid', 'redirect', 'diff' ) );
+
+ if ( 0 != $wgTitle->getNamespace() ) {
+ $a = array( 'bgcolor' => '#ffffec' );
+ }
+ else $a = array( 'bgcolor' => '#FFFFFF' );
+ if($wgOut->isArticle() && $wgUser->getOption('editondblclick') &&
+ $wgTitle->userCanEdit() ) {
+ $t = wfMsg( 'editthispage' );
+ $s = $wgTitle->getFullURL( $this->editUrlOptions() );
+ $s = 'document.location = "' .wfEscapeJSString( $s ) .'";';
+ $a += array ('ondblclick' => $s);
+
+ }
+ $a['onload'] = $wgOut->getOnloadHandler();
+ if( $wgUser->getOption( 'editsectiononrightclick' ) ) {
+ if( $a['onload'] != '' ) {
+ $a['onload'] .= ';';
+ }
+ $a['onload'] .= 'setupRightClickEdit()';
+ }
+ return $a;
+ }
+
+ /**
+ * URL to the logo
+ */
+ function getLogo() {
+ global $wgLogo;
+ return $wgLogo;
+ }
+
+ /**
+ * This will be called immediately after the <body> tag. Split into
+ * two functions to make it easier to subclass.
+ */
+ function beforeContent() {
+ return $this->doBeforeContent();
+ }
+
+ function doBeforeContent() {
+ global $wgContLang;
+ $fname = 'Skin::doBeforeContent';
+ wfProfileIn( $fname );
+
+ $s = '';
+ $qb = $this->qbSetting();
+
+ if( $langlinks = $this->otherLanguages() ) {
+ $rows = 2;
+ $borderhack = '';
+ } else {
+ $rows = 1;
+ $langlinks = false;
+ $borderhack = 'class="top"';
+ }
+
+ $s .= "\n<div id='content'>\n<div id='topbar'>\n" .
+ "<table border='0' cellspacing='0' width='98%'>\n<tr>\n";
+
+ $shove = ($qb != 0);
+ $left = ($qb == 1 || $qb == 3);
+ if($wgContLang->isRTL()) $left = !$left;
+
+ if ( !$shove ) {
+ $s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" .
+ $this->logoText() . '</td>';
+ } elseif( $left ) {
+ $s .= $this->getQuickbarCompensator( $rows );
+ }
+ $l = $wgContLang->isRTL() ? 'right' : 'left';
+ $s .= "<td {$borderhack} align='$l' valign='top'>\n";
+
+ $s .= $this->topLinks() ;
+ $s .= "<p class='subtitle'>" . $this->pageTitleLinks() . "</p>\n";
+
+ $r = $wgContLang->isRTL() ? "left" : "right";
+ $s .= "</td>\n<td {$borderhack} valign='top' align='$r' nowrap='nowrap'>";
+ $s .= $this->nameAndLogin();
+ $s .= "\n<br />" . $this->searchForm() . "</td>";
+
+ if ( $langlinks ) {
+ $s .= "</tr>\n<tr>\n<td class='top' colspan=\"2\">$langlinks</td>\n";
+ }
+
+ if ( $shove && !$left ) { # Right
+ $s .= $this->getQuickbarCompensator( $rows );
+ }
+ $s .= "</tr>\n</table>\n</div>\n";
+ $s .= "\n<div id='article'>\n";
+
+ $notice = wfGetSiteNotice();
+ if( $notice ) {
+ $s .= "\n<div id='siteNotice'>$notice</div>\n";
+ }
+ $s .= $this->pageTitle();
+ $s .= $this->pageSubtitle() ;
+ $s .= $this->getCategories();
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+
+ function getCategoryLinks () {
+ global $wgOut, $wgTitle, $wgUseCategoryBrowser;
+ global $wgContLang;
+
+ if( count( $wgOut->mCategoryLinks ) == 0 ) return '';
+
+ # Separator
+ $sep = wfMsgHtml( 'catseparator' );
+
+ // Use Unicode bidi embedding override characters,
+ // to make sure links don't smash each other up in ugly ways.
+ $dir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+ $embed = "<span dir='$dir'>";
+ $pop = '</span>';
+ $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $wgOut->mCategoryLinks ) . $pop;
+
+ $msg = wfMsgExt('categories', array('parsemag', 'escape'), count( $wgOut->mCategoryLinks ));
+ $s = $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Categories' ),
+ $msg, 'article=' . urlencode( $wgTitle->getPrefixedDBkey() ) )
+ . ': ' . $t;
+
+ # optional 'dmoz-like' category browser. Will be shown under the list
+ # of categories an article belong to
+ if($wgUseCategoryBrowser) {
+ $s .= '<br /><hr />';
+
+ # get a big array of the parents tree
+ $parenttree = $wgTitle->getParentCategoryTree();
+ # Skin object passed by reference cause it can not be
+ # accessed under the method subfunction drawCategoryBrowser
+ $tempout = explode("\n", Skin::drawCategoryBrowser($parenttree, $this) );
+ # Clean out bogus first entry and sort them
+ unset($tempout[0]);
+ asort($tempout);
+ # Output one per line
+ $s .= implode("<br />\n", $tempout);
+ }
+
+ return $s;
+ }
+
+ /** Render the array as a serie of links.
+ * @param $tree Array: categories tree returned by Title::getParentCategoryTree
+ * @param &skin Object: skin passed by reference
+ * @return String separated by &gt;, terminate with "\n"
+ */
+ function drawCategoryBrowser($tree, &$skin) {
+ $return = '';
+ foreach ($tree as $element => $parent) {
+ if (empty($parent)) {
+ # element start a new list
+ $return .= "\n";
+ } else {
+ # grab the others elements
+ $return .= Skin::drawCategoryBrowser($parent, $skin) . ' &gt; ';
+ }
+ # add our current element to the list
+ $eltitle = Title::NewFromText($element);
+ $return .= $skin->makeLinkObj( $eltitle, $eltitle->getText() ) ;
+ }
+ return $return;
+ }
+
+ function getCategories() {
+ $catlinks=$this->getCategoryLinks();
+ if(!empty($catlinks)) {
+ return "<p class='catlinks'>{$catlinks}</p>";
+ }
+ }
+
+ function getQuickbarCompensator( $rows = 1 ) {
+ return "<td width='152' rowspan='{$rows}'>&nbsp;</td>";
+ }
+
+ /**
+ * This gets called immediately before the \</body\> tag.
+ * @return String HTML to be put after \</body\> ???
+ */
+ function afterContent() {
+ $printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n";
+ return $printfooter . $this->doAfterContent();
+ }
+
+ /** @return string Retrievied from HTML text */
+ function printSource() {
+ global $wgTitle;
+ $url = htmlspecialchars( $wgTitle->getFullURL() );
+ return wfMsg( 'retrievedfrom', '<a href="'.$url.'">'.$url.'</a>' );
+ }
+
+ function printFooter() {
+ return "<p>" . $this->printSource() .
+ "</p>\n\n<p>" . $this->pageStats() . "</p>\n";
+ }
+
+ /** overloaded by derived classes */
+ function doAfterContent() { }
+
+ function pageTitleLinks() {
+ global $wgOut, $wgTitle, $wgUser, $wgRequest;
+
+ extract( $wgRequest->getValues( 'oldid', 'diff' ) );
+ $action = $wgRequest->getText( 'action' );
+
+ $s = $this->printableLink();
+ $disclaimer = $this->disclaimerLink(); # may be empty
+ if( $disclaimer ) {
+ $s .= ' | ' . $disclaimer;
+ }
+ $privacy = $this->privacyLink(); # may be empty too
+ if( $privacy ) {
+ $s .= ' | ' . $privacy;
+ }
+
+ if ( $wgOut->isArticleRelated() ) {
+ if ( $wgTitle->getNamespace() == NS_IMAGE ) {
+ $name = $wgTitle->getDBkey();
+ $image = new Image( $wgTitle );
+ if( $image->exists() ) {
+ $link = htmlspecialchars( $image->getURL() );
+ $style = $this->getInternalLinkAttributes( $link, $name );
+ $s .= " | <a href=\"{$link}\"{$style}>{$name}</a>";
+ }
+ }
+ }
+ if ( 'history' == $action || isset( $diff ) || isset( $oldid ) ) {
+ $s .= ' | ' . $this->makeKnownLinkObj( $wgTitle,
+ wfMsg( 'currentrev' ) );
+ }
+
+ if ( $wgUser->getNewtalk() ) {
+ # do not show "You have new messages" text when we are viewing our
+ # own talk page
+ if( !$wgTitle->equals( $wgUser->getTalkPage() ) ) {
+ $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessageslink' ), 'redirect=no' );
+ $dl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessagesdifflink' ), 'diff=cur' );
+ $s.= ' | <strong>'. wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>';
+ # disable caching
+ $wgOut->setSquidMaxage(0);
+ $wgOut->enableClientCache(false);
+ }
+ }
+
+ $undelete = $this->getUndeleteLink();
+ if( !empty( $undelete ) ) {
+ $s .= ' | '.$undelete;
+ }
+ return $s;
+ }
+
+ function getUndeleteLink() {
+ global $wgUser, $wgTitle, $wgContLang, $action;
+ if( $wgUser->isAllowed( 'deletedhistory' ) &&
+ (($wgTitle->getArticleId() == 0) || ($action == "history")) &&
+ ($n = $wgTitle->isDeleted() ) )
+ {
+ if ( $wgUser->isAllowed( 'delete' ) ) {
+ $msg = 'thisisdeleted';
+ } else {
+ $msg = 'viewdeleted';
+ }
+ return wfMsg( $msg,
+ $this->makeKnownLink(
+ $wgContLang->SpecialPage( 'Undelete/' . $wgTitle->getPrefixedDBkey() ),
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $n ) ) );
+ }
+ return '';
+ }
+
+ function printableLink() {
+ global $wgOut, $wgFeedClasses, $wgRequest;
+
+ $baseurl = $_SERVER['REQUEST_URI'];
+ if( strpos( '?', $baseurl ) == false ) {
+ $baseurl .= '?';
+ } else {
+ $baseurl .= '&';
+ }
+ $baseurl = htmlspecialchars( $baseurl );
+ $printurl = $wgRequest->escapeAppendQuery( 'printable=yes' );
+
+ $s = "<a href=\"$printurl\">" . wfMsg( 'printableversion' ) . '</a>';
+ if( $wgOut->isSyndicated() ) {
+ foreach( $wgFeedClasses as $format => $class ) {
+ $feedurl = $wgRequest->escapeAppendQuery( "feed=$format" );
+ $s .= " | <a href=\"$feedurl\">{$format}</a>";
+ }
+ }
+ return $s;
+ }
+
+ function pageTitle() {
+ global $wgOut;
+ $s = '<h1 class="pagetitle">' . htmlspecialchars( $wgOut->getPageTitle() ) . '</h1>';
+ return $s;
+ }
+
+ function pageSubtitle() {
+ global $wgOut;
+
+ $sub = $wgOut->getSubtitle();
+ if ( '' == $sub ) {
+ global $wgExtraSubtitle;
+ $sub = wfMsg( 'tagline' ) . $wgExtraSubtitle;
+ }
+ $subpages = $this->subPageSubtitle();
+ $sub .= !empty($subpages)?"</p><p class='subpages'>$subpages":'';
+ $s = "<p class='subtitle'>{$sub}</p>\n";
+ return $s;
+ }
+
+ function subPageSubtitle() {
+ global $wgOut,$wgTitle,$wgNamespacesWithSubpages;
+ $subpages = '';
+ if($wgOut->isArticle() && !empty($wgNamespacesWithSubpages[$wgTitle->getNamespace()])) {
+ $ptext=$wgTitle->getPrefixedText();
+ if(preg_match('/\//',$ptext)) {
+ $links = explode('/',$ptext);
+ $c = 0;
+ $growinglink = '';
+ foreach($links as $link) {
+ $c++;
+ if ($c<count($links)) {
+ $growinglink .= $link;
+ $getlink = $this->makeLink( $growinglink, htmlspecialchars( $link ) );
+ if(preg_match('/class="new"/i',$getlink)) { break; } # this is a hack, but it saves time
+ if ($c>1) {
+ $subpages .= ' | ';
+ } else {
+ $subpages .= '&lt; ';
+ }
+ $subpages .= $getlink;
+ $growinglink .= '/';
+ }
+ }
+ }
+ }
+ return $subpages;
+ }
+
+ function nameAndLogin() {
+ global $wgUser, $wgTitle, $wgLang, $wgContLang, $wgShowIPinHeader;
+
+ $li = $wgContLang->specialPage( 'Userlogin' );
+ $lo = $wgContLang->specialPage( 'Userlogout' );
+
+ $s = '';
+ if ( $wgUser->isAnon() ) {
+ if( $wgShowIPinHeader && isset( $_COOKIE[ini_get('session.name')] ) ) {
+ $n = wfGetIP();
+
+ $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(),
+ $wgLang->getNsText( NS_TALK ) );
+
+ $s .= $n . ' ('.$tl.')';
+ } else {
+ $s .= wfMsg('notloggedin');
+ }
+
+ $rt = $wgTitle->getPrefixedURL();
+ if ( 0 == strcasecmp( urlencode( $lo ), $rt ) ) {
+ $q = '';
+ } else { $q = "returnto={$rt}"; }
+
+ $s .= "\n<br />" . $this->makeKnownLinkObj(
+ Title::makeTitle( NS_SPECIAL, 'Userlogin' ),
+ wfMsg( 'login' ), $q );
+ } else {
+ $n = $wgUser->getName();
+ $rt = $wgTitle->getPrefixedURL();
+ $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(),
+ $wgLang->getNsText( NS_TALK ) );
+
+ $tl = " ({$tl})";
+
+ $s .= $this->makeKnownLinkObj( $wgUser->getUserPage(),
+ $n ) . "{$tl}<br />" .
+ $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Userlogout' ), wfMsg( 'logout' ),
+ "returnto={$rt}" ) . ' | ' .
+ $this->specialLink( 'preferences' );
+ }
+ $s .= ' | ' . $this->makeKnownLink( wfMsgForContent( 'helppage' ),
+ wfMsg( 'help' ) );
+
+ return $s;
+ }
+
+ function getSearchLink() {
+ $searchPage =& Title::makeTitle( NS_SPECIAL, 'Search' );
+ return $searchPage->getLocalURL();
+ }
+
+ function escapeSearchLink() {
+ return htmlspecialchars( $this->getSearchLink() );
+ }
+
+ function searchForm() {
+ global $wgRequest;
+ $search = $wgRequest->getText( 'search' );
+
+ $s = '<form name="search" class="inline" method="post" action="'
+ . $this->escapeSearchLink() . "\">\n"
+ . '<input type="text" name="search" size="19" value="'
+ . htmlspecialchars(substr($search,0,256)) . "\" />\n"
+ . '<input type="submit" name="go" value="' . wfMsg ('go') . '" />&nbsp;'
+ . '<input type="submit" name="fulltext" value="' . wfMsg ('search') . "\" />\n</form>";
+
+ return $s;
+ }
+
+ function topLinks() {
+ global $wgOut;
+ $sep = " |\n";
+
+ $s = $this->mainPageLink() . $sep
+ . $this->specialLink( 'recentchanges' );
+
+ if ( $wgOut->isArticleRelated() ) {
+ $s .= $sep . $this->editThisPage()
+ . $sep . $this->historyLink();
+ }
+ # Many people don't like this dropdown box
+ #$s .= $sep . $this->specialPagesList();
+
+ /* show links to different language variants */
+ global $wgDisableLangConversion, $wgContLang, $wgTitle;
+ $variants = $wgContLang->getVariants();
+ if( !$wgDisableLangConversion && sizeof( $variants ) > 1 ) {
+ foreach( $variants as $code ) {
+ $varname = $wgContLang->getVariantname( $code );
+ if( $varname == 'disable' )
+ continue;
+ $s .= ' | <a href="' . $wgTitle->getLocalUrl( 'variant=' . $code ) . '">' . $varname . '</a>';
+ }
+ }
+
+ return $s;
+ }
+
+ function bottomLinks() {
+ global $wgOut, $wgUser, $wgTitle, $wgUseTrackbacks;
+ $sep = " |\n";
+
+ $s = '';
+ if ( $wgOut->isArticleRelated() ) {
+ $s .= '<strong>' . $this->editThisPage() . '</strong>';
+ if ( $wgUser->isLoggedIn() ) {
+ $s .= $sep . $this->watchThisPage();
+ }
+ $s .= $sep . $this->talkLink()
+ . $sep . $this->historyLink()
+ . $sep . $this->whatLinksHere()
+ . $sep . $this->watchPageLinksLink();
+
+ if ($wgUseTrackbacks)
+ $s .= $sep . $this->trackbackLink();
+
+ if ( $wgTitle->getNamespace() == NS_USER
+ || $wgTitle->getNamespace() == NS_USER_TALK )
+
+ {
+ $id=User::idFromName($wgTitle->getText());
+ $ip=User::isIP($wgTitle->getText());
+
+ if($id || $ip) { # both anons and non-anons have contri list
+ $s .= $sep . $this->userContribsLink();
+ }
+ if( $this->showEmailUser( $id ) ) {
+ $s .= $sep . $this->emailUserLink();
+ }
+ }
+ if ( $wgTitle->getArticleId() ) {
+ $s .= "\n<br />";
+ if($wgUser->isAllowed('delete')) { $s .= $this->deleteThisPage(); }
+ if($wgUser->isAllowed('protect')) { $s .= $sep . $this->protectThisPage(); }
+ if($wgUser->isAllowed('move')) { $s .= $sep . $this->moveThisPage(); }
+ }
+ $s .= "<br />\n" . $this->otherLanguages();
+ }
+ return $s;
+ }
+
+ function pageStats() {
+ global $wgOut, $wgLang, $wgArticle, $wgRequest, $wgUser;
+ global $wgDisableCounters, $wgMaxCredits, $wgShowCreditsIfMax, $wgTitle, $wgPageShowWatchingUsers;
+
+ extract( $wgRequest->getValues( 'oldid', 'diff' ) );
+ if ( ! $wgOut->isArticle() ) { return ''; }
+ if ( isset( $oldid ) || isset( $diff ) ) { return ''; }
+ if ( 0 == $wgArticle->getID() ) { return ''; }
+
+ $s = '';
+ if ( !$wgDisableCounters ) {
+ $count = $wgLang->formatNum( $wgArticle->getCount() );
+ if ( $count ) {
+ $s = wfMsgExt( 'viewcount', array( 'parseinline' ), $count );
+ }
+ }
+
+ if (isset($wgMaxCredits) && $wgMaxCredits != 0) {
+ require_once('Credits.php');
+ $s .= ' ' . getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax);
+ } else {
+ $s .= $this->lastModified();
+ }
+
+ if ($wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'watchlist' ) );
+ $sql = "SELECT COUNT(*) AS n FROM $watchlist
+ WHERE wl_title='" . $dbr->strencode($wgTitle->getDBKey()) .
+ "' AND wl_namespace=" . $wgTitle->getNamespace() ;
+ $res = $dbr->query( $sql, 'Skin::pageStats');
+ $x = $dbr->fetchObject( $res );
+ $s .= ' ' . wfMsg('number_of_watching_users_pageview', $x->n );
+ }
+
+ return $s . ' ' . $this->getCopyright();
+ }
+
+ function getCopyright( $type = 'detect' ) {
+ global $wgRightsPage, $wgRightsUrl, $wgRightsText, $wgRequest;
+
+ if ( $type == 'detect' ) {
+ $oldid = $wgRequest->getVal( 'oldid' );
+ $diff = $wgRequest->getVal( 'diff' );
+
+ if ( !is_null( $oldid ) && is_null( $diff ) && wfMsgForContent( 'history_copyright' ) !== '-' ) {
+ $type = 'history';
+ } else {
+ $type = 'normal';
+ }
+ }
+
+ if ( $type == 'history' ) {
+ $msg = 'history_copyright';
+ } else {
+ $msg = 'copyright';
+ }
+
+ $out = '';
+ if( $wgRightsPage ) {
+ $link = $this->makeKnownLink( $wgRightsPage, $wgRightsText );
+ } elseif( $wgRightsUrl ) {
+ $link = $this->makeExternalLink( $wgRightsUrl, $wgRightsText );
+ } else {
+ # Give up now
+ return $out;
+ }
+ $out .= wfMsgForContent( $msg, $link );
+ return $out;
+ }
+
+ function getCopyrightIcon() {
+ global $wgRightsUrl, $wgRightsText, $wgRightsIcon, $wgCopyrightIcon;
+ $out = '';
+ if ( isset( $wgCopyrightIcon ) && $wgCopyrightIcon ) {
+ $out = $wgCopyrightIcon;
+ } else if ( $wgRightsIcon ) {
+ $icon = htmlspecialchars( $wgRightsIcon );
+ if ( $wgRightsUrl ) {
+ $url = htmlspecialchars( $wgRightsUrl );
+ $out .= '<a href="'.$url.'">';
+ }
+ $text = htmlspecialchars( $wgRightsText );
+ $out .= "<img src=\"$icon\" alt='$text' />";
+ if ( $wgRightsUrl ) {
+ $out .= '</a>';
+ }
+ }
+ return $out;
+ }
+
+ function getPoweredBy() {
+ global $wgStylePath;
+ $url = htmlspecialchars( "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" );
+ $img = '<a href="http://www.mediawiki.org/"><img src="'.$url.'" alt="MediaWiki" /></a>';
+ return $img;
+ }
+
+ function lastModified() {
+ global $wgLang, $wgArticle, $wgLoadBalancer;
+
+ $timestamp = $wgArticle->getTimestamp();
+ if ( $timestamp ) {
+ $d = $wgLang->timeanddate( $timestamp, true );
+ $s = ' ' . wfMsg( 'lastmodified', $d );
+ } else {
+ $s = '';
+ }
+ if ( $wgLoadBalancer->getLaggedSlaveMode() ) {
+ $s .= ' <strong>' . wfMsg( 'laggedslavemode' ) . '</strong>';
+ }
+ return $s;
+ }
+
+ function logoText( $align = '' ) {
+ if ( '' != $align ) { $a = " align='{$align}'"; }
+ else { $a = ''; }
+
+ $mp = wfMsg( 'mainpage' );
+ $titleObj = Title::newFromText( $mp );
+ if ( is_object( $titleObj ) ) {
+ $url = $titleObj->escapeLocalURL();
+ } else {
+ $url = '';
+ }
+
+ $logourl = $this->getLogo();
+ $s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
+ return $s;
+ }
+
+ /**
+ * show a drop-down box of special pages
+ * @TODO crash bug913. Need to be rewrote completly.
+ */
+ function specialPagesList() {
+ global $wgUser, $wgContLang, $wgServer, $wgRedirectScript, $wgAvailableRights;
+ require_once('SpecialPage.php');
+ $a = array();
+ $pages = SpecialPage::getPages();
+
+ // special pages without access restriction
+ foreach ( $pages[''] as $name => $page ) {
+ $a[$name] = $page->getDescription();
+ }
+
+ // Other special pages that are restricted.
+ // Copied from SpecialSpecialpages.php
+ foreach($wgAvailableRights as $right) {
+ if( $wgUser->isAllowed($right) ) {
+ /** Add all pages for this right */
+ if(isset($pages[$right])) {
+ foreach($pages[$right] as $name => $page) {
+ $a[$name] = $page->getDescription();
+ }
+ }
+ }
+ }
+
+ $go = wfMsg( 'go' );
+ $sp = wfMsg( 'specialpages' );
+ $spp = $wgContLang->specialPage( 'Specialpages' );
+
+ $s = '<form id="specialpages" method="get" class="inline" ' .
+ 'action="' . htmlspecialchars( "{$wgServer}{$wgRedirectScript}" ) . "\">\n";
+ $s .= "<select name=\"wpDropdown\">\n";
+ $s .= "<option value=\"{$spp}\">{$sp}</option>\n";
+
+
+ foreach ( $a as $name => $desc ) {
+ $p = $wgContLang->specialPage( $name );
+ $s .= "<option value=\"{$p}\">{$desc}</option>\n";
+ }
+ $s .= "</select>\n";
+ $s .= "<input type='submit' value=\"{$go}\" name='redirect' />\n";
+ $s .= "</form>\n";
+ return $s;
+ }
+
+ function mainPageLink() {
+ $mp = wfMsgForContent( 'mainpage' );
+ $mptxt = wfMsg( 'mainpage');
+ $s = $this->makeKnownLink( $mp, $mptxt );
+ return $s;
+ }
+
+ function copyrightLink() {
+ $s = $this->makeKnownLink( wfMsgForContent( 'copyrightpage' ),
+ wfMsg( 'copyrightpagename' ) );
+ return $s;
+ }
+
+ function privacyLink() {
+ $privacy = wfMsg( 'privacy' );
+ if ($privacy == '-') {
+ return '';
+ } else {
+ return $this->makeKnownLink( wfMsgForContent( 'privacypage' ), $privacy);
+ }
+ }
+
+ function aboutLink() {
+ $s = $this->makeKnownLink( wfMsgForContent( 'aboutpage' ),
+ wfMsg( 'aboutsite' ) );
+ return $s;
+ }
+
+ function disclaimerLink() {
+ $disclaimers = wfMsg( 'disclaimers' );
+ if ($disclaimers == '-') {
+ return '';
+ } else {
+ return $this->makeKnownLink( wfMsgForContent( 'disclaimerpage' ),
+ $disclaimers );
+ }
+ }
+
+ function editThisPage() {
+ global $wgOut, $wgTitle;
+
+ if ( ! $wgOut->isArticleRelated() ) {
+ $s = wfMsg( 'protectedpage' );
+ } else {
+ if ( $wgTitle->userCanEdit() ) {
+ $t = wfMsg( 'editthispage' );
+ } else {
+ $t = wfMsg( 'viewsource' );
+ }
+
+ $s = $this->makeKnownLinkObj( $wgTitle, $t, $this->editUrlOptions() );
+ }
+ return $s;
+ }
+
+ /**
+ * Return URL options for the 'edit page' link.
+ * This may include an 'oldid' specifier, if the current page view is such.
+ *
+ * @return string
+ * @private
+ */
+ function editUrlOptions() {
+ global $wgArticle;
+
+ if( $this->mRevisionId && ! $wgArticle->isCurrent() ) {
+ return "action=edit&oldid=" . intval( $this->mRevisionId );
+ } else {
+ return "action=edit";
+ }
+ }
+
+ function deleteThisPage() {
+ global $wgUser, $wgTitle, $wgRequest;
+
+ $diff = $wgRequest->getVal( 'diff' );
+ if ( $wgTitle->getArticleId() && ( ! $diff ) && $wgUser->isAllowed('delete') ) {
+ $t = wfMsg( 'deletethispage' );
+
+ $s = $this->makeKnownLinkObj( $wgTitle, $t, 'action=delete' );
+ } else {
+ $s = '';
+ }
+ return $s;
+ }
+
+ function protectThisPage() {
+ global $wgUser, $wgTitle, $wgRequest;
+
+ $diff = $wgRequest->getVal( 'diff' );
+ if ( $wgTitle->getArticleId() && ( ! $diff ) && $wgUser->isAllowed('protect') ) {
+ if ( $wgTitle->isProtected() ) {
+ $t = wfMsg( 'unprotectthispage' );
+ $q = 'action=unprotect';
+ } else {
+ $t = wfMsg( 'protectthispage' );
+ $q = 'action=protect';
+ }
+ $s = $this->makeKnownLinkObj( $wgTitle, $t, $q );
+ } else {
+ $s = '';
+ }
+ return $s;
+ }
+
+ function watchThisPage() {
+ global $wgOut, $wgTitle;
+
+ if ( $wgOut->isArticleRelated() ) {
+ if ( $wgTitle->userIsWatching() ) {
+ $t = wfMsg( 'unwatchthispage' );
+ $q = 'action=unwatch';
+ } else {
+ $t = wfMsg( 'watchthispage' );
+ $q = 'action=watch';
+ }
+ $s = $this->makeKnownLinkObj( $wgTitle, $t, $q );
+ } else {
+ $s = wfMsg( 'notanarticle' );
+ }
+ return $s;
+ }
+
+ function moveThisPage() {
+ global $wgTitle;
+
+ if ( $wgTitle->userCanMove() ) {
+ return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Movepage' ),
+ wfMsg( 'movethispage' ), 'target=' . $wgTitle->getPrefixedURL() );
+ } else {
+ // no message if page is protected - would be redundant
+ return '';
+ }
+ }
+
+ function historyLink() {
+ global $wgTitle;
+
+ return $this->makeKnownLinkObj( $wgTitle,
+ wfMsg( 'history' ), 'action=history' );
+ }
+
+ function whatLinksHere() {
+ global $wgTitle;
+
+ return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ),
+ wfMsg( 'whatlinkshere' ), 'target=' . $wgTitle->getPrefixedURL() );
+ }
+
+ function userContribsLink() {
+ global $wgTitle;
+
+ return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ),
+ wfMsg( 'contributions' ), 'target=' . $wgTitle->getPartialURL() );
+ }
+
+ function showEmailUser( $id ) {
+ global $wgEnableEmail, $wgEnableUserEmail, $wgUser;
+ return $wgEnableEmail &&
+ $wgEnableUserEmail &&
+ $wgUser->isLoggedIn() && # show only to signed in users
+ 0 != $id; # we can only email to non-anons ..
+# '' != $id->getEmail() && # who must have an email address stored ..
+# 0 != $id->getEmailauthenticationtimestamp() && # .. which is authenticated
+# 1 != $wgUser->getOption('disablemail'); # and not disabled
+ }
+
+ function emailUserLink() {
+ global $wgTitle;
+
+ return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Emailuser' ),
+ wfMsg( 'emailuser' ), 'target=' . $wgTitle->getPartialURL() );
+ }
+
+ function watchPageLinksLink() {
+ global $wgOut, $wgTitle;
+
+ if ( ! $wgOut->isArticleRelated() ) {
+ return '(' . wfMsg( 'notanarticle' ) . ')';
+ } else {
+ return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL,
+ 'Recentchangeslinked' ), wfMsg( 'recentchangeslinked' ),
+ 'target=' . $wgTitle->getPrefixedURL() );
+ }
+ }
+
+ function trackbackLink() {
+ global $wgTitle;
+
+ return "<a href=\"" . $wgTitle->trackbackURL() . "\">"
+ . wfMsg('trackbacklink') . "</a>";
+ }
+
+ function otherLanguages() {
+ global $wgOut, $wgContLang, $wgHideInterlanguageLinks;
+
+ if ( $wgHideInterlanguageLinks ) {
+ return '';
+ }
+
+ $a = $wgOut->getLanguageLinks();
+ if ( 0 == count( $a ) ) {
+ return '';
+ }
+
+ $s = wfMsg( 'otherlanguages' ) . ': ';
+ $first = true;
+ if($wgContLang->isRTL()) $s .= '<span dir="LTR">';
+ foreach( $a as $l ) {
+ if ( ! $first ) { $s .= ' | '; }
+ $first = false;
+
+ $nt = Title::newFromText( $l );
+ $url = $nt->escapeFullURL();
+ $text = $wgContLang->getLanguageName( $nt->getInterwiki() );
+
+ if ( '' == $text ) { $text = $l; }
+ $style = $this->getExternalLinkAttributes( $l, $text );
+ $s .= "<a href=\"{$url}\"{$style}>{$text}</a>";
+ }
+ if($wgContLang->isRTL()) $s .= '</span>';
+ return $s;
+ }
+
+ function bugReportsLink() {
+ $s = $this->makeKnownLink( wfMsgForContent( 'bugreportspage' ),
+ wfMsg( 'bugreports' ) );
+ return $s;
+ }
+
+ function dateLink() {
+ $t1 = Title::newFromText( gmdate( 'F j' ) );
+ $t2 = Title::newFromText( gmdate( 'Y' ) );
+
+ $id = $t1->getArticleID();
+
+ if ( 0 == $id ) {
+ $s = $this->makeBrokenLink( $t1->getText() );
+ } else {
+ $s = $this->makeKnownLink( $t1->getText() );
+ }
+ $s .= ', ';
+
+ $id = $t2->getArticleID();
+
+ if ( 0 == $id ) {
+ $s .= $this->makeBrokenLink( $t2->getText() );
+ } else {
+ $s .= $this->makeKnownLink( $t2->getText() );
+ }
+ return $s;
+ }
+
+ function talkLink() {
+ global $wgTitle;
+
+ if ( NS_SPECIAL == $wgTitle->getNamespace() ) {
+ # No discussion links for special pages
+ return '';
+ }
+
+ if( $wgTitle->isTalkPage() ) {
+ $link = $wgTitle->getSubjectPage();
+ switch( $link->getNamespace() ) {
+ case NS_MAIN:
+ $text = wfMsg('articlepage');
+ break;
+ case NS_USER:
+ $text = wfMsg('userpage');
+ break;
+ case NS_PROJECT:
+ $text = wfMsg('projectpage');
+ break;
+ case NS_IMAGE:
+ $text = wfMsg('imagepage');
+ break;
+ default:
+ $text= wfMsg('articlepage');
+ }
+ } else {
+ $link = $wgTitle->getTalkPage();
+ $text = wfMsg( 'talkpage' );
+ }
+
+ $s = $this->makeLinkObj( $link, $text );
+
+ return $s;
+ }
+
+ function commentLink() {
+ global $wgTitle, $wgOut;
+
+ if ( $wgTitle->getNamespace() == NS_SPECIAL ) {
+ return '';
+ }
+
+ # __NEWSECTIONLINK___ changes behaviour here
+ # If it's present, the link points to this page, otherwise
+ # it points to the talk page
+ if( $wgTitle->isTalkPage() ) {
+ $title =& $wgTitle;
+ } elseif( $wgOut->showNewSectionLink() ) {
+ $title =& $wgTitle;
+ } else {
+ $title =& $wgTitle->getTalkPage();
+ }
+
+ return $this->makeKnownLinkObj( $title, wfMsg( 'postcomment' ), 'action=edit&section=new' );
+ }
+
+ /* these are used extensively in SkinTemplate, but also some other places */
+ /*static*/ function makeSpecialUrl( $name, $urlaction='' ) {
+ $title = Title::makeTitle( NS_SPECIAL, $name );
+ return $title->getLocalURL( $urlaction );
+ }
+
+ /*static*/ function makeI18nUrl ( $name, $urlaction='' ) {
+ $title = Title::newFromText( wfMsgForContent($name) );
+ $this->checkTitle($title, $name);
+ return $title->getLocalURL( $urlaction );
+ }
+
+ /*static*/ function makeUrl ( $name, $urlaction='' ) {
+ $title = Title::newFromText( $name );
+ $this->checkTitle($title, $name);
+ return $title->getLocalURL( $urlaction );
+ }
+
+ # If url string starts with http, consider as external URL, else
+ # internal
+ /*static*/ function makeInternalOrExternalUrl( $name ) {
+ if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $name ) ) {
+ return $name;
+ } else {
+ return $this->makeUrl( $name );
+ }
+ }
+
+ # this can be passed the NS number as defined in Language.php
+ /*static*/ function makeNSUrl( $name, $urlaction='', $namespace=NS_MAIN ) {
+ $title = Title::makeTitleSafe( $namespace, $name );
+ $this->checkTitle($title, $name);
+ return $title->getLocalURL( $urlaction );
+ }
+
+ /* these return an array with the 'href' and boolean 'exists' */
+ /*static*/ function makeUrlDetails ( $name, $urlaction='' ) {
+ $title = Title::newFromText( $name );
+ $this->checkTitle($title, $name);
+ return array(
+ 'href' => $title->getLocalURL( $urlaction ),
+ 'exists' => $title->getArticleID() != 0?true:false
+ );
+ }
+
+ /**
+ * Make URL details where the article exists (or at least it's convenient to think so)
+ */
+ function makeKnownUrlDetails( $name, $urlaction='' ) {
+ $title = Title::newFromText( $name );
+ $this->checkTitle($title, $name);
+ return array(
+ 'href' => $title->getLocalURL( $urlaction ),
+ 'exists' => true
+ );
+ }
+
+ # make sure we have some title to operate on
+ /*static*/ function checkTitle ( &$title, &$name ) {
+ if(!is_object($title)) {
+ $title = Title::newFromText( $name );
+ if(!is_object($title)) {
+ $title = Title::newFromText( '--error: link target missing--' );
+ }
+ }
+ }
+
+ /**
+ * Build an array that represents the sidebar(s), the navigation bar among them
+ *
+ * @return array
+ * @private
+ */
+ function buildSidebar() {
+ global $wgDBname, $parserMemc, $wgEnableSidebarCache;
+ global $wgLanguageCode, $wgContLanguageCode;
+
+ $fname = 'SkinTemplate::buildSidebar';
+
+ wfProfileIn( $fname );
+
+ $key = "{$wgDBname}:sidebar";
+ $cacheSidebar = $wgEnableSidebarCache &&
+ ($wgLanguageCode == $wgContLanguageCode);
+
+ if ($cacheSidebar) {
+ $cachedsidebar = $parserMemc->get( $key );
+ if ($cachedsidebar!="") {
+ wfProfileOut($fname);
+ return $cachedsidebar;
+ }
+ }
+
+ $bar = array();
+ $lines = explode( "\n", wfMsgForContent( 'sidebar' ) );
+ foreach ($lines as $line) {
+ if (strpos($line, '*') !== 0)
+ continue;
+ if (strpos($line, '**') !== 0) {
+ $line = trim($line, '* ');
+ $heading = $line;
+ } else {
+ if (strpos($line, '|') !== false) { // sanity check
+ $line = explode( '|' , trim($line, '* '), 2 );
+ $link = wfMsgForContent( $line[0] );
+ if ($link == '-')
+ continue;
+ if (wfEmptyMsg($line[1], $text = wfMsg($line[1])))
+ $text = $line[1];
+ if (wfEmptyMsg($line[0], $link))
+ $link = $line[0];
+ $href = $this->makeInternalOrExternalUrl( $link );
+ $bar[$heading][] = array(
+ 'text' => $text,
+ 'href' => $href,
+ 'id' => 'n-' . strtr($line[1], ' ', '-'),
+ 'active' => false
+ );
+ } else { continue; }
+ }
+ }
+ if ($cacheSidebar)
+ $cachednotice = $parserMemc->set( $key, $bar, 86400 );
+ wfProfileOut( $fname );
+ return $bar;
+ }
+}
+?>
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
new file mode 100644
index 00000000..6657d381
--- /dev/null
+++ b/includes/SkinTemplate.php
@@ -0,0 +1,1109 @@
+<?php
+if ( ! defined( 'MEDIAWIKI' ) )
+ die( 1 );
+
+# 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
+
+/**
+ * Template-filler skin base class
+ * Formerly generic PHPTal (http://phptal.sourceforge.net/) skin
+ * Based on Brion's smarty skin
+ * Copyright (C) Gabriel Wicke -- http://www.aulinx.de/
+ *
+ * Todo: Needs some serious refactoring into functions that correspond
+ * to the computations individual esi snippets need. Most importantly no body
+ * parsing for most of those of course.
+ *
+ * @package MediaWiki
+ * @subpackage Skins
+ */
+
+require_once 'GlobalFunctions.php';
+
+/**
+ * Wrapper object for MediaWiki's localization functions,
+ * to be passed to the template engine.
+ *
+ * @private
+ * @package MediaWiki
+ */
+class MediaWiki_I18N {
+ var $_context = array();
+
+ function set($varName, $value) {
+ $this->_context[$varName] = $value;
+ }
+
+ function translate($value) {
+ $fname = 'SkinTemplate-translate';
+ wfProfileIn( $fname );
+
+ // Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23
+ $value = preg_replace( '/^string:/', '', $value );
+
+ $value = wfMsg( $value );
+ // interpolate variables
+ while (preg_match('/\$([0-9]*?)/sm', $value, $m)) {
+ list($src, $var) = $m;
+ wfSuppressWarnings();
+ $varValue = $this->_context[$var];
+ wfRestoreWarnings();
+ $value = str_replace($src, $varValue, $value);
+ }
+ wfProfileOut( $fname );
+ return $value;
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ */
+class SkinTemplate extends Skin {
+ /**#@+
+ * @private
+ */
+
+ /**
+ * Name of our skin, set in initPage()
+ * It probably need to be all lower case.
+ */
+ var $skinname;
+
+ /**
+ * Stylesheets set to use
+ * Sub directory in ./skins/ where various stylesheets are located
+ */
+ var $stylename;
+
+ /**
+ * For QuickTemplate, the name of the subclass which
+ * will actually fill the template.
+ */
+ var $template;
+
+ /**#@-*/
+
+ /**
+ * Setup the base parameters...
+ * Child classes should override this to set the name,
+ * style subdirectory, and template filler callback.
+ *
+ * @param OutputPage $out
+ */
+ function initPage( &$out ) {
+ parent::initPage( $out );
+ $this->skinname = 'monobook';
+ $this->stylename = 'monobook';
+ $this->template = 'QuickTemplate';
+ }
+
+ /**
+ * Create the template engine object; we feed it a bunch of data
+ * and eventually it spits out some HTML. Should have interface
+ * roughly equivalent to PHPTAL 0.7.
+ *
+ * @param string $callback (or file)
+ * @param string $repository subdirectory where we keep template files
+ * @param string $cache_dir
+ * @return object
+ * @private
+ */
+ function setupTemplate( $classname, $repository=false, $cache_dir=false ) {
+ return new $classname();
+ }
+
+ /**
+ * initialize various variables and generate the template
+ *
+ * @param OutputPage $out
+ * @public
+ */
+ function outputPage( &$out ) {
+ global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang, $wgOut;
+ global $wgScript, $wgStylePath, $wgContLanguageCode;
+ global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest;
+ global $wgDisableCounters, $wgLogo, $action, $wgFeedClasses, $wgHideInterlanguageLinks;
+ global $wgMaxCredits, $wgShowCreditsIfMax;
+ global $wgPageShowWatchingUsers;
+ global $wgUseTrackbacks;
+ global $wgDBname;
+
+ $fname = 'SkinTemplate::outputPage';
+ wfProfileIn( $fname );
+
+ // Hook that allows last minute changes to the output page, e.g.
+ // adding of CSS or Javascript by extensions.
+ wfRunHooks( 'BeforePageDisplay', array( &$out ) );
+
+ extract( $wgRequest->getValues( 'oldid', 'diff' ) );
+
+ wfProfileIn( "$fname-init" );
+ $this->initPage( $out );
+
+ $this->mTitle =& $wgTitle;
+ $this->mUser =& $wgUser;
+
+ $tpl = $this->setupTemplate( $this->template, 'skins' );
+
+ #if ( $wgUseDatabaseMessages ) { // uncomment this to fall back to GetText
+ $tpl->setTranslator(new MediaWiki_I18N());
+ #}
+ wfProfileOut( "$fname-init" );
+
+ wfProfileIn( "$fname-stuff" );
+ $this->thispage = $this->mTitle->getPrefixedDbKey();
+ $this->thisurl = $this->mTitle->getPrefixedURL();
+ $this->loggedin = $wgUser->isLoggedIn();
+ $this->iscontent = ($this->mTitle->getNamespace() != NS_SPECIAL );
+ $this->iseditable = ($this->iscontent and !($action == 'edit' or $action == 'submit'));
+ $this->username = $wgUser->getName();
+ $userPage = $wgUser->getUserPage();
+ $this->userpage = $userPage->getPrefixedText();
+
+ if ( $wgUser->isLoggedIn() || $this->showIPinHeader() ) {
+ $this->userpageUrlDetails = $this->makeUrlDetails($this->userpage);
+ } else {
+ # This won't be used in the standard skins, but we define it to preserve the interface
+ # To save time, we check for existence
+ $this->userpageUrlDetails = $this->makeKnownUrlDetails($this->userpage);
+ }
+
+ $this->usercss = $this->userjs = $this->userjsprev = false;
+ $this->setupUserCss();
+ $this->setupUserJs();
+ $this->titletxt = $this->mTitle->getPrefixedText();
+ wfProfileOut( "$fname-stuff" );
+
+ wfProfileIn( "$fname-stuff2" );
+ $tpl->set( 'title', $wgOut->getPageTitle() );
+ $tpl->set( 'pagetitle', $wgOut->getHTMLTitle() );
+ $tpl->set( 'displaytitle', $wgOut->mPageLinkTitle );
+
+ $tpl->setRef( "thispage", $this->thispage );
+ $subpagestr = $this->subPageSubtitle();
+ $tpl->set(
+ 'subtitle', !empty($subpagestr)?
+ '<span class="subpages">'.$subpagestr.'</span>'.$out->getSubtitle():
+ $out->getSubtitle()
+ );
+ $undelete = $this->getUndeleteLink();
+ $tpl->set(
+ "undelete", !empty($undelete)?
+ '<span class="subpages">'.$undelete.'</span>':
+ ''
+ );
+
+ $tpl->set( 'catlinks', $this->getCategories());
+ if( $wgOut->isSyndicated() ) {
+ $feeds = array();
+ foreach( $wgFeedClasses as $format => $class ) {
+ $feeds[$format] = array(
+ 'text' => $format,
+ 'href' => $wgRequest->appendQuery( "feed=$format" )
+ );
+ }
+ $tpl->setRef( 'feeds', $feeds );
+ } else {
+ $tpl->set( 'feeds', false );
+ }
+ if ($wgUseTrackbacks && $out->isArticleRelated())
+ $tpl->set( 'trackbackhtml', $wgTitle->trackbackRDF());
+
+ $tpl->setRef( 'mimetype', $wgMimeType );
+ $tpl->setRef( 'jsmimetype', $wgJsMimeType );
+ $tpl->setRef( 'charset', $wgOutputEncoding );
+ $tpl->set( 'headlinks', $out->getHeadLinks() );
+ $tpl->set('headscripts', $out->getScript() );
+ $tpl->setRef( 'wgScript', $wgScript );
+ $tpl->setRef( 'skinname', $this->skinname );
+ $tpl->setRef( 'stylename', $this->stylename );
+ $tpl->set( 'printable', $wgRequest->getBool( 'printable' ) );
+ $tpl->setRef( 'loggedin', $this->loggedin );
+ $tpl->set('nsclass', 'ns-'.$this->mTitle->getNamespace());
+ $tpl->set('notspecialpage', $this->mTitle->getNamespace() != NS_SPECIAL);
+ /* XXX currently unused, might get useful later
+ $tpl->set( "editable", ($this->mTitle->getNamespace() != NS_SPECIAL ) );
+ $tpl->set( "exists", $this->mTitle->getArticleID() != 0 );
+ $tpl->set( "watch", $this->mTitle->userIsWatching() ? "unwatch" : "watch" );
+ $tpl->set( "protect", count($this->mTitle->isProtected()) ? "unprotect" : "protect" );
+ $tpl->set( "helppage", wfMsg('helppage'));
+ */
+ $tpl->set( 'searchaction', $this->escapeSearchLink() );
+ $tpl->set( 'search', trim( $wgRequest->getVal( 'search' ) ) );
+ $tpl->setRef( 'stylepath', $wgStylePath );
+ $tpl->setRef( 'logopath', $wgLogo );
+ $tpl->setRef( "lang", $wgContLanguageCode );
+ $tpl->set( 'dir', $wgContLang->isRTL() ? "rtl" : "ltr" );
+ $tpl->set( 'rtl', $wgContLang->isRTL() );
+ $tpl->set( 'langname', $wgContLang->getLanguageName( $wgContLanguageCode ) );
+ $tpl->set( 'showjumplinks', $wgUser->getOption( 'showjumplinks' ) );
+ $tpl->setRef( 'username', $this->username );
+ $tpl->setRef( 'userpage', $this->userpage);
+ $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href']);
+ $tpl->set( 'pagecss', $this->setupPageCss() );
+ $tpl->setRef( 'usercss', $this->usercss);
+ $tpl->setRef( 'userjs', $this->userjs);
+ $tpl->setRef( 'userjsprev', $this->userjsprev);
+ global $wgUseSiteJs;
+ if ($wgUseSiteJs) {
+ if($this->loggedin) {
+ $tpl->set( 'jsvarurl', $this->makeUrl('-','action=raw&smaxage=0&gen=js') );
+ } else {
+ $tpl->set( 'jsvarurl', $this->makeUrl('-','action=raw&gen=js') );
+ }
+ } else {
+ $tpl->set('jsvarurl', false);
+ }
+ $newtalks = $wgUser->getNewMessageLinks();
+
+ if (count($newtalks) == 1 && $newtalks[0]["wiki"] === $wgDBname) {
+ $usertitle = $this->mUser->getUserPage();
+ $usertalktitle = $usertitle->getTalkPage();
+ if( !$usertalktitle->equals( $this->mTitle ) ) {
+ $ntl = wfMsg( 'youhavenewmessages',
+ $this->makeKnownLinkObj(
+ $usertalktitle,
+ wfMsgHtml( 'newmessageslink' ),
+ 'redirect=no'
+ ),
+ $this->makeKnownLinkObj(
+ $usertalktitle,
+ wfMsgHtml( 'newmessagesdifflink' ),
+ 'diff=cur'
+ )
+ );
+ # Disable Cache
+ $wgOut->setSquidMaxage(0);
+ }
+ } else if (count($newtalks)) {
+ $sep = str_replace("_", " ", wfMsgHtml("newtalkseperator"));
+ $msgs = array();
+ foreach ($newtalks as $newtalk) {
+ $msgs[] = wfElement("a",
+ array('href' => $newtalk["link"]), $newtalk["wiki"]);
+ }
+ $parts = implode($sep, $msgs);
+ $ntl = wfMsgHtml('youhavenewmessagesmulti', $parts);
+ $wgOut->setSquidMaxage(0);
+ } else {
+ $ntl = '';
+ }
+ wfProfileOut( "$fname-stuff2" );
+
+ wfProfileIn( "$fname-stuff3" );
+ $tpl->setRef( 'newtalk', $ntl );
+ $tpl->setRef( 'skin', $this);
+ $tpl->set( 'logo', $this->logoText() );
+ if ( $wgOut->isArticle() and (!isset( $oldid ) or isset( $diff )) and 0 != $wgArticle->getID() ) {
+ if ( !$wgDisableCounters ) {
+ $viewcount = $wgLang->formatNum( $wgArticle->getCount() );
+ if ( $viewcount ) {
+ $tpl->set('viewcount', wfMsgExt( 'viewcount', array( 'parseinline' ), $viewcount ) );
+ } else {
+ $tpl->set('viewcount', false);
+ }
+ } else {
+ $tpl->set('viewcount', false);
+ }
+
+ if ($wgPageShowWatchingUsers) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'watchlist' ) );
+ $sql = "SELECT COUNT(*) AS n FROM $watchlist
+ WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBKey()) .
+ "' AND wl_namespace=" . $this->mTitle->getNamespace() ;
+ $res = $dbr->query( $sql, 'SkinTemplate::outputPage');
+ $x = $dbr->fetchObject( $res );
+ $numberofwatchingusers = $x->n;
+ if ($numberofwatchingusers > 0) {
+ $tpl->set('numberofwatchingusers', wfMsg('number_of_watching_users_pageview', $numberofwatchingusers));
+ } else {
+ $tpl->set('numberofwatchingusers', false);
+ }
+ } else {
+ $tpl->set('numberofwatchingusers', false);
+ }
+
+ $tpl->set('copyright',$this->getCopyright());
+
+ $this->credits = false;
+
+ if (isset($wgMaxCredits) && $wgMaxCredits != 0) {
+ require_once("Credits.php");
+ $this->credits = getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax);
+ } else {
+ $tpl->set('lastmod', $this->lastModified());
+ }
+
+ $tpl->setRef( 'credits', $this->credits );
+
+ } elseif ( isset( $oldid ) && !isset( $diff ) ) {
+ $tpl->set('copyright', $this->getCopyright());
+ $tpl->set('viewcount', false);
+ $tpl->set('lastmod', false);
+ $tpl->set('credits', false);
+ $tpl->set('numberofwatchingusers', false);
+ } else {
+ $tpl->set('copyright', false);
+ $tpl->set('viewcount', false);
+ $tpl->set('lastmod', false);
+ $tpl->set('credits', false);
+ $tpl->set('numberofwatchingusers', false);
+ }
+ wfProfileOut( "$fname-stuff3" );
+
+ wfProfileIn( "$fname-stuff4" );
+ $tpl->set( 'copyrightico', $this->getCopyrightIcon() );
+ $tpl->set( 'poweredbyico', $this->getPoweredBy() );
+ $tpl->set( 'disclaimer', $this->disclaimerLink() );
+ $tpl->set( 'privacy', $this->privacyLink() );
+ $tpl->set( 'about', $this->aboutLink() );
+
+ $tpl->setRef( 'debug', $out->mDebugtext );
+ $tpl->set( 'reporttime', $out->reportTime() );
+ $tpl->set( 'sitenotice', wfGetSiteNotice() );
+
+ $printfooter = "<div class=\"printfooter\">\n" . $this->printSource() . "</div>\n";
+ $out->mBodytext .= $printfooter ;
+ $tpl->setRef( 'bodytext', $out->mBodytext );
+
+ # Language links
+ $language_urls = array();
+
+ if ( !$wgHideInterlanguageLinks ) {
+ foreach( $wgOut->getLanguageLinks() as $l ) {
+ $tmp = explode( ':', $l, 2 );
+ $class = 'interwiki-' . $tmp[0];
+ unset($tmp);
+ $nt = Title::newFromText( $l );
+ $language_urls[] = array(
+ 'href' => $nt->getFullURL(),
+ 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l),
+ 'class' => $class
+ );
+ }
+ }
+ if(count($language_urls)) {
+ $tpl->setRef( 'language_urls', $language_urls);
+ } else {
+ $tpl->set('language_urls', false);
+ }
+ wfProfileOut( "$fname-stuff4" );
+
+ # Personal toolbar
+ $tpl->set('personal_urls', $this->buildPersonalUrls());
+ $content_actions = $this->buildContentActionUrls();
+ $tpl->setRef('content_actions', $content_actions);
+
+ // XXX: attach this from javascript, same with section editing
+ if($this->iseditable && $wgUser->getOption("editondblclick") )
+ {
+ $tpl->set('body_ondblclick', 'document.location = "' .$content_actions['edit']['href'] .'";');
+ } else {
+ $tpl->set('body_ondblclick', false);
+ }
+ if( $this->iseditable && $wgUser->getOption( 'editsectiononrightclick' ) ) {
+ $tpl->set( 'body_onload', 'setupRightClickEdit()' );
+ } else {
+ $tpl->set( 'body_onload', false );
+ }
+ $tpl->set( 'sidebar', $this->buildSidebar() );
+ $tpl->set( 'nav_urls', $this->buildNavUrls() );
+
+ // execute template
+ wfProfileIn( "$fname-execute" );
+ $res = $tpl->execute();
+ wfProfileOut( "$fname-execute" );
+
+ // result may be an error
+ $this->printOrError( $res );
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Output the string, or print error message if it's
+ * an error object of the appropriate type.
+ * For the base class, assume strings all around.
+ *
+ * @param mixed $str
+ * @private
+ */
+ function printOrError( &$str ) {
+ echo $str;
+ }
+
+ /**
+ * build array of urls for personal toolbar
+ * @return array
+ * @private
+ */
+ function buildPersonalUrls() {
+ global $wgTitle, $wgShowIPinHeader;
+
+ $fname = 'SkinTemplate::buildPersonalUrls';
+ $pageurl = $wgTitle->getLocalURL();
+ wfProfileIn( $fname );
+
+ /* set up the default links for the personal toolbar */
+ $personal_urls = array();
+ if ($this->loggedin) {
+ $personal_urls['userpage'] = array(
+ 'text' => $this->username,
+ 'href' => &$this->userpageUrlDetails['href'],
+ 'class' => $this->userpageUrlDetails['exists']?false:'new',
+ 'active' => ( $this->userpageUrlDetails['href'] == $pageurl )
+ );
+ $usertalkUrlDetails = $this->makeTalkUrlDetails($this->userpage);
+ $personal_urls['mytalk'] = array(
+ 'text' => wfMsg('mytalk'),
+ 'href' => &$usertalkUrlDetails['href'],
+ 'class' => $usertalkUrlDetails['exists']?false:'new',
+ 'active' => ( $usertalkUrlDetails['href'] == $pageurl )
+ );
+ $href = $this->makeSpecialUrl('Preferences');
+ $personal_urls['preferences'] = array(
+ 'text' => wfMsg('preferences'),
+ 'href' => $this->makeSpecialUrl('Preferences'),
+ 'active' => ( $href == $pageurl )
+ );
+ $href = $this->makeSpecialUrl('Watchlist');
+ $personal_urls['watchlist'] = array(
+ 'text' => wfMsg('watchlist'),
+ 'href' => $href,
+ 'active' => ( $href == $pageurl )
+ );
+ $href = $this->makeSpecialUrl("Contributions/$this->username");
+ $personal_urls['mycontris'] = array(
+ 'text' => wfMsg('mycontris'),
+ 'href' => $href
+ # FIXME # 'active' => ( $href == $pageurl . '/' . $this->username )
+ );
+ $personal_urls['logout'] = array(
+ 'text' => wfMsg('userlogout'),
+ 'href' => $this->makeSpecialUrl( 'Userlogout',
+ $wgTitle->getNamespace() === NS_SPECIAL && $wgTitle->getText() === 'Preferences' ? '' : "returnto={$this->thisurl}"
+ )
+ );
+ } else {
+ if( $wgShowIPinHeader && isset( $_COOKIE[ini_get("session.name")] ) ) {
+ $href = &$this->userpageUrlDetails['href'];
+ $personal_urls['anonuserpage'] = array(
+ 'text' => $this->username,
+ 'href' => $href,
+ 'class' => $this->userpageUrlDetails['exists']?false:'new',
+ 'active' => ( $pageurl == $href )
+ );
+ $usertalkUrlDetails = $this->makeTalkUrlDetails($this->userpage);
+ $href = &$usertalkUrlDetails['href'];
+ $personal_urls['anontalk'] = array(
+ 'text' => wfMsg('anontalk'),
+ 'href' => $href,
+ 'class' => $usertalkUrlDetails['exists']?false:'new',
+ 'active' => ( $pageurl == $href )
+ );
+ $personal_urls['anonlogin'] = array(
+ 'text' => wfMsg('userlogin'),
+ 'href' => $this->makeSpecialUrl('Userlogin', 'returnto=' . $this->thisurl ),
+ 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() )
+ );
+ } else {
+
+ $personal_urls['login'] = array(
+ 'text' => wfMsg('userlogin'),
+ 'href' => $this->makeSpecialUrl('Userlogin', 'returnto=' . $this->thisurl ),
+ 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() )
+ );
+ }
+ }
+
+ wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$wgTitle ) );
+ wfProfileOut( $fname );
+ return $personal_urls;
+ }
+
+ /**
+ * Returns true if the IP should be shown in the header
+ */
+ function showIPinHeader() {
+ global $wgShowIPinHeader;
+ return $wgShowIPinHeader && isset( $_COOKIE[ini_get("session.name")] );
+ }
+
+ function tabAction( $title, $message, $selected, $query='', $checkEdit=false ) {
+ $classes = array();
+ if( $selected ) {
+ $classes[] = 'selected';
+ }
+ if( $checkEdit && $title->getArticleId() == 0 ) {
+ $classes[] = 'new';
+ $query = 'action=edit';
+ }
+
+ $text = wfMsg( $message );
+ if ( $text == "&lt;$message&gt;" ) {
+ global $wgContLang;
+ $text = $wgContLang->getNsText( Namespace::getSubject( $title->getNamespace() ) );
+ }
+
+ return array(
+ 'class' => implode( ' ', $classes ),
+ 'text' => $text,
+ 'href' => $title->getLocalUrl( $query ) );
+ }
+
+ function makeTalkUrlDetails( $name, $urlaction='' ) {
+ $title = Title::newFromText( $name );
+ $title = $title->getTalkPage();
+ $this->checkTitle($title, $name);
+ return array(
+ 'href' => $title->getLocalURL( $urlaction ),
+ 'exists' => $title->getArticleID() != 0?true:false
+ );
+ }
+
+ function makeArticleUrlDetails( $name, $urlaction='' ) {
+ $title = Title::newFromText( $name );
+ $title= $title->getSubjectPage();
+ $this->checkTitle($title, $name);
+ return array(
+ 'href' => $title->getLocalURL( $urlaction ),
+ 'exists' => $title->getArticleID() != 0?true:false
+ );
+ }
+
+ /**
+ * an array of edit links by default used for the tabs
+ * @return array
+ * @private
+ */
+ function buildContentActionUrls () {
+ global $wgContLang, $wgOut;
+ $fname = 'SkinTemplate::buildContentActionUrls';
+ wfProfileIn( $fname );
+
+ global $wgUser, $wgRequest;
+ $action = $wgRequest->getText( 'action' );
+ $section = $wgRequest->getText( 'section' );
+ $content_actions = array();
+
+ $prevent_active_tabs = false ;
+ wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this , &$prevent_active_tabs ) ) ;
+
+ if( $this->iscontent ) {
+ $subjpage = $this->mTitle->getSubjectPage();
+ $talkpage = $this->mTitle->getTalkPage();
+
+ $nskey = $this->mTitle->getNamespaceKey();
+ $content_actions[$nskey] = $this->tabAction(
+ $subjpage,
+ $nskey,
+ !$this->mTitle->isTalkPage() && !$prevent_active_tabs,
+ '', true);
+
+ $content_actions['talk'] = $this->tabAction(
+ $talkpage,
+ 'talk',
+ $this->mTitle->isTalkPage() && !$prevent_active_tabs,
+ '',
+ true);
+
+ wfProfileIn( "$fname-edit" );
+ if ( $this->mTitle->userCanEdit() && ( $this->mTitle->exists() || $this->mTitle->userCanCreate() ) ) {
+ $istalk = $this->mTitle->isTalkPage();
+ $istalkclass = $istalk?' istalk':'';
+ $content_actions['edit'] = array(
+ 'class' => ((($action == 'edit' or $action == 'submit') and $section != 'new') ? 'selected' : '').$istalkclass,
+ 'text' => wfMsg('edit'),
+ 'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
+ );
+
+ if ( $istalk || $wgOut->showNewSectionLink() ) {
+ $content_actions['addsection'] = array(
+ 'class' => $section == 'new'?'selected':false,
+ 'text' => wfMsg('addsection'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=edit&section=new' )
+ );
+ }
+ } else {
+ $content_actions['viewsource'] = array(
+ 'class' => ($action == 'edit') ? 'selected' : false,
+ 'text' => wfMsg('viewsource'),
+ 'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
+ );
+ }
+ wfProfileOut( "$fname-edit" );
+
+ wfProfileIn( "$fname-live" );
+ if ( $this->mTitle->getArticleId() ) {
+
+ $content_actions['history'] = array(
+ 'class' => ($action == 'history') ? 'selected' : false,
+ 'text' => wfMsg('history_short'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=history')
+ );
+
+ if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) {
+ if(!$this->mTitle->isProtected()){
+ $content_actions['protect'] = array(
+ 'class' => ($action == 'protect') ? 'selected' : false,
+ 'text' => wfMsg('protect'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=protect' )
+ );
+
+ } else {
+ $content_actions['unprotect'] = array(
+ 'class' => ($action == 'unprotect') ? 'selected' : false,
+ 'text' => wfMsg('unprotect'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=unprotect' )
+ );
+ }
+ }
+ if($wgUser->isAllowed('delete')){
+ $content_actions['delete'] = array(
+ 'class' => ($action == 'delete') ? 'selected' : false,
+ 'text' => wfMsg('delete'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=delete' )
+ );
+ }
+ if ( $this->mTitle->userCanMove()) {
+ $moveTitle = Title::makeTitle( NS_SPECIAL, 'Movepage' );
+ $content_actions['move'] = array(
+ 'class' => ($this->mTitle->getDbKey() == 'Movepage' and $this->mTitle->getNamespace == NS_SPECIAL) ? 'selected' : false,
+ 'text' => wfMsg('move'),
+ 'href' => $moveTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
+ );
+ }
+ } else {
+ //article doesn't exist or is deleted
+ if( $wgUser->isAllowed( 'delete' ) ) {
+ if( $n = $this->mTitle->isDeleted() ) {
+ $undelTitle = Title::makeTitle( NS_SPECIAL, 'Undelete' );
+ $content_actions['undelete'] = array(
+ 'class' => false,
+ 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $n ),
+ 'href' => $undelTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
+ #'href' => $this->makeSpecialUrl("Undelete/$this->thispage")
+ );
+ }
+ }
+ }
+ wfProfileOut( "$fname-live" );
+
+ if( $this->loggedin ) {
+ if( !$this->mTitle->userIsWatching()) {
+ $content_actions['watch'] = array(
+ 'class' => ($action == 'watch' or $action == 'unwatch') ? 'selected' : false,
+ 'text' => wfMsg('watch'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=watch' )
+ );
+ } else {
+ $content_actions['unwatch'] = array(
+ 'class' => ($action == 'unwatch' or $action == 'watch') ? 'selected' : false,
+ 'text' => wfMsg('unwatch'),
+ 'href' => $this->mTitle->getLocalUrl( 'action=unwatch' )
+ );
+ }
+ }
+
+ wfRunHooks( 'SkinTemplateTabs', array( &$this , &$content_actions ) ) ;
+ } else {
+ /* show special page tab */
+
+ $content_actions['article'] = array(
+ 'class' => 'selected',
+ 'text' => wfMsg('specialpage'),
+ 'href' => $wgRequest->getRequestURL(), // @bug 2457, 2510
+ );
+
+ wfRunHooks( 'SkinTemplateBuildContentActionUrlsAfterSpecialPage', array( &$this, &$content_actions ) );
+ }
+
+ /* show links to different language variants */
+ global $wgDisableLangConversion;
+ $variants = $wgContLang->getVariants();
+ if( !$wgDisableLangConversion && sizeof( $variants ) > 1 ) {
+ $preferred = $wgContLang->getPreferredVariant();
+ $actstr = '';
+ if( $action )
+ $actstr = 'action=' . $action . '&';
+ $vcount=0;
+ foreach( $variants as $code ) {
+ $varname = $wgContLang->getVariantname( $code );
+ if( $varname == 'disable' )
+ continue;
+ $selected = ( $code == $preferred )? 'selected' : false;
+ $content_actions['varlang-' . $vcount] = array(
+ 'class' => $selected,
+ 'text' => $varname,
+ 'href' => $this->mTitle->getLocalUrl( $actstr . 'variant=' . urlencode( $code ) )
+ );
+ $vcount ++;
+ }
+ }
+
+ wfRunHooks( 'SkinTemplateContentActions', array( &$content_actions ) );
+
+ wfProfileOut( $fname );
+ return $content_actions;
+ }
+
+
+
+ /**
+ * build array of common navigation links
+ * @return array
+ * @private
+ */
+ function buildNavUrls () {
+ global $wgUseTrackbacks, $wgTitle, $wgArticle;
+
+ $fname = 'SkinTemplate::buildNavUrls';
+ wfProfileIn( $fname );
+
+ global $wgUser, $wgRequest;
+ global $wgEnableUploads, $wgUploadNavigationUrl;
+
+ $action = $wgRequest->getText( 'action' );
+ $oldid = $wgRequest->getVal( 'oldid' );
+ $diff = $wgRequest->getVal( 'diff' );
+
+ $nav_urls = array();
+ $nav_urls['mainpage'] = array('href' => $this->makeI18nUrl('mainpage'));
+ if( $wgEnableUploads ) {
+ if ($wgUploadNavigationUrl) {
+ $nav_urls['upload'] = array('href' => $wgUploadNavigationUrl );
+ } else {
+ $nav_urls['upload'] = array('href' => $this->makeSpecialUrl('Upload'));
+ }
+ } else {
+ if ($wgUploadNavigationUrl)
+ $nav_urls['upload'] = array('href' => $wgUploadNavigationUrl );
+ else
+ $nav_urls['upload'] = false;
+ }
+ $nav_urls['specialpages'] = array('href' => $this->makeSpecialUrl('Specialpages'));
+
+
+ // A print stylesheet is attached to all pages, but nobody ever
+ // figures that out. :) Add a link...
+ if( $this->iscontent && ($action == '' || $action == 'view' || $action == 'purge' ) ) {
+ $revid = $wgArticle->getLatest();
+ if ( !( $revid == 0 ) )
+ $nav_urls['print'] = array(
+ 'text' => wfMsg( 'printableversion' ),
+ 'href' => $wgRequest->appendQuery( 'printable=yes' )
+ );
+
+ // Also add a "permalink" while we're at it
+ if ( (int)$oldid ) {
+ $nav_urls['permalink'] = array(
+ 'text' => wfMsg( 'permalink' ),
+ 'href' => ''
+ );
+ } else {
+ if ( !( $revid == 0 ) )
+ $nav_urls['permalink'] = array(
+ 'text' => wfMsg( 'permalink' ),
+ 'href' => $wgTitle->getLocalURL( "oldid=$revid" )
+ );
+ }
+
+ wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$oldid, &$revid ) );
+ }
+
+ if( $this->mTitle->getNamespace() != NS_SPECIAL ) {
+ $wlhTitle = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' );
+ $nav_urls['whatlinkshere'] = array(
+ 'href' => $wlhTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
+ );
+ if( $this->mTitle->getArticleId() ) {
+ $rclTitle = Title::makeTitle( NS_SPECIAL, 'Recentchangeslinked' );
+ $nav_urls['recentchangeslinked'] = array(
+ 'href' => $rclTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
+ );
+ }
+ if ($wgUseTrackbacks)
+ $nav_urls['trackbacklink'] = array(
+ 'href' => $wgTitle->trackbackURL()
+ );
+ }
+
+ if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
+ $id = User::idFromName($this->mTitle->getText());
+ $ip = User::isIP($this->mTitle->getText());
+ } else {
+ $id = 0;
+ $ip = false;
+ }
+
+ if($id || $ip) { # both anons and non-anons have contri list
+ $nav_urls['contributions'] = array(
+ 'href' => $this->makeSpecialUrl('Contributions/' . $this->mTitle->getText() )
+ );
+ if ( $wgUser->isAllowed( 'block' ) )
+ $nav_urls['blockip'] = array(
+ 'href' => $this->makeSpecialUrl( 'Blockip/' . $this->mTitle->getText() )
+ );
+ } else {
+ $nav_urls['contributions'] = false;
+ }
+ $nav_urls['emailuser'] = false;
+ if( $this->showEmailUser( $id ) ) {
+ $nav_urls['emailuser'] = array(
+ 'href' => $this->makeSpecialUrl('Emailuser/' . $this->mTitle->getText() )
+ );
+ }
+ wfProfileOut( $fname );
+ return $nav_urls;
+ }
+
+ /**
+ * Generate strings used for xml 'id' names
+ * @return string
+ * @private
+ */
+ function getNameSpaceKey () {
+ return $this->mTitle->getNamespaceKey();
+ }
+
+ /**
+ * @private
+ */
+ function setupUserCss() {
+ $fname = 'SkinTemplate::setupUserCss';
+ wfProfileIn( $fname );
+
+ global $wgRequest, $wgAllowUserCss, $wgUseSiteCss, $wgContLang, $wgSquidMaxage, $wgStylePath, $wgUser;
+
+ $sitecss = '';
+ $usercss = '';
+ $siteargs = '&maxage=' . $wgSquidMaxage;
+
+ # Add user-specific code if this is a user and we allow that kind of thing
+
+ if ( $wgAllowUserCss && $this->loggedin ) {
+ $action = $wgRequest->getText('action');
+
+ # if we're previewing the CSS page, use it
+ if( $this->mTitle->isCssSubpage() and $this->userCanPreview( $action ) ) {
+ $siteargs = "&smaxage=0&maxage=0";
+ $usercss = $wgRequest->getText('wpTextbox1');
+ } else {
+ $usercss = '@import "' .
+ $this->makeUrl($this->userpage . '/'.$this->skinname.'.css',
+ 'action=raw&ctype=text/css') . '";' ."\n";
+ }
+
+ $siteargs .= '&ts=' . $wgUser->mTouched;
+ }
+
+ if ($wgContLang->isRTL()) $sitecss .= '@import "' . $wgStylePath . '/' . $this->stylename . '/rtl.css";' . "\n";
+
+ # If we use the site's dynamic CSS, throw that in, too
+ if ( $wgUseSiteCss ) {
+ $query = "action=raw&ctype=text/css&smaxage=$wgSquidMaxage";
+ $sitecss .= '@import "' . $this->makeNSUrl('Common.css', $query, NS_MEDIAWIKI) . '";' . "\n";
+ $sitecss .= '@import "' . $this->makeNSUrl(ucfirst($this->skinname) . '.css', $query, NS_MEDIAWIKI) . '";' . "\n";
+ $sitecss .= '@import "' . $this->makeUrl('-','action=raw&gen=css' . $siteargs) . '";' . "\n";
+ }
+
+ # If we use any dynamic CSS, make a little CDATA block out of it.
+
+ if ( !empty($sitecss) || !empty($usercss) ) {
+ $this->usercss = "/*<![CDATA[*/\n" . $sitecss . $usercss . '/*]]>*/';
+ }
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * @private
+ */
+ function setupUserJs() {
+ $fname = 'SkinTemplate::setupUserJs';
+ wfProfileIn( $fname );
+
+ global $wgRequest, $wgAllowUserJs, $wgJsMimeType;
+ $action = $wgRequest->getText('action');
+
+ if( $wgAllowUserJs && $this->loggedin ) {
+ if( $this->mTitle->isJsSubpage() and $this->userCanPreview( $action ) ) {
+ # XXX: additional security check/prompt?
+ $this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText('wpTextbox1') . ' /*]]>*/';
+ } else {
+ $this->userjs = $this->makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s');
+ }
+ }
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Code for extensions to hook into to provide per-page CSS, see
+ * extensions/PageCSS/PageCSS.php for an implementation of this.
+ *
+ * @private
+ */
+ function setupPageCss() {
+ $fname = 'SkinTemplate::setupPageCss';
+ wfProfileIn( $fname );
+ $out = false;
+ wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) );
+
+ wfProfileOut( $fname );
+ return $out;
+ }
+
+ /**
+ * returns css with user-specific options
+ * @public
+ */
+
+ function getUserStylesheet() {
+ $fname = 'SkinTemplate::getUserStylesheet';
+ wfProfileIn( $fname );
+
+ $s = "/* generated user stylesheet */\n";
+ $s .= $this->reallyDoGetUserStyles();
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /**
+ * @public
+ */
+ function getUserJs() {
+ $fname = 'SkinTemplate::getUserJs';
+ wfProfileIn( $fname );
+
+ global $wgStylePath;
+ $s = '/* generated javascript */';
+ $s .= "var skin = '{$this->skinname}';\nvar stylepath = '{$wgStylePath}';";
+ $s .= '/* MediaWiki:'.ucfirst($this->skinname)." */\n";
+
+ // avoid inclusion of non defined user JavaScript (with custom skins only)
+ // by checking for default message content
+ $msgKey = ucfirst($this->skinname).'.js';
+ $userJS = wfMsg($msgKey);
+ if ('&lt;'.$msgKey.'&gt;' != $userJS) {
+ $s .= $userJS;
+ }
+
+ wfProfileOut( $fname );
+ return $s;
+ }
+}
+
+/**
+ * Generic wrapper for template functions, with interface
+ * compatible with what we use of PHPTAL 0.7.
+ * @package MediaWiki
+ * @subpackage Skins
+ */
+class QuickTemplate {
+ /**
+ * @public
+ */
+ function QuickTemplate() {
+ $this->data = array();
+ $this->translator = new MediaWiki_I18N();
+ }
+
+ /**
+ * @public
+ */
+ function set( $name, $value ) {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * @public
+ */
+ function setRef($name, &$value) {
+ $this->data[$name] =& $value;
+ }
+
+ /**
+ * @public
+ */
+ function setTranslator( &$t ) {
+ $this->translator = &$t;
+ }
+
+ /**
+ * @public
+ */
+ function execute() {
+ echo "Override this function.";
+ }
+
+
+ /**
+ * @private
+ */
+ function text( $str ) {
+ echo htmlspecialchars( $this->data[$str] );
+ }
+
+ /**
+ * @private
+ */
+ function html( $str ) {
+ echo $this->data[$str];
+ }
+
+ /**
+ * @private
+ */
+ function msg( $str ) {
+ echo htmlspecialchars( $this->translator->translate( $str ) );
+ }
+
+ /**
+ * @private
+ */
+ function msgHtml( $str ) {
+ echo $this->translator->translate( $str );
+ }
+
+ /**
+ * An ugly, ugly hack.
+ * @private
+ */
+ function msgWiki( $str ) {
+ global $wgParser, $wgTitle, $wgOut;
+
+ $text = $this->translator->translate( $str );
+ $parserOutput = $wgParser->parse( $text, $wgTitle,
+ $wgOut->mParserOptions, true );
+ echo $parserOutput->getText();
+ }
+
+ /**
+ * @private
+ */
+ function haveData( $str ) {
+ return $this->data[$str];
+ }
+
+ /**
+ * @private
+ */
+ function haveMsg( $str ) {
+ $msg = $this->translator->translate( $str );
+ return ($msg != '-') && ($msg != ''); # ????
+ }
+}
+?>
diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php
new file mode 100644
index 00000000..60258f9e
--- /dev/null
+++ b/includes/SpecialAllmessages.php
@@ -0,0 +1,212 @@
+<?php
+/**
+ * Provide functions to generate a special page
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialAllmessages() {
+ global $wgOut, $wgAllMessagesEn, $wgRequest, $wgMessageCache, $wgTitle;
+ global $wgUseDatabaseMessages;
+
+ # The page isn't much use if the MediaWiki namespace is not being used
+ if( !$wgUseDatabaseMessages ) {
+ $wgOut->addWikiText( wfMsg( 'allmessagesnotsupportedDB' ) );
+ return;
+ }
+
+ $fname = "wfSpecialAllMessages";
+ wfProfileIn( $fname );
+
+ wfProfileIn( "$fname-setup");
+ $ot = $wgRequest->getText( 'ot' );
+
+ $navText = wfMsg( 'allmessagestext' );
+
+ # Make sure all extension messages are available
+ wfLoadAllExtensions();
+
+ $first = true;
+ $sortedArray = array_merge( $wgAllMessagesEn, $wgMessageCache->mExtensionMessages );
+ ksort( $sortedArray );
+ $messages = array();
+ $wgMessageCache->disableTransform();
+
+ foreach ( $sortedArray as $key => $value ) {
+ $messages[$key]['enmsg'] = is_array( $value ) ? $value['en'] : $value;
+ $messages[$key]['statmsg'] = wfMsgNoDb( $key );
+ $messages[$key]['msg'] = wfMsg ( $key );
+ }
+
+ $wgMessageCache->enableTransform();
+ wfProfileOut( "$fname-setup" );
+
+ wfProfileIn( "$fname-output" );
+ if ($ot == 'php') {
+ $navText .= makePhp($messages);
+ $wgOut->addHTML('PHP | <a href="'.$wgTitle->escapeLocalUrl('ot=html').'">HTML</a><pre>'.htmlspecialchars($navText).'</pre>');
+ } else {
+ $wgOut->addHTML( '<a href="'.$wgTitle->escapeLocalUrl('ot=php').'">PHP</a> | HTML' );
+ $wgOut->addWikiText( $navText );
+ $wgOut->addHTML( makeHTMLText( $messages ) );
+ }
+ wfProfileOut( "$fname-output" );
+
+ wfProfileOut( $fname );
+}
+
+/**
+ *
+ */
+function makePhp($messages) {
+ global $wgLanguageCode;
+ $txt = "\n\n".'$wgAllMessages'.ucfirst($wgLanguageCode).' = array('."\n";
+ foreach( $messages as $key => $m ) {
+ if(strtolower($wgLanguageCode) != 'en' and $m['msg'] == $m['enmsg'] ) {
+ //if (strstr($m['msg'],"\n")) {
+ // $txt.='/* ';
+ // $comment=' */';
+ //} else {
+ // $txt .= '#';
+ // $comment = '';
+ //}
+ continue;
+ } elseif ($m['msg'] == '&lt;'.$key.'&gt;'){
+ $m['msg'] = '';
+ $comment = ' #empty';
+ } else {
+ $comment = '';
+ }
+ $txt .= "'$key' => '" . preg_replace( "/(?<!\\\\)'/", "\'", $m['msg']) . "',$comment\n";
+ }
+ $txt .= ');';
+ return $txt;
+}
+
+/**
+ *
+ */
+function makeHTMLText( $messages ) {
+ global $wgLang, $wgUser, $wgLanguageCode, $wgContLanguageCode;
+ $fname = "makeHTMLText";
+ wfProfileIn( $fname );
+
+ $sk =& $wgUser->getSkin();
+ $talk = $wgLang->getNsText( NS_TALK );
+ $mwnspace = $wgLang->getNsText( NS_MEDIAWIKI );
+ $mwtalk = $wgLang->getNsText( NS_MEDIAWIKI_TALK );
+
+ $input = wfElement( 'input', array(
+ 'type' => 'text',
+ 'id' => 'allmessagesinput',
+ 'onkeyup' => 'allmessagesfilter()',),
+ '');
+ $checkbox = wfElement( 'input', array(
+ 'type' => 'button',
+ 'value' => wfMsgHtml( 'allmessagesmodified' ),
+ 'id' => 'allmessagescheckbox',
+ 'onclick' => 'allmessagesmodified()',),
+ '');
+
+ $txt = '<span id="allmessagesfilter" style="display:none;">' .
+ wfMsgHtml('allmessagesfilter') . " {$input}{$checkbox} " . '</span>';
+
+ $txt .= "
+<table border='1' cellspacing='0' width='100%' id='allmessagestable'>
+ <tr>
+ <th rowspan='2'>" . wfMsgHtml('allmessagesname') . "</th>
+ <th>" . wfMsgHtml('allmessagesdefault') . "</th>
+ </tr>
+ <tr>
+ <th>" . wfMsgHtml('allmessagescurrent') . "</th>
+ </tr>";
+
+ wfProfileIn( "$fname-check" );
+ # This is a nasty hack to avoid doing independent existence checks
+ # without sending the links and table through the slow wiki parser.
+ $pageExists = array(
+ NS_MEDIAWIKI => array(),
+ NS_MEDIAWIKI_TALK => array()
+ );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")";
+ $res = $dbr->query( $sql );
+ while( $s = $dbr->fetchObject( $res ) ) {
+ $pageExists[$s->page_namespace][$s->page_title] = true;
+ }
+ $dbr->freeResult( $res );
+ wfProfileOut( "$fname-check" );
+
+ wfProfileIn( "$fname-output" );
+
+ $i = 0;
+
+ foreach( $messages as $key => $m ) {
+
+ $title = $wgLang->ucfirst( $key );
+ if($wgLanguageCode != $wgContLanguageCode)
+ $title.="/$wgLanguageCode";
+
+ $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title );
+ $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
+
+ $changed = ($m['statmsg'] != $m['msg']);
+ $message = htmlspecialchars( $m['statmsg'] );
+ $mw = htmlspecialchars( $m['msg'] );
+
+ #$pageLink = $sk->makeLinkObj( $titleObj, htmlspecialchars( $key ) );
+ #$talkLink = $sk->makeLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) {
+ $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id='sp-allmessages-i-$i'>" . htmlspecialchars( $key ) . "</span>" );
+ } else {
+ $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id='sp-allmessages-i-$i'>" . htmlspecialchars( $key ) . "</span>" );
+ }
+ if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
+ $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ } else {
+ $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ }
+
+ $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) );
+ $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>";
+
+ if($changed) {
+
+ $txt .= "
+ <tr class='orig' id='sp-allmessages-r1-$i'>
+ <td rowspan='2'>
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+$message
+ </td>
+ </tr><tr class='new' id='sp-allmessages-r2-$i'>
+ <td>
+$mw
+ </td>
+ </tr>";
+ } else {
+
+ $txt .= "
+ <tr class='def' id='sp-allmessages-r1-$i'>
+ <td>
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+$mw
+ </td>
+ </tr>";
+
+ }
+ $i++;
+ }
+ $txt .= "</table>";
+ wfProfileOut( "$fname-output" );
+
+ wfProfileOut( $fname );
+ return $txt;
+}
+
+?>
diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php
new file mode 100644
index 00000000..53a5b348
--- /dev/null
+++ b/includes/SpecialAllpages.php
@@ -0,0 +1,322 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
+ * @param $specialPage @see SpecialPage object.
+ */
+function wfSpecialAllpages( $par=NULL, $specialPage ) {
+ global $wgRequest, $wgOut, $wgContLang;
+
+ # GET values
+ $from = $wgRequest->getVal( 'from' );
+ $namespace = $wgRequest->getInt( 'namespace' );
+
+ $namespaces = $wgContLang->getNamespaces();
+
+ $indexPage = new SpecialAllpages();
+
+ if( !in_array($namespace, array_keys($namespaces)) )
+ $namespace = 0;
+
+ $wgOut->setPagetitle( $namespace > 0 ?
+ wfMsg( 'allinnamespace', $namespaces[$namespace] ) :
+ wfMsg( 'allarticles' )
+ );
+
+ if ( isset($par) ) {
+ $indexPage->showChunk( $namespace, $par, $specialPage->including() );
+ } elseif ( isset($from) ) {
+ $indexPage->showChunk( $namespace, $from, $specialPage->including() );
+ } else {
+ $indexPage->showToplevel ( $namespace, $specialPage->including() );
+ }
+}
+
+class SpecialAllpages {
+ var $maxPerPage=960;
+ var $topLevelMax=50;
+ var $name='Allpages';
+ # Determines, which message describes the input field 'nsfrom' (->SpecialPrefixindex.php)
+ var $nsfromMsg='allpagesfrom';
+
+/**
+ * HTML for the top form
+ * @param integer $namespace A namespace constant (default NS_MAIN).
+ * @param string $from Article name we are starting listing at.
+ */
+function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
+ global $wgScript;
+ $t = Title::makeTitle( NS_SPECIAL, $this->name );
+
+ $namespaceselect = HTMLnamespaceselector($namespace, null);
+
+ $frombox = "<input type='text' size='20' name='from' id='nsfrom' value=\""
+ . htmlspecialchars ( $from ) . '"/>';
+ $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . '" />';
+
+ $out = "<div class='namespaceoptions'><form method='get' action='{$wgScript}'>";
+ $out .= '<input type="hidden" name="title" value="'.$t->getPrefixedText().'" />';
+ $out .= "
+<table id='nsselect' class='allpages'>
+ <tr>
+ <td align='right'>" . wfMsgHtml($this->nsfromMsg) . "</td>
+ <td align='left'><label for='nsfrom'>$frombox</label></td>
+ </tr>
+ <tr>
+ <td align='right'><label for='namespace'>" . wfMsgHtml('namespace') . "</label></td>
+ <td align='left'>
+ $namespaceselect $submitbutton
+ </td>
+ </tr>
+</table>
+";
+ $out .= '</form></div>';
+ return $out;
+}
+
+/**
+ * @param integer $namespace (default NS_MAIN)
+ */
+function showToplevel ( $namespace = NS_MAIN, $including = false ) {
+ global $wgOut, $wgUser;
+ $sk = $wgUser->getSkin();
+ $fname = "indexShowToplevel";
+
+ # TODO: Either make this *much* faster or cache the title index points
+ # in the querycache table.
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $fromwhere = "FROM $page WHERE page_namespace=$namespace";
+ $order_arr = array ( 'ORDER BY' => 'page_title' );
+ $order_str = 'ORDER BY page_title';
+ $out = "";
+ $where = array( 'page_namespace' => $namespace );
+
+ global $wgMemc, $wgDBname;
+ $key = "$wgDBname:allpages:ns:$namespace";
+ $lines = $wgMemc->get( $key );
+
+ if( !is_array( $lines ) ) {
+ $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, array( 'LIMIT' => 1 ) );
+ $lastTitle = $firstTitle;
+
+ # This array is going to hold the page_titles in order.
+ $lines = array( $firstTitle );
+
+ # If we are going to show n rows, we need n+1 queries to find the relevant titles.
+ $done = false;
+ for( $i = 0; !$done; ++$i ) {
+ // Fetch the last title of this chunk and the first of the next
+ $chunk = is_null( $lastTitle )
+ ? ''
+ : 'page_title >= ' . $dbr->addQuotes( $lastTitle );
+ $res = $dbr->select(
+ 'page', /* FROM */
+ 'page_title', /* WHAT */
+ $where + array( $chunk),
+ $fname,
+ array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
+
+ if ( $s = $dbr->fetchObject( $res ) ) {
+ array_push( $lines, $s->page_title );
+ } else {
+ // Final chunk, but ended prematurely. Go back and find the end.
+ $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
+ array(
+ 'page_namespace' => $namespace,
+ $chunk
+ ), $fname );
+ array_push( $lines, $endTitle );
+ $done = true;
+ }
+ if( $s = $dbr->fetchObject( $res ) ) {
+ array_push( $lines, $s->page_title );
+ $lastTitle = $s->page_title;
+ } else {
+ // This was a final chunk and ended exactly at the limit.
+ // Rare but convenient!
+ $done = true;
+ }
+ $dbr->freeResult( $res );
+ }
+ $wgMemc->add( $key, $lines, 3600 );
+ }
+
+ // If there are only two or less sections, don't even display them.
+ // Instead, display the first section directly.
+ if( count( $lines ) <= 2 ) {
+ $this->showChunk( $namespace, '', $including );
+ return;
+ }
+
+ # At this point, $lines should contain an even number of elements.
+ $out .= "<table class='allpageslist' style='background: inherit;'>";
+ while ( count ( $lines ) > 0 ) {
+ $inpoint = array_shift ( $lines );
+ $outpoint = array_shift ( $lines );
+ $out .= $this->showline ( $inpoint, $outpoint, $namespace, false );
+ }
+ $out .= '</table>';
+ $nsForm = $this->namespaceForm ( $namespace, '', false );
+
+ # Is there more?
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $morelinks = '';
+ if ( $morelinks != '' ) {
+ $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+ $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
+ $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">';
+ $out2 .= $morelinks . '</td></tr></table><hr />';
+ } else {
+ $out2 = $nsForm . '<hr />';
+ }
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+}
+
+/**
+ * @todo Document
+ * @param string $from
+ * @param integer $namespace (Default NS_MAIN)
+ */
+function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
+ $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
+ $queryparams = ($namespace ? "namespace=$namespace" : '');
+ $special = Title::makeTitle( NS_SPECIAL, $this->name . '/' . $inpoint );
+ $link = $special->escapeLocalUrl( $queryparams );
+
+ $out = wfMsgHtml(
+ 'alphaindexline',
+ "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
+ "</a></td><td align=\"left\"><a href=\"$link\">$outpointf</a>"
+ );
+ return '<tr><td align="right">'.$out.'</td></tr>';
+}
+
+/**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ */
+function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
+ global $wgOut, $wgUser, $wgContLang;
+
+ $fname = 'indexShowChunk';
+
+ $sk = $wgUser->getSkin();
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+
+ if ( !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } else {
+ list( $namespace, $fromKey, $from ) = $fromList;
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title >= ' . $dbr->addQuotes( $fromKey )
+ ),
+ $fname,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ ### FIXME: side link to previous
+
+ $n = 0;
+ $out = '<table style="background: inherit;" border="0" width="100%">';
+
+ $namespaces = $wgContLang->getFormattedNamespaces();
+ while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $t = Title::makeTitle( $s->page_namespace, $s->page_title );
+ if( $t ) {
+ $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+ $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+ ($s->page_is_redirect ? '</div>' : '' );
+ } else {
+ $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+ }
+ if( $n % 3 == 0 ) {
+ $out .= '<tr>';
+ }
+ $out .= "<td>$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $nsForm = $this->namespaceForm ( $namespace, $from );
+ $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+ $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
+ $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">' .
+ $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
+ wfMsgHtml ( 'allpages' ) );
+ if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+ $out2 .= " | " . $sk->makeKnownLink(
+ $wgContLang->specialPage( "Allpages" ),
+ wfMsgHtml ( 'nextpage', $s->page_title ),
+ "from=" . wfUrlEncode ( $s->page_title ) . $namespaceparam );
+ }
+ $out2 .= "</td></tr></table><hr />";
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+}
+
+/**
+ * @param int $ns the namespace of the article
+ * @param string $text the name of the article
+ * @return array( int namespace, string dbkey, string pagename ) or NULL on error
+ * @static (sort of)
+ * @access private
+ */
+function getNamespaceKeyAndText ($ns, $text) {
+ if ( $text == '' )
+ return array( $ns, '', '' ); # shortcut for common case
+
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() )
+ return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
+ else if ( $t )
+ return NULL;
+
+ # try again, in case the problem was an empty pagename
+ $text = preg_replace('/(#|$)/', 'X$1', $text);
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() )
+ return array( $t->getNamespace(), '', '' );
+ else
+ return NULL;
+}
+}
+
+?>
diff --git a/includes/SpecialAncientpages.php b/includes/SpecialAncientpages.php
new file mode 100644
index 00000000..39a3c8ea
--- /dev/null
+++ b/includes/SpecialAncientpages.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class AncientPagesPage extends QueryPage {
+
+ function getName() {
+ return "Ancientpages";
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgDBtype;
+ $db =& wfGetDB( DB_SLAVE );
+ $page = $db->tableName( 'page' );
+ $revision = $db->tableName( 'revision' );
+ #$use_index = $db->useIndexClause( 'cur_timestamp' ); # FIXME! this is gone
+ $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
+ 'EXTRACT(epoch FROM rev_timestamp)';
+ return
+ "SELECT 'Ancientpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ $epoch as value
+ FROM $page, $revision
+ WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0
+ AND page_latest=rev_id";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true );
+ $title = Title::makeTitle( $result->namespace, $result->title );
+ $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+ return wfSpecialList($link, $d);
+ }
+}
+
+function wfSpecialAncientpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $app = new AncientPagesPage();
+
+ $app->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php
new file mode 100644
index 00000000..b3f67ab1
--- /dev/null
+++ b/includes/SpecialBlockip.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Constructor for Special:Blockip page
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialBlockip( $par ) {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+
+ $ipb = new IPBlockForm( $par );
+
+ $action = $wgRequest->getVal( 'action' );
+ if ( 'success' == $action ) {
+ $ipb->showSuccess();
+ } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $ipb->doSubmit();
+ } else {
+ $ipb->showForm( '' );
+ }
+}
+
+/**
+ * Form object
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class IPBlockForm {
+ var $BlockAddress, $BlockExpiry, $BlockReason;
+
+ function IPBlockForm( $par ) {
+ global $wgRequest;
+
+ $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) );
+ $this->BlockReason = $wgRequest->getText( 'wpBlockReason' );
+ $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') );
+ $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' );
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->addWikiText( wfMsg( 'blockiptext' ) );
+
+ if($wgSysopUserBans) {
+ $mIpaddress = wfMsgHtml( 'ipadressorusername' );
+ } else {
+ $mIpaddress = wfMsgHtml( 'ipaddress' );
+ }
+ $mIpbexpiry = wfMsgHtml( 'ipbexpiry' );
+ $mIpbother = wfMsgHtml( 'ipbother' );
+ $mIpbothertime = wfMsgHtml( 'ipbotheroption' );
+ $mIpbreason = wfMsgHtml( 'ipbreason' );
+ $mIpbsubmit = wfMsgHtml( 'ipbsubmit' );
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' );
+ $action = $titleObj->escapeLocalURL( "action=submit" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
+ $wgOut->addHTML( "<p class='error'>{$err}</p>\n" );
+ }
+
+ $scBlockAddress = htmlspecialchars( $this->BlockAddress );
+ $scBlockExpiry = htmlspecialchars( $this->BlockExpiry );
+ $scBlockReason = htmlspecialchars( $this->BlockReason );
+ $scBlockOtherTime = htmlspecialchars( $this->BlockOther );
+ $scBlockExpiryOptions = htmlspecialchars( wfMsgForContent( 'ipboptions' ) );
+
+ $showblockoptions = $scBlockExpiryOptions != '-';
+ if (!$showblockoptions)
+ $mIpbother = $mIpbexpiry;
+
+ $blockExpiryFormOptions = "<option value=\"other\">$mIpbothertime</option>";
+ foreach (explode(',', $scBlockExpiryOptions) as $option) {
+ if ( strpos($option, ":") === false ) $option = "$option:$option";
+ list($show, $value) = explode(":", $option);
+ $show = htmlspecialchars($show);
+ $value = htmlspecialchars($value);
+ $selected = "";
+ if ($this->BlockExpiry === $value)
+ $selected = ' selected="selected"';
+ $blockExpiryFormOptions .= "<option value=\"$value\"$selected>$show</option>";
+ }
+
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( "
+<form id=\"blockip\" method=\"post\" action=\"{$action}\">
+ <table border='0'>
+ <tr>
+ <td align=\"right\">{$mIpaddress}:</td>
+ <td align=\"left\">
+ <input tabindex='1' type='text' size='20' name=\"wpBlockAddress\" value=\"{$scBlockAddress}\" />
+ </td>
+ </tr>
+ <tr>");
+ if ($showblockoptions) {
+ $wgOut->addHTML("
+ <td align=\"right\">{$mIpbexpiry}:</td>
+ <td align=\"left\">
+ <select tabindex='2' id='wpBlockExpiry' name=\"wpBlockExpiry\" onchange=\"considerChangingExpiryFocus()\">
+ $blockExpiryFormOptions
+ </select>
+ </td>
+ ");
+ }
+ $wgOut->addHTML("
+ </tr>
+ <tr id='wpBlockOther'>
+ <td align=\"right\">{$mIpbother}:</td>
+ <td align=\"left\">
+ <input tabindex='3' type='text' size='40' name=\"wpBlockOther\" value=\"{$scBlockOtherTime}\" />
+ </td>
+ </tr>
+ <tr>
+ <td align=\"right\">{$mIpbreason}:</td>
+ <td align=\"left\">
+ <input tabindex='3' type='text' size='40' name=\"wpBlockReason\" value=\"{$scBlockReason}\" />
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align=\"left\">
+ <input tabindex='4' type='submit' name=\"wpBlock\" value=\"{$mIpbsubmit}\" />
+ </td>
+ </tr>
+ </table>
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+</form>\n" );
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
+
+ $userId = 0;
+ $this->BlockAddress = trim( $this->BlockAddress );
+ $rxIP = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
+
+ # Check for invalid specifications
+ if ( ! preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
+ if ( preg_match( "/^($rxIP)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) {
+ if ( $wgSysopRangeBans ) {
+ if ( $matches[2] > 31 || $matches[2] < 16 ) {
+ $this->showForm( wfMsg( 'ip_range_invalid' ) );
+ return;
+ }
+ $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+ } else {
+ # Range block illegal
+ $this->showForm( wfMsg( 'range_block_disabled' ) );
+ return;
+ }
+ } else {
+ # Username block
+ if ( $wgSysopUserBans ) {
+ $userId = User::idFromName( $this->BlockAddress );
+ if ( $userId == 0 ) {
+ $this->showForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->BlockAddress ) ) );
+ return;
+ }
+ } else {
+ $this->showForm( wfMsg( 'badipaddress' ) );
+ return;
+ }
+ }
+ }
+
+ $expirestr = $this->BlockExpiry;
+ if( $expirestr == 'other' )
+ $expirestr = $this->BlockOther;
+
+ if (strlen($expirestr) == 0) {
+ $this->showForm( wfMsg( 'ipb_expiry_invalid' ) );
+ return;
+ }
+
+ if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) {
+ $expiry = '';
+ } else {
+ # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1
+ $expiry = strtotime( $expirestr );
+
+ if ( $expiry < 0 || $expiry === false ) {
+ $this->showForm( wfMsg( 'ipb_expiry_invalid' ) );
+ return;
+ }
+
+ $expiry = wfTimestamp( TS_MW, $expiry );
+
+ }
+
+ # Create block
+ # Note: for a user block, ipb_address is only for display purposes
+
+ $ban = new Block( $this->BlockAddress, $userId, $wgUser->getID(),
+ $this->BlockReason, wfTimestampNow(), 0, $expiry );
+
+ if (wfRunHooks('BlockIp', array(&$ban, &$wgUser))) {
+
+ $ban->insert();
+
+ wfRunHooks('BlockIpComplete', array($ban, $wgUser));
+
+ # Make log entry
+ $log = new LogPage( 'block' );
+ $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ),
+ $this->BlockReason, $expirestr );
+
+ # Report to the user
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
+ urlencode( $this->BlockAddress ) ) );
+ }
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
+ $text = wfMsg( 'blockipsuccesstext', $this->BlockAddress );
+ $wgOut->addWikiText( $text );
+ }
+}
+
+?>
diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php
new file mode 100644
index 00000000..5bfce4ee
--- /dev/null
+++ b/includes/SpecialBlockme.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialBlockme() {
+ global $wgRequest, $wgBlockOpenProxies, $wgOut, $wgProxyKey;
+
+ $ip = wfGetIP();
+
+ if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
+ $wgOut->addWikiText( wfMsg( 'disabled' ) );
+ return;
+ }
+
+ $blockerName = wfMsg( "proxyblocker" );
+ $reason = wfMsg( "proxyblockreason" );
+ $success = wfMsg( "proxyblocksuccess" );
+
+ $u = User::newFromName( $blockerName );
+ $id = $u->idForName();
+ if ( !$id ) {
+ $u = User::newFromName( $blockerName );
+ $u->addToDatabase();
+ $u->setPassword( bin2hex( mt_rand(0, 0x7fffffff ) ) );
+ $u->saveSettings();
+ $id = $u->getID();
+ }
+
+ $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
+ $block->insert();
+
+ $wgOut->addWikiText( $success );
+}
+?>
diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php
new file mode 100644
index 00000000..960f6224
--- /dev/null
+++ b/includes/SpecialBooksources.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * ISBNs in wiki pages will create links to this page, with the ISBN passed
+ * in via the query string.
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialBooksources( $par ) {
+ global $wgRequest;
+
+ $isbn = $par;
+ if( empty( $par ) ) {
+ $isbn = $wgRequest->getVal( 'isbn' );
+ }
+ $isbn = preg_replace( '/[^0-9X]/', '', $isbn );
+
+ $bsl = new BookSourceList( $isbn );
+ $bsl->show();
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class BookSourceList {
+ var $mIsbn;
+
+ function BookSourceList( $isbn ) {
+ $this->mIsbn = $isbn;
+ }
+
+ function show() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( "booksources" ) );
+ if( $this->mIsbn == '' ) {
+ $this->askForm();
+ } else {
+ $this->showList();
+ }
+ }
+
+ function showList() {
+ global $wgOut, $wgContLang;
+ $fname = "BookSourceList::showList()";
+
+ # First, see if we have a custom list setup in
+ # [[Wikipedia:Book sources]] or equivalent.
+ $bstitle = Title::makeTitleSafe( NS_PROJECT, wfMsg( "booksources" ) );
+ if( $bstitle ) {
+ $revision = Revision::newFromTitle( $bstitle );
+ if( $revision ) {
+ $bstext = $revision->getText();
+ if( $bstext ) {
+ $bstext = str_replace( "MAGICNUMBER", $this->mIsbn, $bstext );
+ $wgOut->addWikiText( $bstext );
+ return;
+ }
+ }
+ }
+
+ # Otherwise, use the list of links in the default Language.php file.
+ $s = wfMsgWikiHtml( 'booksourcetext' ) . "<ul>\n";
+ $bs = $wgContLang->getBookstoreList() ;
+ $bsn = array_keys ( $bs ) ;
+ foreach ( $bsn as $name ) {
+ $adr = $bs[$name] ;
+ if ( ! $this->mIsbn ) {
+ $adr = explode( ":" , $adr , 2 );
+ $adr = explode( "/" , $adr[1] );
+ $a = "";
+ while ( $a == "" ) {
+ $a = array_shift( $adr );
+ }
+ $adr = "http://".$a ;
+ } else {
+ $adr = str_replace ( "$1" , $this->mIsbn , $adr ) ;
+ }
+ $name = htmlspecialchars( $name );
+ $adr = htmlspecialchars( $adr );
+ $s .= "<li><a href=\"{$adr}\" class=\"external\">{$name}</a></li>\n" ;
+ }
+ $s .= "</ul>\n";
+
+ $wgOut->addHTML( $s );
+ }
+
+ function askForm() {
+ global $wgOut, $wgTitle;
+ $fname = "BookSourceList::askForm()";
+
+ $action = $wgTitle->escapeLocalUrl();
+ $isbn = htmlspecialchars( wfMsg( "isbn" ) );
+ $go = htmlspecialchars( wfMsg( "go" ) );
+ $out = "<form action=\"$action\" method='post'>
+ $isbn: <input name='isbn' id='isbn' />
+ <input type='submit' value=\"$go\" />
+ </form>";
+ $wgOut->addHTML( $out );
+ }
+}
+
+?>
diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php
new file mode 100644
index 00000000..e5c2dd8e
--- /dev/null
+++ b/includes/SpecialBrokenRedirects.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class BrokenRedirectsPage extends PageQueryPage {
+ var $targets = array();
+
+ function getName() {
+ return 'BrokenRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ global $wgOut;
+ return $wgOut->parse( wfMsg( 'brokenredirectstext' ) );
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'pagelinks' ) );
+
+ $sql = "SELECT 'BrokenRedirects' AS type,
+ p1.page_namespace AS namespace,
+ p1.page_title AS title,
+ pl_namespace,
+ pl_title
+ FROM $pagelinks AS pl
+ JOIN $page p1 ON (p1.page_is_redirect=1 AND pl.pl_from=p1.page_id)
+ LEFT JOIN $page AS p2 ON (pl_namespace=p2.page_namespace AND pl_title=p2.page_title )
+ WHERE p2.page_namespace IS NULL";
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $fromObj = Title::makeTitle( $result->namespace, $result->title );
+ if ( isset( $result->pl_title ) ) {
+ $toObj = Title::makeTitle( $result->pl_namespace, $result->pl_title );
+ } else {
+ $blinks = $fromObj->getBrokenLinksFrom();
+ if ( $blinks ) {
+ $toObj = $blinks[0];
+ } else {
+ $toObj = false;
+ }
+ }
+
+ // $toObj may very easily be false if the $result list is cached
+ if ( !is_object( $toObj ) ) {
+ return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
+ }
+
+ $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
+ $edit = $skin->makeBrokenLinkObj( $fromObj , "(".wfMsg("qbedit").")" , 'redirect=no');
+ $to = $skin->makeBrokenLinkObj( $toObj );
+ $arr = $wgContLang->isRTL() ? '&larr;' : '&rarr;';
+
+ return "$from $edit $arr $to";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialBrokenRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sbr = new BrokenRedirectsPage();
+
+ return $sbr->doQuery( $offset, $limit );
+
+}
+?>
diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php
new file mode 100644
index 00000000..8a6dd5ff
--- /dev/null
+++ b/includes/SpecialCategories.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class CategoriesPage extends QueryPage {
+
+ function getName() {
+ return "Categories";
+ }
+
+ function isExpensive() {
+ return false;
+ }
+
+ function isSyndicated() { return false; }
+
+ function getPageHeader() {
+ return wfMsgWikiHtml( 'categoriespagetext' );
+ }
+
+ function getSQL() {
+ $NScat = NS_CATEGORY;
+ $dbr =& wfGetDB( DB_SLAVE );
+ $categorylinks = $dbr->tableName( 'categorylinks' );
+ $s= "SELECT 'Categories' as type,
+ {$NScat} as namespace,
+ cl_to as title,
+ 1 as value,
+ COUNT(*) as count
+ FROM $categorylinks
+ GROUP BY cl_to";
+ return $s;
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitle( NS_CATEGORY, $result->title );
+ $plink = $skin->makeLinkObj( $title, $title->getText() );
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->count ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ *
+ */
+function wfSpecialCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $cap = new CategoriesPage();
+
+ return $cap->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php
new file mode 100644
index 00000000..fd0425a8
--- /dev/null
+++ b/includes/SpecialConfirmemail.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * Special page allows users to request email confirmation message, and handles
+ * processing of the confirmation code when the link in the email is followed
+ *
+ * @package MediaWiki
+ * @subpackage Special pages
+ * @author Rob Church <robchur@gmail.com>
+ */
+
+/**
+ * Main execution point
+ *
+ * @param $par Parameters passed to the page
+ */
+function wfSpecialConfirmemail( $par ) {
+ $form = new EmailConfirmation();
+ $form->execute( $par );
+}
+
+class EmailConfirmation extends SpecialPage {
+
+ /**
+ * Main execution point
+ *
+ * @param $code Confirmation code passed to the page
+ */
+ function execute( $code ) {
+ global $wgUser, $wgOut;
+ if( empty( $code ) ) {
+ if( $wgUser->isLoggedIn() ) {
+ $this->showRequestForm();
+ } else {
+ $title = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $self = Title::makeTitle( NS_SPECIAL, 'Confirmemail' );
+ $skin = $wgUser->getSkin();
+ $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() );
+ $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
+ }
+ } else {
+ $this->attemptConfirm( $code );
+ }
+ }
+
+ /**
+ * Show a nice form for the user to request a confirmation mail
+ */
+ function showRequestForm() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
+ $ok = $wgUser->sendConfirmationMail();
+ $message = WikiError::isError( $ok ) ? 'confirmemail_sendfailed' : 'confirmemail_sent';
+ $wgOut->addWikiText( wfMsg( $message ) );
+ } else {
+ if( $wgUser->isEmailConfirmed() ) {
+ $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
+ $wgOut->addWikiText( wfMsg( 'emailauthenticated', $time ) );
+ }
+ $wgOut->addWikiText( wfMsg( 'confirmemail_text' ) );
+ $self = Title::makeTitle( NS_SPECIAL, 'Confirmemail' );
+ $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+ $form .= wfHidden( 'token', $wgUser->editToken() );
+ $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) );
+ $form .= wfCloseElement( 'form' );
+ $wgOut->addHtml( $form );
+ }
+ }
+
+ /**
+ * Attempt to confirm the user's email address and show success or failure
+ * as needed; if successful, take the user to log in
+ *
+ * @param $code Confirmation code
+ */
+ function attemptConfirm( $code ) {
+ global $wgUser, $wgOut;
+ $user = User::newFromConfirmationCode( $code );
+ if( is_object( $user ) ) {
+ if( $user->confirmEmail() ) {
+ $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
+ $wgOut->addWikiText( wfMsg( $message ) );
+ if( !$wgUser->isLoggedIn() ) {
+ $title = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $wgOut->returnToMain( true, $title->getPrefixedText() );
+ }
+ } else {
+ $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) );
+ }
+ } else {
+ $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) );
+ }
+ }
+
+}
+
+?>
diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php
new file mode 100644
index 00000000..8477b6bc
--- /dev/null
+++ b/includes/SpecialContributions.php
@@ -0,0 +1,444 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/** @package MediaWiki */
+class ContribsFinder {
+ var $username, $offset, $limit, $namespace;
+ var $dbr;
+
+ function ContribsFinder( $username ) {
+ $this->username = $username;
+ $this->namespace = false;
+ $this->dbr =& wfGetDB( DB_SLAVE );
+ }
+
+ function setNamespace( $ns ) {
+ $this->namespace = $ns;
+ }
+
+ function setLimit( $limit ) {
+ $this->limit = $limit;
+ }
+
+ function setOffset( $offset ) {
+ $this->offset = $offset;
+ }
+
+ function getEditLimit( $dir ) {
+ list( $index, $usercond ) = $this->getUserCond();
+ $nscond = $this->getNamespaceCond();
+ $use_index = $this->dbr->useIndexClause( $index );
+ extract( $this->dbr->tableNames( 'revision', 'page' ) );
+ $sql = "SELECT rev_timestamp " .
+ " FROM $page,$revision $use_index " .
+ " WHERE rev_page=page_id AND $usercond $nscond" .
+ " ORDER BY rev_timestamp $dir LIMIT 1";
+
+ $res = $this->dbr->query( $sql, __METHOD__ );
+ $row = $this->dbr->fetchObject( $res );
+ if ( $row ) {
+ return $row->rev_timestamp;
+ } else {
+ return false;
+ }
+ }
+
+ function getEditLimits() {
+ return array(
+ $this->getEditLimit( "ASC" ),
+ $this->getEditLimit( "DESC" )
+ );
+ }
+
+ function getUserCond() {
+ $condition = '';
+
+ if ( $this->username == 'newbies' ) {
+ $max = $this->dbr->selectField( 'user', 'max(user_id)', false, 'make_sql' );
+ $condition = '>' . (int)($max - $max / 100);
+ }
+
+ if ( $condition == '' ) {
+ $condition = ' rev_user_text=' . $this->dbr->addQuotes( $this->username );
+ $index = 'usertext_timestamp';
+ } else {
+ $condition = ' rev_user '.$condition ;
+ $index = 'user_timestamp';
+ }
+ return array( $index, $condition );
+ }
+
+ function getNamespaceCond() {
+ if ( $this->namespace !== false )
+ return ' AND page_namespace = ' . (int)$this->namespace;
+ return '';
+ }
+
+ function getPreviousOffsetForPaging() {
+ list( $index, $usercond ) = $this->getUserCond();
+ $nscond = $this->getNamespaceCond();
+
+ $use_index = $this->dbr->useIndexClause( $index );
+ extract( $this->dbr->tableNames( 'page', 'revision' ) );
+
+ $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " .
+ "WHERE page_id = rev_page AND rev_timestamp > '" . $this->offset . "' AND " .
+ $usercond . $nscond;
+ $sql .= " ORDER BY rev_timestamp ASC";
+ $sql = $this->dbr->limitResult( $sql, $this->limit, 0 );
+ $res = $this->dbr->query( $sql );
+
+ $numRows = $this->dbr->numRows( $res );
+ if ( $numRows ) {
+ $this->dbr->dataSeek( $res, $numRows - 1 );
+ $row = $this->dbr->fetchObject( $res );
+ $offset = $row->rev_timestamp;
+ } else {
+ $offset = false;
+ }
+ $this->dbr->freeResult( $res );
+ return $offset;
+ }
+
+ function getFirstOffsetForPaging() {
+ list( $index, $usercond ) = $this->getUserCond();
+ $use_index = $this->dbr->useIndexClause( $index );
+ extract( $this->dbr->tableNames( 'page', 'revision' ) );
+ $nscond = $this->getNamespaceCond();
+ $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " .
+ "WHERE page_id = rev_page AND " .
+ $usercond . $nscond;
+ $sql .= " ORDER BY rev_timestamp ASC";
+ $sql = $this->dbr->limitResult( $sql, $this->limit, 0 );
+ $res = $this->dbr->query( $sql );
+
+ $numRows = $this->dbr->numRows( $res );
+ if ( $numRows ) {
+ $this->dbr->dataSeek( $res, $numRows - 1 );
+ $row = $this->dbr->fetchObject( $res );
+ $offset = $row->rev_timestamp;
+ } else {
+ $offset = false;
+ }
+ $this->dbr->freeResult( $res );
+ return $offset;
+ }
+
+ /* private */ function makeSql() {
+ $userCond = $condition = $index = $offsetQuery = '';
+
+ extract( $this->dbr->tableNames( 'page', 'revision' ) );
+ list( $index, $userCond ) = $this->getUserCond();
+
+ if ( $this->offset )
+ $offsetQuery = "AND rev_timestamp <= '{$this->offset}'";
+
+ $nscond = $this->getNamespaceCond();
+ $use_index = $this->dbr->useIndexClause( $index );
+ $sql = "SELECT
+ page_namespace,page_title,page_is_new,page_latest,
+ rev_id,rev_page,rev_text_id,rev_timestamp,rev_comment,rev_minor_edit,rev_user,rev_user_text,
+ rev_deleted
+ FROM $page,$revision $use_index
+ WHERE page_id=rev_page AND $userCond $nscond $offsetQuery
+ ORDER BY rev_timestamp DESC";
+ $sql = $this->dbr->limitResult( $sql, $this->limit, 0 );
+ return $sql;
+ }
+
+ function find() {
+ $contribs = array();
+ $res = $this->dbr->query( $this->makeSql(), __METHOD__ );
+ while ( $c = $this->dbr->fetchObject( $res ) )
+ $contribs[] = $c;
+ $this->dbr->freeResult( $res );
+ return $contribs;
+ }
+};
+
+/**
+ * Special page "user contributions".
+ * Shows a list of the contributions of a user.
+ *
+ * @return none
+ * @param $par String: (optional) user name of the user for which to show the contributions
+ */
+function wfSpecialContributions( $par = null ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+ $fname = 'wfSpecialContributions';
+
+ $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
+ if ( !strlen( $target ) ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+
+ $nt = Title::newFromURL( $target );
+ if ( !$nt ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+
+ $options = array();
+
+ list( $options['limit'], $options['offset']) = wfCheckLimits();
+ $options['offset'] = $wgRequest->getVal( 'offset' );
+ /* Offset must be an integral. */
+ if ( !strlen( $options['offset'] ) || !preg_match( '/^[0-9]+$/', $options['offset'] ) )
+ $options['offset'] = '';
+
+ $title = Title::makeTitle( NS_SPECIAL, 'Contributions' );
+ $options['target'] = $target;
+
+ $nt =& Title::makeTitle( NS_USER, $nt->getDBkey() );
+ $finder = new ContribsFinder( ( $target == 'newbies' ) ? 'newbies' : $nt->getText() );
+ $finder->setLimit( $options['limit'] );
+ $finder->setOffset( $options['offset'] );
+
+ if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
+ $options['namespace'] = intval( $ns );
+ $finder->setNamespace( $options['namespace'] );
+ } else {
+ $options['namespace'] = '';
+ }
+
+ if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) {
+ $options['bot'] = '1';
+ }
+
+ if ( $wgRequest->getText( 'go' ) == 'prev' ) {
+ $offset = $finder->getPreviousOffsetForPaging();
+ if ( $offset !== false ) {
+ $options['offset'] = $offset;
+ $prevurl = $title->getLocalURL( wfArrayToCGI( $options ) );
+ $wgOut->redirect( $prevurl );
+ return;
+ }
+ }
+
+ if ( $wgRequest->getText( 'go' ) == 'first' && $target != 'newbies') {
+ $offset = $finder->getFirstOffsetForPaging();
+ if ( $offset !== false ) {
+ $options['offset'] = $offset;
+ $prevurl = $title->getLocalURL( wfArrayToCGI( $options ) );
+ $wgOut->redirect( $prevurl );
+ return;
+ }
+ }
+
+ if ( $target == 'newbies' ) {
+ $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
+ } else {
+ $wgOut->setSubtitle( wfMsgHtml( 'contribsub', contributionsSub( $nt ) ) );
+ }
+
+ $id = User::idFromName( $nt->getText() );
+ wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
+
+ $wgOut->addHTML( contributionsForm( $options) );
+
+ $contribs = $finder->find();
+
+ if ( count( $contribs ) == 0) {
+ $wgOut->addWikiText( wfMsg( 'nocontribs' ) );
+ return;
+ }
+
+ list( $early, $late ) = $finder->getEditLimits();
+ $lastts = count( $contribs ) ? $contribs[count( $contribs ) - 1]->rev_timestamp : 0;
+ $atstart = ( !count( $contribs ) || $late == $contribs[0]->rev_timestamp );
+ $atend = ( !count( $contribs ) || $early == $lastts );
+
+ // These four are defaults
+ $newestlink = wfMsgHtml( 'sp-contributions-newest' );
+ $oldestlink = wfMsgHtml( 'sp-contributions-oldest' );
+ $newerlink = wfMsgHtml( 'sp-contributions-newer', $options['limit'] );
+ $olderlink = wfMsgHtml( 'sp-contributions-older', $options['limit'] );
+
+ if ( !$atstart ) {
+ $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'offset' => '' ), $options ) );
+ $newestlink = "<a href=\"$stuff\">$newestlink</a>";
+ $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'go' => 'prev' ), $options ) );
+ $newerlink = "<a href=\"$stuff\">$newerlink</a>";
+ }
+
+ if ( !$atend ) {
+ $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'go' => 'first' ), $options ) );
+ $oldestlink = "<a href=\"$stuff\">$oldestlink</a>";
+ $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'offset' => $lastts ), $options ) );
+ $olderlink = "<a href=\"$stuff\">$olderlink</a>";
+ }
+
+ if ( $target == 'newbies' ) {
+ $firstlast ="($newestlink)";
+ } else {
+ $firstlast = "($newestlink | $oldestlink)";
+ }
+
+ $urls = array();
+ foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
+ $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'limit' => $num ), $options ) );
+ $urls[] = "<a href=\"$stuff\">".$wgLang->formatNum( $num )."</a>";
+ }
+ $bits = implode( $urls, ' | ' );
+
+ $prevnextbits = $firstlast .' '. wfMsgHtml( 'viewprevnext', $newerlink, $olderlink, $bits );
+
+ $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
+
+ $wgOut->addHTML( "<ul>\n" );
+
+ $sk = $wgUser->getSkin();
+ foreach ( $contribs as $contrib )
+ $wgOut->addHTML( ucListEdit( $sk, $contrib ) );
+
+ $wgOut->addHTML( "</ul>\n" );
+ $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
+}
+
+/**
+ * Generates the subheading with links
+ * @param $nt @see Title object for the target
+ */
+function contributionsSub( $nt ) {
+ global $wgSysopUserBans, $wgLang, $wgUser;
+
+ $sk = $wgUser->getSkin();
+ $id = User::idFromName( $nt->getText() );
+
+ if ( 0 == $id ) {
+ $ul = $nt->getText();
+ } else {
+ $ul = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ }
+ $talk = $nt->getTalkPage();
+ if( $talk ) {
+ # Talk page link
+ $tools[] = $sk->makeLinkObj( $talk, $wgLang->getNsText( NS_TALK ) );
+ if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
+ # Block link
+ if( $wgUser->isAllowed( 'block' ) )
+ $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Blockip/' . $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
+ # Block log link
+ $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Log' ), htmlspecialchars( LogPage::logName( 'block' ) ), 'type=block&page=' . $nt->getPrefixedUrl() );
+ }
+ # Other logs link
+ $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
+ $ul .= ' (' . implode( ' | ', $tools ) . ')';
+ }
+ return $ul;
+}
+
+/**
+ * Generates the namespace selector form with hidden attributes.
+ * @param $options Array: the options to be included.
+ */
+function contributionsForm( $options ) {
+ global $wgScript, $wgTitle;
+
+ $options['title'] = $wgTitle->getPrefixedText();
+
+ $f = "<form method='get' action=\"$wgScript\">\n";
+ foreach ( $options as $name => $value ) {
+ if( $name === 'namespace') continue;
+ $f .= "\t" . wfElement( 'input', array(
+ 'name' => $name,
+ 'type' => 'hidden',
+ 'value' => $value ) ) . "\n";
+ }
+
+ $f .= '<p>' . wfMsgHtml( 'namespace' ) . ' ' .
+ HTMLnamespaceselector( $options['namespace'], '' ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'value' => wfMsg( 'allpagessubmit' ) )
+ ) .
+ "</p></form>\n";
+
+ return $f;
+}
+
+/**
+ * Generates each row in the contributions list.
+ *
+ * Contributions which are marked "top" are currently on top of the history.
+ * For these contributions, a [rollback] link is shown for users with sysop
+ * privileges. The rollback link restores the most recent version that was not
+ * written by the target user.
+ *
+ * If the contributions page is called with the parameter &bot=1, all rollback
+ * links also get that parameter. It causes the edit itself and the rollback
+ * to be marked as "bot" edits. Bot edits are hidden by default from recent
+ * changes, so this allows sysops to combat a busy vandal without bothering
+ * other users.
+ *
+ * @todo This would probably look a lot nicer in a table.
+ */
+function ucListEdit( $sk, $row ) {
+ $fname = 'ucListEdit';
+ wfProfileIn( $fname );
+
+ global $wgLang, $wgUser, $wgRequest;
+ static $messages;
+ if( !isset( $messages ) ) {
+ foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) {
+ $messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+ }
+ }
+
+ $rev = new Revision( $row );
+
+ $page = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $link = $sk->makeKnownLinkObj( $page );
+ $difftext = $topmarktext = '';
+ if( $row->rev_id == $row->page_latest ) {
+ $topmarktext .= '<strong>' . $messages['uctop'] . '</strong>';
+ if( !$row->page_is_new ) {
+ $difftext .= '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=0' ) . ')';
+ } else {
+ $difftext .= $messages['newarticle'];
+ }
+
+ if( $wgUser->isAllowed( 'rollback' ) ) {
+ $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : '';
+ $extraRollback .= '&token=' . urlencode(
+ $wgUser->editToken( array( $page->getPrefixedText(), $row->rev_user_text ) ) );
+ $topmarktext .= ' ['. $sk->makeKnownLinkObj( $page,
+ $messages['rollbacklink'],
+ 'action=rollback&from=' . urlencode( $row->rev_user_text ) . $extraRollback ) .']';
+ }
+
+ }
+ if( $rev->userCan( Revision::DELETED_TEXT ) ) {
+ $difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
+ } else {
+ $difftext = '(' . $messages['diff'] . ')';
+ }
+ $histlink='('.$sk->makeKnownLinkObj( $page, $messages['hist'], 'action=history' ) . ')';
+
+ $comment = $sk->revComment( $rev );
+ $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $d = '<span class="history-deleted">' . $d . '</span>';
+ }
+
+ if( $row->rev_minor_edit ) {
+ $mflag = '<span class="minor">' . $messages['minoreditletter'] . '</span> ';
+ } else {
+ $mflag = '';
+ }
+
+ $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}";
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ }
+ $ret = "<li>$ret</li>\n";
+ wfProfileOut( $fname );
+ return $ret;
+}
+
+?>
diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php
new file mode 100644
index 00000000..3f4a0519
--- /dev/null
+++ b/includes/SpecialDeadendpages.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class DeadendPagesPage extends PageQueryPage {
+
+ function getName( ) {
+ return "Deadendpages";
+ }
+
+ /**
+ * LEFT JOIN is expensive
+ *
+ * @return true
+ */
+ function isExpensive( ) {
+ return 1;
+ }
+
+ function isSyndicated() { return false; }
+
+ /**
+ * @return false
+ */
+ function sortDescending() {
+ return false;
+ }
+
+ /**
+ * @return string an sqlquery
+ */
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'pagelinks' ) );
+ return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " .
+ "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " .
+ "WHERE pl_from IS NULL " .
+ "AND page_namespace = 0 " .
+ "AND page_is_redirect = 0";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDeadendpages() {
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $depp = new DeadendPagesPage();
+
+ return $depp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php
new file mode 100644
index 00000000..1a0297af
--- /dev/null
+++ b/includes/SpecialDisambiguations.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class DisambiguationsPage extends PageQueryPage {
+
+ function getName() {
+ return 'Disambiguations';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+
+ #FIXME : probably need to add a backlink to the maintenance page.
+ return '<p>'.wfMsg('disambiguationstext', $sk->makeKnownLink(wfMsgForContent('disambiguationspage')) )."</p><br />\n";
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'pagelinks', 'templatelinks' ) );
+
+ $dp = Title::newFromText(wfMsgForContent('disambiguationspage'));
+ $id = $dp->getArticleId();
+ $dns = $dp->getNamespace();
+ $dtitle = $dbr->addQuotes( $dp->getDBkey() );
+
+ if($dns != NS_TEMPLATE) {
+ # FIXME we assume the disambiguation message is a template but
+ # the page can potentially be from another namespace :/
+ wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
+ }
+
+ $sql = "SELECT 'Disambiguations' AS \"type\", pa.page_namespace AS namespace,"
+ ." pa.page_title AS title, la.pl_from AS value"
+ ." FROM {$templatelinks} AS lb, {$page} AS pa, {$pagelinks} AS la"
+ ." WHERE lb.tl_namespace = $dns AND lb.tl_title = $dtitle" # disambiguation template
+ .' AND pa.page_id = lb.tl_from'
+ .' AND pa.page_namespace = la.pl_namespace'
+ .' AND pa.page_title = la.pl_title';
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::newFromId( $result->value );
+ $dp = Title::makeTitle( $result->namespace, $result->title );
+
+ $from = $skin->makeKnownLinkObj( $title,'');
+ $edit = $skin->makeBrokenLinkObj( $title, "(".wfMsg("qbedit").")" , 'redirect=no');
+ $to = $skin->makeKnownLinkObj( $dp,'');
+
+ return "$from $edit => $to";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDisambiguations() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sd = new DisambiguationsPage();
+
+ return $sd->doQuery( $offset, $limit );
+}
+?>
diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php
new file mode 100644
index 00000000..fe480f60
--- /dev/null
+++ b/includes/SpecialDoubleRedirects.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class DoubleRedirectsPage extends PageQueryPage {
+
+ function getName() {
+ return 'DoubleRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ #FIXME : probably need to add a backlink to the maintenance page.
+ return '<p>'.wfMsg("doubleredirectstext")."</p><br />\n";
+ }
+
+ function getSQLText( &$dbr, $namespace = null, $title = null ) {
+
+ extract( $dbr->tableNames( 'page', 'pagelinks' ) );
+
+ $limitToTitle = !( $namespace === null && $title === null );
+ $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ;
+ $sql .=
+ " pa.page_namespace as namespace, pa.page_title as title," .
+ " pb.page_namespace as nsb, pb.page_title as tb," .
+ " pc.page_namespace as nsc, pc.page_title as tc" .
+ " FROM $pagelinks AS la, $pagelinks AS lb, $page AS pa, $page AS pb, $page AS pc" .
+ " WHERE pa.page_is_redirect=1 AND pb.page_is_redirect=1" .
+ " AND la.pl_from=pa.page_id" .
+ " AND la.pl_namespace=pb.page_namespace" .
+ " AND la.pl_title=pb.page_title" .
+ " AND lb.pl_from=pb.page_id" .
+ " AND lb.pl_namespace=pc.page_namespace" .
+ " AND lb.pl_title=pc.page_title";
+
+ if( $limitToTitle ) {
+ $encTitle = $dbr->addQuotes( $title );
+ $sql .= " AND pa.page_namespace=$namespace" .
+ " AND pa.page_title=$encTitle";
+ }
+
+ return $sql;
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $this->getSQLText( $dbr );
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $fname = 'DoubleRedirectsPage::formatResult';
+ $titleA = Title::makeTitle( $result->namespace, $result->title );
+
+ if ( $result && !isset( $result->nsb ) ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $sql = $this->getSQLText( $dbr, $result->namespace, $result->title );
+ $res = $dbr->query( $sql, $fname );
+ if ( $res ) {
+ $result = $dbr->fetchObject( $res );
+ $dbr->freeResult( $res );
+ }
+ }
+ if ( !$result ) {
+ return '';
+ }
+
+ $titleB = Title::makeTitle( $result->nsb, $result->tb );
+ $titleC = Title::makeTitle( $result->nsc, $result->tc );
+
+ $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' );
+ $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
+ $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
+ $linkC = $skin->makeKnownLinkObj( $titleC );
+ $arr = $wgContLang->isRTL() ? '&larr;' : '&rarr;';
+
+ return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialDoubleRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sdr = new DoubleRedirectsPage();
+
+ return $sdr->doQuery( $offset, $limit );
+
+}
+?>
diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php
new file mode 100644
index 00000000..c66389e1
--- /dev/null
+++ b/includes/SpecialEmailuser.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once('UserMailer.php');
+
+function wfSpecialEmailuser( $par ) {
+ global $wgUser, $wgOut, $wgRequest, $wgEnableEmail, $wgEnableUserEmail;
+
+ if( !( $wgEnableEmail && $wgEnableUserEmail ) ) {
+ $wgOut->showErrorPage( "nosuchspecialpage", "nospecialpagetext" );
+ return;
+ }
+
+ if( !$wgUser->canSendEmail() ) {
+ wfDebug( "User can't send.\n" );
+ $wgOut->showErrorPage( "mailnologin", "mailnologintext" );
+ return;
+ }
+
+ $action = $wgRequest->getVal( 'action' );
+ $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+ if ( "" == $target ) {
+ wfDebug( "Target is empty.\n" );
+ $wgOut->showErrorPage( "notargettitle", "notargettext" );
+ return;
+ }
+
+ $nt = Title::newFromURL( $target );
+ if ( is_null( $nt ) ) {
+ wfDebug( "Target is invalid title.\n" );
+ $wgOut->showErrorPage( "notargettitle", "notargettext" );
+ return;
+ }
+
+ $nu = User::newFromName( $nt->getText() );
+ if( is_null( $nu ) || !$nu->canReceiveEmail() ) {
+ wfDebug( "Target is invalid user or can't receive.\n" );
+ $wgOut->showErrorPage( "noemailtitle", "noemailtext" );
+ return;
+ }
+
+ $f = new EmailUserForm( $nu );
+
+ if ( "success" == $action ) {
+ $f->showSuccess();
+ } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm();
+ }
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class EmailUserForm {
+
+ var $target;
+ var $text, $subject;
+
+ /**
+ * @param User $target
+ */
+ function EmailUserForm( $target ) {
+ global $wgRequest;
+ $this->target = $target;
+ $this->text = $wgRequest->getText( 'wpText' );
+ $this->subject = $wgRequest->getText( 'wpSubject' );
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( "emailpage" ) );
+ $wgOut->addWikiText( wfMsg( "emailpagetext" ) );
+
+ if ( $this->subject === "" ) {
+ $this->subject = wfMsg( "defemailsubject" );
+ }
+
+ $emf = wfMsg( "emailfrom" );
+ $sender = $wgUser->getName();
+ $emt = wfMsg( "emailto" );
+ $rcpt = $this->target->getName();
+ $emr = wfMsg( "emailsubject" );
+ $emm = wfMsg( "emailmessage" );
+ $ems = wfMsg( "emailsend" );
+ $encSubject = htmlspecialchars( $this->subject );
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" );
+ $action = $titleObj->escapeLocalURL( "target=" .
+ urlencode( $this->target->getName() ) . "&action=submit" );
+ $token = $wgUser->editToken();
+
+ $wgOut->addHTML( "
+<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
+<table border='0' id='mailheader'><tr>
+<td align='right'>{$emf}:</td>
+<td align='left'><strong>" . htmlspecialchars( $sender ) . "</strong></td>
+</tr><tr>
+<td align='right'>{$emt}:</td>
+<td align='left'><strong>" . htmlspecialchars( $rcpt ) . "</strong></td>
+</tr><tr>
+<td align='right'>{$emr}:</td>
+<td align='left'>
+<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" />
+</td>
+</tr>
+</table>
+<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
+<textarea name=\"wpText\" rows='20' cols='80' wrap='virtual' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
+"</textarea>
+<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
+<input type='hidden' name='wpEditToken' value=\"$token\" />
+</form>\n" );
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser;
+
+ $to = new MailAddress( $this->target );
+ $from = new MailAddress( $wgUser );
+ $subject = $this->subject;
+
+ if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
+
+ $mailResult = userMailer( $to, $from, $subject, $this->text );
+
+ if( WikiError::isError( $mailResult ) ) {
+ $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult);
+ } else {
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" );
+ $encTarget = wfUrlencode( $this->target->getName() );
+ $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) );
+ wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) );
+ }
+ }
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( "emailsent" ) );
+ $wgOut->addHTML( wfMsg( "emailsenttext" ) );
+
+ $wgOut->returnToMain( false );
+ }
+}
+?>
diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php
new file mode 100644
index 00000000..73dcbcd5
--- /dev/null
+++ b/includes/SpecialExport.php
@@ -0,0 +1,106 @@
+<?php
+# Copyright (C) 2003 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
+ */
+
+/** */
+require_once( 'Export.php' );
+
+/**
+ *
+ */
+function wfSpecialExport( $page = '' ) {
+ global $wgOut, $wgRequest, $wgExportAllowListContributors;
+ global $wgExportAllowHistory, $wgExportMaxHistory;
+
+ $curonly = true;
+ if( $wgRequest->getVal( 'action' ) == 'submit') {
+ $page = $wgRequest->getText( 'pages' );
+ $curonly = $wgRequest->getCheck( 'curonly' );
+ }
+ if( $wgRequest->getCheck( 'history' ) ) {
+ $curonly = false;
+ }
+ if( !$wgExportAllowHistory ) {
+ // Override
+ $curonly = true;
+ }
+
+ $list_authors = $wgRequest->getCheck( 'listauthors' );
+ if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
+
+ if( $page != '' ) {
+ $wgOut->disable();
+
+ // Cancel output buffering and gzipping if set
+ // This should provide safer streaming for pages with history
+ while( $status = ob_get_status() ) {
+ ob_end_clean();
+ if( $status['name'] == 'ob_gzhandler' ) {
+ header( 'Content-Encoding:' );
+ }
+ }
+ header( "Content-type: application/xml; charset=utf-8" );
+ $pages = explode( "\n", $page );
+
+ $db =& wfGetDB( DB_SLAVE );
+ $history = $curonly ? MW_EXPORT_CURRENT : MW_EXPORT_FULL;
+ $exporter = new WikiExporter( $db, $history );
+ $exporter->list_authors = $list_authors ;
+ $exporter->openStream();
+
+ foreach( $pages as $page ) {
+ if( $wgExportMaxHistory && !$curonly ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $count = Revision::countByTitle( $db, $title );
+ if( $count > $wgExportMaxHistory ) {
+ wfDebug( __FUNCTION__ .
+ ": Skipped $page, $count revisions too big\n" );
+ continue;
+ }
+ }
+ }
+ $exporter->pageByName( $page );
+ }
+
+ $exporter->closeStream();
+ return;
+ }
+
+ $wgOut->addWikiText( wfMsg( "exporttext" ) );
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Export" );
+
+ $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalUrl() ) );
+ $form .= wfOpenElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ) . '</textarea><br />';
+ if( $wgExportAllowHistory ) {
+ $form .= wfCheck( 'curonly', true, array( 'value' => 'true', 'id' => 'curonly' ) );
+ $form .= wfLabel( wfMsg( 'exportcuronly' ), 'curonly' ) . '<br />';
+ } else {
+ $wgOut->addWikiText( wfMsg( 'exportnohistory' ) );
+ }
+ $form .= wfHidden( 'action', 'submit' );
+ $form .= wfSubmitButton( wfMsg( 'export-submit' ) ) . '</form>';
+ $wgOut->addHtml( $form );
+}
+
+?>
diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php
new file mode 100644
index 00000000..e456abf5
--- /dev/null
+++ b/includes/SpecialImagelist.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialImagelist() {
+ global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgMiserMode;
+
+ $sort = $wgRequest->getVal( 'sort' );
+ $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $sql = "SELECT img_size,img_name,img_user,img_user_text," .
+ "img_description,img_timestamp FROM $image";
+
+ if ( !$wgMiserMode && !empty( $wpIlMatch ) ) {
+ $nt = Title::newFromUrl( $wpIlMatch );
+ if($nt ) {
+ $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+ $m = str_replace( "%", "\\%", $m );
+ $m = str_replace( "_", "\\_", $m );
+ $sql .= " WHERE LCASE(img_name) LIKE '%{$m}%'";
+ }
+ }
+
+ if ( "bysize" == $sort ) {
+ $sql .= " ORDER BY img_size DESC";
+ } else if ( "byname" == $sort ) {
+ $sql .= " ORDER BY img_name";
+ } else {
+ $sort = "bydate";
+ $sql .= " ORDER BY img_timestamp DESC";
+ }
+
+ list( $limit, $offset ) = wfCheckLimits( 50 );
+ $lt = $wgLang->formatNum( "${limit}" );
+ $sql .= " LIMIT {$limit}";
+
+ $wgOut->addWikiText( wfMsg( 'imglegend' ) );
+ $wgOut->addHTML( wfMsgExt( 'imagelisttext', array('parse'), $lt, wfMsg( $sort ) ) );
+
+ $sk = $wgUser->getSkin();
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Imagelist" );
+ $action = $titleObj->escapeLocalURL( "sort={$sort}&limit={$limit}" );
+
+ if ( !$wgMiserMode ) {
+ $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
+ "{$action}\">" .
+ wfElement( 'input',
+ array(
+ 'type' => 'text',
+ 'size' => '20',
+ 'name' => 'wpIlMatch',
+ 'value' => $wpIlMatch, )) .
+ wfElement( 'input',
+ array(
+ 'type' => 'submit',
+ 'name' => 'wpIlSubmit',
+ 'value' => wfMsg( 'ilsubmit'), )) .
+ '</form>' );
+ }
+
+ $here = Title::makeTitle( NS_SPECIAL, 'Imagelist' );
+
+ foreach ( array( 'byname', 'bysize', 'bydate') as $sorttype ) {
+ $urls = null;
+ foreach ( array( 50, 100, 250, 500 ) as $num ) {
+ $urls[] = $sk->makeKnownLinkObj( $here, $wgLang->formatNum( $num ),
+ "sort={$sorttype}&limit={$num}&wpIlMatch=" . urlencode( $wpIlMatch ) );
+ }
+ $sortlinks[] = wfMsgExt(
+ 'showlast',
+ array( 'parseinline', 'replaceafter' ),
+ implode($urls, ' | '),
+ wfMsgExt( $sorttype, array('escape') )
+ );
+ }
+ $wgOut->addHTML( implode( $sortlinks, "<br />\n") . "\n\n<hr />" );
+
+ // lines
+ $wgOut->addHTML( '<p>' );
+ $res = $dbr->query( $sql, "wfSpecialImagelist" );
+
+ while ( $s = $dbr->fetchObject( $res ) ) {
+ $name = $s->img_name;
+ $ut = $s->img_user_text;
+ if ( 0 == $s->img_user ) {
+ $ul = $ut;
+ } else {
+ $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
+ }
+
+ $dirmark = $wgContLang->getDirMark(); // to keep text in correct direction
+
+ $ilink = "<a href=\"" . htmlspecialchars( Image::imageUrl( $name ) ) .
+ "\">" . strtr(htmlspecialchars( $name ), '_', ' ') . "</a>";
+
+ $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $s->img_size ) );
+
+ $desc = $sk->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ),
+ wfMsg( 'imgdesc' ) );
+
+ $date = $wgLang->timeanddate( $s->img_timestamp, true );
+ $comment = $sk->commentBlock( $s->img_description );
+
+ $l = "({$desc}) {$dirmark}{$ilink} . . {$dirmark}{$nb} . . {$dirmark}{$ul}".
+ " . . {$dirmark}{$date} . . {$dirmark}{$comment}<br />\n";
+ $wgOut->addHTML( $l );
+ }
+
+ $dbr->freeResult( $res );
+ $wgOut->addHTML( '</p>' );
+}
+
+?>
diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php
new file mode 100644
index 00000000..7976d6c8
--- /dev/null
+++ b/includes/SpecialImport.php
@@ -0,0 +1,848 @@
+<?php
+/**
+ * MediaWiki page data importer
+ * Copyright (C) 2003,2005 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
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialImport( $page = '' ) {
+ global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
+ global $wgImportTargetNamespace;
+
+ $interwiki = false;
+ $namespace = $wgImportTargetNamespace;
+ $frompage = '';
+ $history = true;
+
+ if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
+ $isUpload = false;
+ $namespace = $wgRequest->getIntOrNull( 'namespace' );
+
+ switch( $wgRequest->getVal( "source" ) ) {
+ case "upload":
+ $isUpload = true;
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $source = ImportStreamSource::newFromUpload( "xmlimport" );
+ } else {
+ return $wgOut->permissionRequired( 'importupload' );
+ }
+ break;
+ case "interwiki":
+ $interwiki = $wgRequest->getVal( 'interwiki' );
+ $history = $wgRequest->getCheck( 'interwikiHistory' );
+ $frompage = $wgRequest->getText( "frompage" );
+ $source = ImportStreamSource::newFromInterwiki(
+ $interwiki,
+ $frompage,
+ $history );
+ break;
+ default:
+ $source = new WikiErrorMsg( "importunknownsource" );
+ }
+
+ if( WikiError::isError( $source ) ) {
+ $wgOut->addWikiText( wfEscapeWikiText( $source->getMessage() ) );
+ } else {
+ $wgOut->addWikiText( wfMsg( "importstart" ) );
+
+ $importer = new WikiImporter( $source );
+ if( !is_null( $namespace ) ) {
+ $importer->setTargetNamespace( $namespace );
+ }
+ $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
+
+ $reporter->open();
+ $result = $importer->doImport();
+ $reporter->close();
+
+ if( WikiError::isError( $result ) ) {
+ $wgOut->addWikiText( wfMsg( "importfailed",
+ wfEscapeWikiText( $result->getMessage() ) ) );
+ } else {
+ # Success!
+ $wgOut->addWikiText( wfMsg( "importsuccess" ) );
+ }
+ }
+ }
+
+ $action = $wgTitle->escapeLocalUrl( 'action=submit' );
+
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $wgOut->addWikiText( wfMsg( "importtext" ) );
+ $wgOut->addHTML( "
+<fieldset>
+ <legend>" . wfMsgHtml('upload') . "</legend>
+ <form enctype='multipart/form-data' method='post' action=\"$action\">
+ <input type='hidden' name='action' value='submit' />
+ <input type='hidden' name='source' value='upload' />
+ <input type='hidden' name='MAX_FILE_SIZE' value='2000000' />
+ <input type='file' name='xmlimport' value='' size='30' />
+ <input type='submit' value=\"" . wfMsgHtml( "uploadbtn" ) . "\" />
+ </form>
+</fieldset>
+" );
+ } else {
+ if( empty( $wgImportSources ) ) {
+ $wgOut->addWikiText( wfMsg( 'importnosources' ) );
+ }
+ }
+
+ if( !empty( $wgImportSources ) ) {
+ $wgOut->addHTML( "
+<fieldset>
+ <legend>" . wfMsgHtml('importinterwiki') . "</legend>
+ <form method='post' action=\"$action\">" .
+ $wgOut->parse( wfMsg( 'import-interwiki-text' ) ) . "
+ <input type='hidden' name='action' value='submit' />
+ <input type='hidden' name='source' value='interwiki' />
+ <table>
+ <tr>
+ <td>
+ <select name='interwiki'>" );
+ foreach( $wgImportSources as $prefix ) {
+ $iw = htmlspecialchars( $prefix );
+ $selected = ($interwiki === $prefix) ? ' selected="selected"' : '';
+ $wgOut->addHTML( "<option value=\"$iw\"$selected>$iw</option>\n" );
+ }
+ $wgOut->addHTML( "
+ </select>
+ </td>
+ <td>" .
+ wfInput( 'frompage', 50, $frompage ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>" .
+ wfCheckLabel( wfMsg( 'import-interwiki-history' ),
+ 'interwikiHistory', 'interwikiHistory', $history ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>
+ " . wfMsgHtml( 'import-interwiki-namespace' ) . " " .
+ HTMLnamespaceselector( $namespace, '' ) . "
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>" .
+ wfSubmitButton( wfMsg( 'import-interwiki-submit' ) ) .
+ "</td>
+ </tr>
+ </table>
+ </form>
+</fieldset>
+" );
+ }
+}
+
+/**
+ * Reporting callback
+ */
+class ImportReporter {
+ function __construct( $importer, $upload, $interwiki ) {
+ $importer->setPageOutCallback( array( $this, 'reportPage' ) );
+ $this->mPageCount = 0;
+ $this->mIsUpload = $upload;
+ $this->mInterwiki = $interwiki;
+ }
+
+ function open() {
+ global $wgOut;
+ $wgOut->addHtml( "<ul>\n" );
+ }
+
+ function reportPage( $title, $origTitle, $revisionCount ) {
+ global $wgOut, $wgUser, $wgLang, $wgContLang;
+
+ $skin = $wgUser->getSkin();
+
+ $this->mPageCount++;
+
+ $localCount = $wgLang->formatNum( $revisionCount );
+ $contentCount = $wgContLang->formatNum( $revisionCount );
+
+ $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) .
+ " " .
+ wfMsgHtml( 'import-revision-count', $localCount ) .
+ "</li>\n" );
+
+ $log = new LogPage( 'import' );
+ if( $this->mIsUpload ) {
+ $detail = wfMsgForContent( 'import-logentry-upload-detail',
+ $contentCount );
+ $log->addEntry( 'upload', $title, $detail );
+ } else {
+ $interwiki = '[[:' . $this->mInterwiki . ':' .
+ $origTitle->getPrefixedText() . ']]';
+ $detail = wfMsgForContent( 'import-logentry-interwiki-detail',
+ $contentCount, $interwiki );
+ $log->addEntry( 'interwiki', $title, $detail );
+ }
+
+ $comment = $detail; // quick
+ $dbw = wfGetDB( DB_MASTER );
+ $nullRevision = Revision::newNullRevision(
+ $dbw, $title->getArticleId(), $comment, true );
+ $nullRevId = $nullRevision->insertOn( $dbw );
+ }
+
+ function close() {
+ global $wgOut;
+ if( $this->mPageCount == 0 ) {
+ $wgOut->addHtml( "<li>" . wfMsgHtml( 'importnopages' ) . "</li>\n" );
+ }
+ $wgOut->addHtml( "</ul>\n" );
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class WikiRevision {
+ var $title = null;
+ var $id = 0;
+ var $timestamp = "20010115000000";
+ var $user = 0;
+ var $user_text = "";
+ var $text = "";
+ var $comment = "";
+ var $minor = false;
+
+ function setTitle( $title ) {
+ if( is_object( $title ) ) {
+ $this->title = $title;
+ } elseif( is_null( $title ) ) {
+ throw new MWException( "WikiRevision given a null title in import." );
+ } else {
+ throw new MWException( "WikiRevision given non-object title in import." );
+ }
+ }
+
+ function setID( $id ) {
+ $this->id = $id;
+ }
+
+ function setTimestamp( $ts ) {
+ # 2003-08-05T18:30:02Z
+ $this->timestamp = wfTimestamp( TS_MW, $ts );
+ }
+
+ function setUsername( $user ) {
+ $this->user_text = $user;
+ }
+
+ function setUserIP( $ip ) {
+ $this->user_text = $ip;
+ }
+
+ function setText( $text ) {
+ $this->text = $text;
+ }
+
+ function setComment( $text ) {
+ $this->comment = $text;
+ }
+
+ function setMinor( $minor ) {
+ $this->minor = (bool)$minor;
+ }
+
+ function getTitle() {
+ return $this->title;
+ }
+
+ function getID() {
+ return $this->id;
+ }
+
+ function getTimestamp() {
+ return $this->timestamp;
+ }
+
+ function getUser() {
+ return $this->user_text;
+ }
+
+ function getText() {
+ return $this->text;
+ }
+
+ function getComment() {
+ return $this->comment;
+ }
+
+ function getMinor() {
+ return $this->minor;
+ }
+
+ function importOldRevision() {
+ $fname = "WikiImporter::importOldRevision";
+ $dbw =& wfGetDB( DB_MASTER );
+
+ # Sneak a single revision into place
+ $user = User::newFromName( $this->getUser() );
+ if( $user ) {
+ $userId = intval( $user->getId() );
+ $userText = $user->getName();
+ } else {
+ $userId = 0;
+ $userText = $this->getUser();
+ }
+
+ // avoid memory leak...?
+ $linkCache =& LinkCache::singleton();
+ $linkCache->clear();
+
+ $article = new Article( $this->title );
+ $pageId = $article->getId();
+ if( $pageId == 0 ) {
+ # must create the page...
+ $pageId = $article->insertOn( $dbw );
+ $created = true;
+ } else {
+ $created = false;
+ }
+
+ # FIXME: Check for exact conflicts
+ # FIXME: Use original rev_id optionally
+ # FIXME: blah blah blah
+
+ #if( $numrows > 0 ) {
+ # return wfMsg( "importhistoryconflict" );
+ #}
+
+ # Insert the row
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'text' => $this->getText(),
+ 'comment' => $this->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $this->timestamp,
+ 'minor_edit' => $this->minor,
+ ) );
+ $revId = $revision->insertOn( $dbw );
+ $changed = $article->updateIfNewerOn( $dbw, $revision );
+
+ if( $created ) {
+ wfDebug( __METHOD__ . ": running onArticleCreate\n" );
+ Article::onArticleCreate( $this->title );
+ } else {
+ if( $changed ) {
+ wfDebug( __METHOD__ . ": running onArticleEdit\n" );
+ Article::onArticleEdit( $this->title );
+ }
+ }
+ if( $created || $changed ) {
+ wfDebug( __METHOD__ . ": running edit updates\n" );
+ $article->editUpdates(
+ $this->getText(),
+ $this->getComment(),
+ $this->minor,
+ $this->timestamp,
+ $revId );
+ }
+
+ return true;
+ }
+
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class WikiImporter {
+ var $mSource = null;
+ var $mPageCallback = null;
+ var $mPageOutCallback = null;
+ var $mRevisionCallback = null;
+ var $mTargetNamespace = null;
+ var $lastfield;
+
+ function WikiImporter( $source ) {
+ $this->setRevisionCallback( array( &$this, "importRevision" ) );
+ $this->mSource = $source;
+ }
+
+ function throwXmlError( $err ) {
+ $this->debug( "FAILURE: $err" );
+ wfDebug( "WikiImporter XML error: $err\n" );
+ }
+
+ # --------------
+
+ function doImport() {
+ if( empty( $this->mSource ) ) {
+ return new WikiErrorMsg( "importnotext" );
+ }
+
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ xml_set_object( $parser, $this );
+ xml_set_element_handler( $parser, "in_start", "" );
+
+ $offset = 0; // for context extraction on error reporting
+ do {
+ $chunk = $this->mSource->readChunk();
+ if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
+ wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
+ return new WikiXmlError( $parser, 'XML import parse failure', $chunk, $offset );
+ }
+ $offset += strlen( $chunk );
+ } while( $chunk !== false && !$this->mSource->atEnd() );
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ function debug( $data ) {
+ #wfDebug( "IMPORT: $data\n" );
+ }
+
+ function notice( $data ) {
+ global $wgCommandLineMode;
+ if( $wgCommandLineMode ) {
+ print "$data\n";
+ } else {
+ global $wgOut;
+ $wgOut->addHTML( "<li>$data</li>\n" );
+ }
+ }
+
+ /**
+ * Sets the action to perform as each new page in the stream is reached.
+ * @param callable $callback
+ * @return callable
+ */
+ function setPageCallback( $callback ) {
+ $previous = $this->mPageCallback;
+ $this->mPageCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page in the stream is completed.
+ * Callback accepts the page title (as a Title object), a second object
+ * with the original title form (in case it's been overridden into a
+ * local namespace), and a count of revisions.
+ *
+ * @param callable $callback
+ * @return callable
+ */
+ function setPageOutCallback( $callback ) {
+ $previous = $this->mPageOutCallback;
+ $this->mPageOutCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page revision is reached.
+ * @param callable $callback
+ * @return callable
+ */
+ function setRevisionCallback( $callback ) {
+ $previous = $this->mRevisionCallback;
+ $this->mRevisionCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Set a target namespace to override the defaults
+ */
+ function setTargetNamespace( $namespace ) {
+ if( is_null( $namespace ) ) {
+ // Don't override namespaces
+ $this->mTargetNamespace = null;
+ } elseif( $namespace >= 0 ) {
+ // FIXME: Check for validity
+ $this->mTargetNamespace = intval( $namespace );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Default per-revision callback, performs the import.
+ * @param WikiRevision $revision
+ * @private
+ */
+ function importRevision( &$revision ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->deadlockLoop( array( &$revision, 'importOldRevision' ) );
+ }
+
+ /**
+ * Alternate per-revision callback, for debugging.
+ * @param WikiRevision $revision
+ * @private
+ */
+ function debugRevisionHandler( &$revision ) {
+ $this->debug( "Got revision:" );
+ if( is_object( $revision->title ) ) {
+ $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
+ } else {
+ $this->debug( "-- Title: <invalid>" );
+ }
+ $this->debug( "-- User: " . $revision->user_text );
+ $this->debug( "-- Timestamp: " . $revision->timestamp );
+ $this->debug( "-- Comment: " . $revision->comment );
+ $this->debug( "-- Text: " . $revision->text );
+ }
+
+ /**
+ * Notify the callback function when a new <page> is reached.
+ * @param Title $title
+ * @private
+ */
+ function pageCallback( $title ) {
+ if( is_callable( $this->mPageCallback ) ) {
+ call_user_func( $this->mPageCallback, $title );
+ }
+ }
+
+ /**
+ * Notify the callback function when a </page> is closed.
+ * @param Title $title
+ * @param Title $origTitle
+ * @param int $revisionCount
+ * @private
+ */
+ function pageOutCallback( $title, $origTitle, $revisionCount ) {
+ if( is_callable( $this->mPageOutCallback ) ) {
+ call_user_func( $this->mPageOutCallback, $title, $origTitle,
+ $revisionCount );
+ }
+ }
+
+
+ # XML parser callbacks from here out -- beware!
+ function donothing( $parser, $x, $y="" ) {
+ #$this->debug( "donothing" );
+ }
+
+ function in_start( $parser, $name, $attribs ) {
+ $this->debug( "in_start $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+
+ function in_mediawiki( $parser, $name, $attribs ) {
+ $this->debug( "in_mediawiki $name" );
+ if( $name == 'siteinfo' ) {
+ xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
+ } elseif( $name == 'page' ) {
+ $this->workRevisionCount = 0;
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+ } else {
+ return $this->throwXMLerror( "Expected <page>, got <$name>" );
+ }
+ }
+ function out_mediawiki( $parser, $name ) {
+ $this->debug( "out_mediawiki $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "donothing", "donothing" );
+ }
+
+
+ function in_siteinfo( $parser, $name, $attribs ) {
+ // no-ops for now
+ $this->debug( "in_siteinfo $name" );
+ switch( $name ) {
+ case "sitename":
+ case "base":
+ case "generator":
+ case "case":
+ case "namespaces":
+ case "namespace":
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
+ }
+ }
+
+ function out_siteinfo( $parser, $name ) {
+ if( $name == "siteinfo" ) {
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+ }
+
+
+ function in_page( $parser, $name, $attribs ) {
+ $this->debug( "in_page $name" );
+ switch( $name ) {
+ case "id":
+ case "title":
+ case "restrictions":
+ $this->appendfield = $name;
+ $this->appenddata = "";
+ $this->parenttag = "page";
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "revision":
+ $this->workRevision = new WikiRevision;
+ $this->workRevision->setTitle( $this->pageTitle );
+ $this->workRevisionCount++;
+ xml_set_element_handler( $parser, "in_revision", "out_revision" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
+ }
+ }
+
+ function out_page( $parser, $name ) {
+ $this->debug( "out_page $name" );
+ if( $name != "page" ) {
+ return $this->throwXMLerror( "Expected </page>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+
+ $this->pageOutCallback( $this->pageTitle, $this->origTitle,
+ $this->workRevisionCount );
+
+ $this->workTitle = null;
+ $this->workRevision = null;
+ $this->workRevisionCount = 0;
+ $this->pageTitle = null;
+ $this->origTitle = null;
+ }
+
+ function in_nothing( $parser, $name, $attribs ) {
+ $this->debug( "in_nothing $name" );
+ return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
+ }
+ function char_append( $parser, $data ) {
+ $this->debug( "char_append '$data'" );
+ $this->appenddata .= $data;
+ }
+ function out_append( $parser, $name ) {
+ $this->debug( "out_append $name" );
+ if( $name != $this->appendfield ) {
+ return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_$this->parenttag", "out_$this->parenttag" );
+ xml_set_character_data_handler( $parser, "donothing" );
+
+ switch( $this->appendfield ) {
+ case "title":
+ $this->workTitle = $this->appenddata;
+ $this->origTitle = Title::newFromText( $this->workTitle );
+ if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
+ $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
+ $this->origTitle->getDbKey() );
+ } else {
+ $this->pageTitle = Title::newFromText( $this->workTitle );
+ }
+ $this->pageCallback( $this->workTitle );
+ break;
+ case "id":
+ if ( $this->parenttag == 'revision' ) {
+ $this->workRevision->setID( $this->appenddata );
+ }
+ break;
+ case "text":
+ $this->workRevision->setText( $this->appenddata );
+ break;
+ case "username":
+ $this->workRevision->setUsername( $this->appenddata );
+ break;
+ case "ip":
+ $this->workRevision->setUserIP( $this->appenddata );
+ break;
+ case "timestamp":
+ $this->workRevision->setTimestamp( $this->appenddata );
+ break;
+ case "comment":
+ $this->workRevision->setComment( $this->appenddata );
+ break;
+ case "minor":
+ $this->workRevision->setMinor( true );
+ break;
+ default:
+ $this->debug( "Bad append: {$this->appendfield}" );
+ }
+ $this->appendfield = "";
+ $this->appenddata = "";
+ }
+
+ function in_revision( $parser, $name, $attribs ) {
+ $this->debug( "in_revision $name" );
+ switch( $name ) {
+ case "id":
+ case "timestamp":
+ case "comment":
+ case "minor":
+ case "text":
+ $this->parenttag = "revision";
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
+ }
+ }
+
+ function out_revision( $parser, $name ) {
+ $this->debug( "out_revision $name" );
+ if( $name != "revision" ) {
+ return $this->throwXMLerror( "Expected </revision>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+
+ $out = call_user_func_array( $this->mRevisionCallback,
+ array( &$this->workRevision, &$this ) );
+ if( !empty( $out ) ) {
+ global $wgOut;
+ $wgOut->addHTML( "<li>" . $out . "</li>\n" );
+ }
+ }
+
+ function in_contributor( $parser, $name, $attribs ) {
+ $this->debug( "in_contributor $name" );
+ switch( $name ) {
+ case "username":
+ case "ip":
+ case "id":
+ $this->parenttag = "contributor";
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ default:
+ $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
+ }
+ }
+
+ function out_contributor( $parser, $name ) {
+ $this->debug( "out_contributor $name" );
+ if( $name != "contributor" ) {
+ return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_revision", "out_revision" );
+ }
+
+}
+
+/** @package MediaWiki */
+class ImportStringSource {
+ function ImportStringSource( $string ) {
+ $this->mString = $string;
+ $this->mRead = false;
+ }
+
+ function atEnd() {
+ return $this->mRead;
+ }
+
+ function readChunk() {
+ if( $this->atEnd() ) {
+ return false;
+ } else {
+ $this->mRead = true;
+ return $this->mString;
+ }
+ }
+}
+
+/** @package MediaWiki */
+class ImportStreamSource {
+ function ImportStreamSource( $handle ) {
+ $this->mHandle = $handle;
+ }
+
+ function atEnd() {
+ return feof( $this->mHandle );
+ }
+
+ function readChunk() {
+ return fread( $this->mHandle, 32768 );
+ }
+
+ function newFromFile( $filename ) {
+ $file = @fopen( $filename, 'rt' );
+ if( !$file ) {
+ return new WikiErrorMsg( "importcantopen" );
+ }
+ return new ImportStreamSource( $file );
+ }
+
+ function newFromUpload( $fieldname = "xmlimport" ) {
+ $upload =& $_FILES[$fieldname];
+
+ if( !isset( $upload ) || !$upload['name'] ) {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ if( !empty( $upload['error'] ) ) {
+ return new WikiErrorMsg( 'importuploaderror', $upload['error'] );
+ }
+ $fname = $upload['tmp_name'];
+ if( is_uploaded_file( $fname ) ) {
+ return ImportStreamSource::newFromFile( $fname );
+ } else {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ }
+
+ function newFromURL( $url ) {
+ wfDebug( __METHOD__ . ": opening $url\n" );
+ # fopen-wrappers are normally turned off for security.
+ ini_set( "allow_url_fopen", true );
+ $ret = ImportStreamSource::newFromFile( $url );
+ ini_set( "allow_url_fopen", false );
+ return $ret;
+ }
+
+ function newFromInterwiki( $interwiki, $page, $history=false ) {
+ $base = Title::getInterwikiLink( $interwiki );
+ $link = Title::newFromText( "$interwiki:Special:Export/$page" );
+ if( empty( $base ) || empty( $link ) ) {
+ return new WikiErrorMsg( 'importbadinterwiki' );
+ } else {
+ $params = $history ? 'history=1' : '';
+ $url = $link->getFullUrl( $params );
+ return ImportStreamSource::newFromURL( $url );
+ }
+ }
+}
+
+
+?>
diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php
new file mode 100644
index 00000000..cc5c805c
--- /dev/null
+++ b/includes/SpecialIpblocklist.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialIpblocklist() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
+ $reason = $wgRequest->getText( 'wpUnblockReason' );
+ $action = $wgRequest->getText( 'action' );
+
+ $ipu = new IPUnblockForm( $ip, $reason );
+
+ if ( "success" == $action ) {
+ $ipu->showList( wfMsgWikiHtml( 'unblocked', htmlspecialchars( $ip ) ) );
+ } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ if ( ! $wgUser->isAllowed('block') ) {
+ $wgOut->sysopRequired();
+ return;
+ }
+ $ipu->doSubmit();
+ } else if ( "unblock" == $action ) {
+ $ipu->showForm( "" );
+ } else {
+ $ipu->showList( "" );
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class IPUnblockForm {
+ var $ip, $reason;
+
+ function IPUnblockForm( $ip, $reason ) {
+ $this->ip = $ip;
+ $this->reason = $reason;
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'unblockip' ) );
+ $wgOut->addWikiText( wfMsg( 'unblockiptext' ) );
+
+ $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
+ $ipr = wfMsgHtml( 'ipbreason' );
+ $ipus = wfMsgHtml( 'ipusubmit' );
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" );
+ $action = $titleObj->escapeLocalURL( "action=submit" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addWikitext( "<span class='error'>{$err}</span>\n" );
+ }
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( "
+<form id=\"unblockip\" method=\"post\" action=\"{$action}\">
+ <table border='0'>
+ <tr>
+ <td align='right'>{$ipa}:</td>
+ <td align='left'>
+ <input tabindex='1' type='text' size='20' name=\"wpUnblockAddress\" value=\"" . htmlspecialchars( $this->ip ) . "\" />
+ </td>
+ </tr>
+ <tr>
+ <td align='right'>{$ipr}:</td>
+ <td align='left'>
+ <input tabindex='1' type='text' size='40' name=\"wpUnblockReason\" value=\"" . htmlspecialchars( $this->reason ) . "\" />
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align='left'>
+ <input tabindex='2' type='submit' name=\"wpBlock\" value=\"{$ipus}\" />
+ </td>
+ </tr>
+ </table>
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+</form>\n" );
+
+ }
+
+ function doSubmit() {
+ global $wgOut;
+
+ $block = new Block();
+ $this->ip = trim( $this->ip );
+
+ if ( $this->ip{0} == "#" ) {
+ $block->mId = substr( $this->ip, 1 );
+ } else {
+ $block->mAddress = $this->ip;
+ }
+
+ # Delete block (if it exists)
+ # We should probably check for errors rather than just declaring success
+ $block->delete();
+
+ # Make log entry
+ $log = new LogPage( 'block' );
+ $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason );
+
+ # Report to the user
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" );
+ $success = $titleObj->getFullURL( "action=success&ip=" . urlencode( $this->ip ) );
+ $wgOut->redirect( $success );
+ }
+
+ function showList( $msg ) {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+ global $wgRequest;
+ list( $this->limit, $this->offset ) = $wgRequest->getLimitOffset();
+ $this->counter = 0;
+
+ $paging = '<p>' . wfViewPrevNext( $this->offset, $this->limit,
+ Title::makeTitle( NS_SPECIAL, 'Ipblocklist' ),
+ 'ip=' . urlencode( $this->ip ) ) . "</p>\n";
+ $wgOut->addHTML( $paging );
+
+ $search = $this->searchForm();
+ $wgOut->addHTML( $search );
+
+ $wgOut->addHTML( "<ul>" );
+ if( !Block::enumBlocks( array( &$this, "addRow" ), 0 ) ) {
+ // FIXME hack to solve #bug 1487
+ $wgOut->addHTML( '<li>'.wfMsgHtml( 'ipblocklistempty' ).'</li>' );
+ }
+ $wgOut->addHTML( "</ul>\n" );
+ $wgOut->addHTML( $paging );
+ }
+
+ function searchForm() {
+ global $wgTitle;
+ return
+ wfElement( 'form', array(
+ 'action' => $wgTitle->getLocalUrl() ),
+ null ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'action',
+ 'value' => 'search' ) ).
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'limit',
+ 'value' => $this->limit ) ).
+ wfElement( 'input', array(
+ 'name' => 'ip',
+ 'value' => $this->ip ) ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'value' => wfMsg( 'search' ) ) ) .
+ '</form>';
+ }
+
+ /**
+ * Callback function to output a block
+ */
+ function addRow( $block, $tag ) {
+ global $wgOut, $wgUser, $wgLang;
+
+ if( $this->ip != '' ) {
+ if( $block->mAuto ) {
+ if( stristr( $block->mId, $this->ip ) == false ) {
+ return;
+ }
+ } else {
+ if( stristr( $block->mAddress, $this->ip ) == false ) {
+ return;
+ }
+ }
+ }
+
+ // Loading blocks is fast; displaying them is slow.
+ // Quick hack for paging.
+ $this->counter++;
+ if( $this->counter <= $this->offset ) {
+ return;
+ }
+ if( $this->counter - $this->offset > $this->limit ) {
+ return;
+ }
+
+ $fname = 'IPUnblockForm-addRow';
+ wfProfileIn( $fname );
+
+ static $sk=null, $msg=null;
+
+ if( is_null( $sk ) )
+ $sk = $wgUser->getSkin();
+ if( is_null( $msg ) ) {
+ $msg = array();
+ foreach( array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink' ) as $key ) {
+ $msg[$key] = wfMsgHtml( $key );
+ }
+ $msg['blocklistline'] = wfMsg( 'blocklistline' );
+ $msg['contribslink'] = wfMsg( 'contribslink' );
+ }
+
+
+ # Prepare links to the blocker's user and talk pages
+ $blocker_name = $block->getByName();
+ $blocker = $sk->MakeLinkObj( Title::makeTitle( NS_USER, $blocker_name ), $blocker_name );
+ $blocker .= ' (' . $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $blocker_name ), $wgLang->getNsText( NS_TALK ) ) . ')';
+
+ # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks)
+ if( $block->mAuto ) {
+ $target = '#' . $block->mId; # Hide the IP addresses of auto-blocks; privacy
+ } else {
+ $target = $sk->makeLinkObj( Title::makeTitle( NS_USER, $block->mAddress ), $block->mAddress );
+ $target .= ' (' . $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $msg['contribslink'], 'target=' . urlencode( $block->mAddress ) ) . ')';
+ }
+
+ # Prep the address for the unblock link, masking autoblocks as before
+ $addr = $block->mAuto ? '#' . $block->mId : $block->mAddress;
+
+ $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
+
+ if ( $block->mExpiry === "" ) {
+ $formattedExpiry = $msg['infiniteblock'];
+ } else {
+ $formattedExpiry = wfMsgReplaceArgs( $msg['expiringblock'],
+ array( $wgLang->timeanddate( $block->mExpiry, true ) ) );
+ }
+
+ $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $formattedExpiry ) );
+
+ $wgOut->addHTML( "<li>{$line}" );
+
+ if ( $wgUser->isAllowed('block') ) {
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" );
+ $wgOut->addHTML( ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&ip=' . urlencode( $addr ) ) . ')' );
+ }
+ $wgOut->addHTML( $sk->commentBlock( $block->mReason ) );
+ $wgOut->addHTML( "</li>\n" );
+ wfProfileOut( $fname );
+ }
+}
+
+?>
diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php
new file mode 100644
index 00000000..3cbdedab
--- /dev/null
+++ b/includes/SpecialListredirects.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+class ListredirectsPage extends QueryPage {
+
+ function getName() { return( 'Listredirects' ); }
+ function isExpensive() { return( true ); }
+ function isSyndicated() { return( false ); }
+ function sortDescending() { return( false ); }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1";
+ return( $sql );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ # Make a link to the redirect itself
+ $rd_title = Title::makeTitle( $result->namespace, $result->title );
+ $rd_link = $skin->makeKnownLinkObj( $rd_title, '', 'redirect=no' );
+
+ # Find out where the redirect leads
+ $revision = Revision::newFromTitle( $rd_title );
+ if( $revision ) {
+ # Make a link to the destination page
+ $target = Title::newFromRedirect( $revision->getText() );
+ if( $target ) {
+ $targetLink = $skin->makeLinkObj( $target );
+ } else {
+ /** @todo Put in some decent error display here */
+ $targetLink = '*';
+ }
+ } else {
+ /** @todo Put in some decent error display here */
+ $targetLink = '*';
+ }
+
+ # Check the language; RTL wikis need a &larr;
+ $arr = $wgContLang->isRTL() ? ' &larr; ' : ' &rarr; ';
+
+ # Format the whole thing and return it
+ return( $rd_link . $arr . $targetLink );
+
+ }
+
+}
+
+function wfSpecialListredirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $lrp = new ListredirectsPage();
+ $lrp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php
new file mode 100644
index 00000000..20b26b63
--- /dev/null
+++ b/includes/SpecialListusers.php
@@ -0,0 +1,235 @@
+<?php
+
+# Copyright (C) 2004 Brion Vibber, lcrocker, Tim Starling,
+# Domas Mituzas, Ashar Voultoiz, Jens Frank, Zhengzhu.
+#
+# © 2006 Rob Church <robchur@gmail.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
+ */
+
+/**
+ * This class is used to get a list of user. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class ListUsersPage extends QueryPage {
+ var $requestedGroup = '';
+ var $requestedUser = '';
+
+ function getName() {
+ return 'Listusers';
+ }
+ function isSyndicated() { return false; }
+
+ /**
+ * Not expensive, this class won't work properly with the caching system anyway
+ */
+ function isExpensive() {
+ return false;
+ }
+
+ /**
+ * Fetch user page links and cache their existence
+ */
+ function preprocessResults( &$db, &$res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) ) {
+ $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ }
+ $batch->execute();
+
+ // Back to start for display
+ if( $db->numRows( $res ) > 0 ) {
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+ }
+
+ /**
+ * Show a drop down list to select a group as well as a user name
+ * search box.
+ * @todo localize
+ */
+ function getPageHeader( ) {
+ $self = $this->getTitle();
+
+ # Form tag
+ $out = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+
+ # Group drop-down list
+ $out .= wfElement( 'label', array( 'for' => 'group' ), wfMsg( 'group' ) ) . ' ';
+ $out .= wfOpenElement( 'select', array( 'name' => 'group' ) );
+ $out .= wfElement( 'option', array( 'value' => '' ), wfMsg( 'group-all' ) ); # Item for "all groups"
+ $groups = User::getAllGroups();
+ foreach( $groups as $group ) {
+ $attribs = array( 'value' => $group );
+ if( $group == $this->requestedGroup )
+ $attribs['selected'] = 'selected';
+ $out .= wfElement( 'option', $attribs, User::getGroupName( $group ) );
+ }
+ $out .= wfCloseElement( 'select' ) . ' ';;# . wfElement( 'br' );
+
+ # Username field
+ $out .= wfElement( 'label', array( 'for' => 'username' ), wfMsg( 'specialloguserlabel' ) ) . ' ';
+ $out .= wfElement( 'input', array( 'type' => 'text', 'id' => 'username', 'name' => 'username',
+ 'value' => $this->requestedUser ) ) . ' ';
+
+ # Preserve offset and limit
+ if( $this->offset )
+ $out .= wfElement( 'input', array( 'type' => 'hidden', 'name' => 'offset', 'value' => $this->offset ) );
+ if( $this->limit )
+ $out .= wfElement( 'input', array( 'type' => 'hidden', 'name' => 'limit', 'value' => $this->limit ) );
+
+ # Submit button and form bottom
+ $out .= wfElement( 'input', array( 'type' => 'submit', 'value' => wfMsg( 'allpagessubmit' ) ) );
+ $out .= wfCloseElement( 'form' );
+
+ return $out;
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $user = $dbr->tableName( 'user' );
+ $user_groups = $dbr->tableName( 'user_groups' );
+
+ // We need to get an 'atomic' list of users, so that we
+ // don't break the list half-way through a user's group set
+ // and so that lists by group will show all group memberships.
+ //
+ // On MySQL 4.1 we could use GROUP_CONCAT to grab group
+ // assignments together with users pretty easily. On other
+ // versions, it's not so easy to do it consistently.
+ // For now we'll just grab the number of memberships, so
+ // we can then do targetted checks on those who are in
+ // non-default groups as we go down the list.
+
+ $userspace = NS_USER;
+ $sql = "SELECT 'Listusers' as type, $userspace AS namespace, user_name AS title, " .
+ "user_name as value, user_id, COUNT(ug_group) as numgroups " .
+ "FROM $user ".
+ "LEFT JOIN $user_groups ON user_id=ug_user " .
+ $this->userQueryWhere( $dbr ) .
+ " GROUP BY user_name";
+
+ return $sql;
+ }
+
+ function userQueryWhere( &$dbr ) {
+ $conds = $this->userQueryConditions();
+ return empty( $conds )
+ ? ""
+ : "WHERE " . $dbr->makeList( $conds, LIST_AND );
+ }
+
+ function userQueryConditions() {
+ $conds = array();
+ if( $this->requestedGroup != '' ) {
+ $conds['ug_group'] = $this->requestedGroup;
+ }
+ if( $this->requestedUser != '' ) {
+ $conds['user_name'] = $this->requestedUser;
+ }
+ return $conds;
+ }
+
+ function linkParameters() {
+ $conds = array();
+ if( $this->requestedGroup != '' ) {
+ $conds['group'] = $this->requestedGroup;
+ }
+ if( $this->requestedUser != '' ) {
+ $conds['username'] = $this->requestedUser;
+ }
+ return $conds;
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ $userPage = Title::makeTitle( $result->namespace, $result->title );
+ $name = $skin->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
+ $groups = null;
+
+ if( !isset( $result->numgroups ) || $result->numgroups > 0 ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $result = $dbr->select( 'user_groups',
+ array( 'ug_group' ),
+ array( 'ug_user' => $result->user_id ),
+ 'ListUsersPage::formatResult' );
+ $groups = array();
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $groups[$row->ug_group] = User::getGroupMember( $row->ug_group );
+ }
+ $dbr->freeResult( $result );
+
+ if( count( $groups ) > 0 ) {
+ foreach( $groups as $group => $desc ) {
+ if( $page = User::getGroupPage( $group ) ) {
+ $list[] = $skin->makeLinkObj( $page, htmlspecialchars( $desc ) );
+ } else {
+ $list[] = htmlspecialchars( $desc );
+ }
+ }
+ $groups = implode( ', ', $list );
+ } else {
+ $groups = '';
+ }
+
+ }
+
+ return wfSpecialList( $name, $groups );
+ }
+}
+
+/**
+ * constructor
+ * $par string (optional) A group to list users from
+ */
+function wfSpecialListusers( $par = null ) {
+ global $wgRequest, $wgContLang;
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+
+ $slu = new ListUsersPage();
+
+ /**
+ * Get some parameters
+ */
+ $groupTarget = isset($par) ? $par : $wgRequest->getVal( 'group' );
+ $slu->requestedGroup = $groupTarget;
+
+ # 'Validate' the username first
+ $username = $wgRequest->getText( 'username', '' );
+ $user = User::newFromName( $username );
+ $slu->requestedUser = is_object( $user ) ? $user->getName() : '';
+
+ return $slu->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php
new file mode 100644
index 00000000..38d715be
--- /dev/null
+++ b/includes/SpecialLockdb.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialLockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if ( ! $wgUser->isAllowed('siteadmin') ) {
+ $wgOut->developerRequired();
+ return;
+ }
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBLockForm();
+
+ if ( 'success' == $action ) {
+ $f->showSuccess();
+ } else if ( 'submit' == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( '' );
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class DBLockForm {
+ var $reason = '';
+
+ function DBLockForm() {
+ global $wgRequest;
+ $this->reason = $wgRequest->getText( 'wpLockReason' );
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->addWikiText( wfMsg( 'lockdbtext' ) );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) );
+ $lb = htmlspecialchars( wfMsg( 'lockbtn' ) );
+ $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) );
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' );
+ $action = $titleObj->escapeLocalURL( 'action=submit' );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+<form id="lockdb" method="post" action="{$action}">
+{$elr}:
+<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual"></textarea>
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ global $wgReadOnlyFile;
+
+ if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
+ $this->showForm( wfMsg( 'locknoconfirm' ) );
+ return;
+ }
+ $fp = fopen( $wgReadOnlyFile, 'w' );
+
+ if ( false === $fp ) {
+ $wgOut->showFileNotFoundError( $wgReadOnlyFile );
+ return;
+ }
+ fwrite( $fp, $this->reason );
+ fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
+ $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+ fclose( $fp );
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
+ $wgOut->addWikiText( wfMsg( 'lockdbsuccesstext' ) );
+ }
+}
+
+?>
diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php
new file mode 100644
index 00000000..a9e8573a
--- /dev/null
+++ b/includes/SpecialLog.php
@@ -0,0 +1,427 @@
+<?php
+# 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
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialLog( $par = '' ) {
+ global $wgRequest;
+ $logReader =& new LogReader( $wgRequest );
+ if( $wgRequest->getVal( 'type' ) == '' && $par != '' ) {
+ $logReader->limitType( $par );
+ }
+ $logViewer =& new LogViewer( $logReader );
+ $logViewer->show();
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class LogReader {
+ var $db, $joinClauses, $whereClauses;
+ var $type = '', $user = '', $title = null;
+
+ /**
+ * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters.
+ */
+ function LogReader( $request ) {
+ $this->db =& wfGetDB( DB_SLAVE );
+ $this->setupQuery( $request );
+ }
+
+ /**
+ * Basic setup and applies the limiting factors from the WebRequest object.
+ * @param WebRequest $request
+ * @private
+ */
+ function setupQuery( $request ) {
+ $page = $this->db->tableName( 'page' );
+ $user = $this->db->tableName( 'user' );
+ $this->joinClauses = array(
+ "LEFT OUTER JOIN $page ON log_namespace=page_namespace AND log_title=page_title",
+ "INNER JOIN $user ON user_id=log_user" );
+ $this->whereClauses = array();
+
+ $this->limitType( $request->getVal( 'type' ) );
+ $this->limitUser( $request->getText( 'user' ) );
+ $this->limitTitle( $request->getText( 'page' ) );
+ $this->limitTime( $request->getVal( 'from' ), '>=' );
+ $this->limitTime( $request->getVal( 'until' ), '<=' );
+
+ list( $this->limit, $this->offset ) = $request->getLimitOffset();
+ }
+
+ /**
+ * Set the log reader to return only entries of the given type.
+ * @param string $type A log type ('upload', 'delete', etc)
+ * @private
+ */
+ function limitType( $type ) {
+ if( empty( $type ) ) {
+ return false;
+ }
+ $this->type = $type;
+ $safetype = $this->db->strencode( $type );
+ $this->whereClauses[] = "log_type='$safetype'";
+ }
+
+ /**
+ * Set the log reader to return only entries by the given user.
+ * @param string $name (In)valid user name
+ * @private
+ */
+ function limitUser( $name ) {
+ if ( $name == '' )
+ return false;
+ $usertitle = Title::makeTitle( NS_USER, $name );
+ if ( is_null( $usertitle ) )
+ return false;
+ $this->user = $usertitle->getText();
+
+ /* Fetch userid at first, if known, provides awesome query plan afterwards */
+ $userid = $this->db->selectField('user','user_id',array('user_name'=>$this->user));
+ if (!$userid)
+ /* It should be nicer to abort query at all,
+ but for now it won't pass anywhere behind the optimizer */
+ $this->whereClauses[] = "NULL";
+ else
+ $this->whereClauses[] = "log_user=$userid";
+ }
+
+ /**
+ * Set the log reader to return only entries affecting the given page.
+ * (For the block and rights logs, this is a user page.)
+ * @param string $page Title name as text
+ * @private
+ */
+ function limitTitle( $page ) {
+ $title = Title::newFromText( $page );
+ if( empty( $page ) || is_null( $title ) ) {
+ return false;
+ }
+ $this->title =& $title;
+ $safetitle = $this->db->strencode( $title->getDBkey() );
+ $ns = $title->getNamespace();
+ $this->whereClauses[] = "log_namespace=$ns AND log_title='$safetitle'";
+ }
+
+ /**
+ * Set the log reader to return only entries in a given time range.
+ * @param string $time Timestamp of one endpoint
+ * @param string $direction either ">=" or "<=" operators
+ * @private
+ */
+ function limitTime( $time, $direction ) {
+ # Direction should be a comparison operator
+ if( empty( $time ) ) {
+ return false;
+ }
+ $safetime = $this->db->strencode( wfTimestamp( TS_MW, $time ) );
+ $this->whereClauses[] = "log_timestamp $direction '$safetime'";
+ }
+
+ /**
+ * Build an SQL query from all the set parameters.
+ * @return string the SQL query
+ * @private
+ */
+ function getQuery() {
+ $logging = $this->db->tableName( "logging" );
+ $user = $this->db->tableName( 'user' );
+ $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp,
+ log_user, user_name,
+ log_namespace, log_title, page_id,
+ log_comment, log_params FROM $logging ";
+ if( !empty( $this->joinClauses ) ) {
+ $sql .= implode( ' ', $this->joinClauses );
+ }
+ if( !empty( $this->whereClauses ) ) {
+ $sql .= " WHERE " . implode( ' AND ', $this->whereClauses );
+ }
+ $sql .= " ORDER BY log_timestamp DESC ";
+ $sql = $this->db->limitResult($sql, $this->limit, $this->offset );
+ return $sql;
+ }
+
+ /**
+ * Execute the query and start returning results.
+ * @return ResultWrapper result object to return the relevant rows
+ */
+ function getRows() {
+ $res = $this->db->query( $this->getQuery(), 'LogReader::getRows' );
+ return $this->db->resultObject( $res );
+ }
+
+ /**
+ * @return string The query type that this LogReader has been limited to.
+ */
+ function queryType() {
+ return $this->type;
+ }
+
+ /**
+ * @return string The username type that this LogReader has been limited to, if any.
+ */
+ function queryUser() {
+ return $this->user;
+ }
+
+ /**
+ * @return string The text of the title that this LogReader has been limited to.
+ */
+ function queryTitle() {
+ if( is_null( $this->title ) ) {
+ return '';
+ } else {
+ return $this->title->getPrefixedText();
+ }
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class LogViewer {
+ /**
+ * @var LogReader $reader
+ */
+ var $reader;
+ var $numResults = 0;
+
+ /**
+ * @param LogReader &$reader where to get our data from
+ */
+ function LogViewer( &$reader ) {
+ global $wgUser;
+ $this->skin =& $wgUser->getSkin();
+ $this->reader =& $reader;
+ }
+
+ /**
+ * Take over the whole output page in $wgOut with the log display.
+ */
+ function show() {
+ global $wgOut;
+ $this->showHeader( $wgOut );
+ $this->showOptions( $wgOut );
+ $result = $this->getLogRows();
+ $this->showPrevNext( $wgOut );
+ $this->doShowList( $wgOut, $result );
+ $this->showPrevNext( $wgOut );
+ }
+
+ /**
+ * Load the data from the linked LogReader
+ * Preload the link cache
+ * Initialise numResults
+ *
+ * Must be called before calling showPrevNext
+ *
+ * @return object database result set
+ */
+ function getLogRows() {
+ $result = $this->reader->getRows();
+ $this->numResults = 0;
+
+ // Fetch results and form a batch link existence query
+ $batch = new LinkBatch;
+ while ( $s = $result->fetchObject() ) {
+ // User link
+ $title = Title::makeTitleSafe( NS_USER, $s->user_name );
+ $batch->addObj( $title );
+
+ // Move destination link
+ if ( $s->log_type == 'move' ) {
+ $paramArray = LogPage::extractParams( $s->log_params );
+ $title = Title::newFromText( $paramArray[0] );
+ $batch->addObj( $title );
+ }
+ ++$this->numResults;
+ }
+ $batch->execute();
+
+ return $result;
+ }
+
+
+ /**
+ * Output just the list of entries given by the linked LogReader,
+ * with extraneous UI elements. Use for displaying log fragments in
+ * another page (eg at Special:Undelete)
+ * @param OutputPage $out where to send output
+ */
+ function showList( &$out ) {
+ $this->doShowList( $out, $this->getLogRows() );
+ }
+
+ function doShowList( &$out, $result ) {
+ // Rewind result pointer and go through it again, making the HTML
+ if ($this->numResults > 0) {
+ $html = "\n<ul>\n";
+ $result->seek( 0 );
+ while( $s = $result->fetchObject() ) {
+ $html .= $this->logLine( $s );
+ }
+ $html .= "\n</ul>\n";
+ $out->addHTML( $html );
+ } else {
+ $out->addWikiText( wfMsg( 'logempty' ) );
+ }
+ $result->free();
+ }
+
+ /**
+ * @param Object $s a single row from the result set
+ * @return string Formatted HTML list item
+ * @private
+ */
+ function logLine( $s ) {
+ global $wgLang;
+ $title = Title::makeTitle( $s->log_namespace, $s->log_title );
+ $user = Title::makeTitleSafe( NS_USER, $s->user_name );
+ $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $s->log_timestamp), true );
+
+ // Enter the existence or non-existence of this page into the link cache,
+ // for faster makeLinkObj() in LogPage::actionText()
+ $linkCache =& LinkCache::singleton();
+ if( $s->page_id ) {
+ $linkCache->addGoodLinkObj( $s->page_id, $title );
+ } else {
+ $linkCache->addBadLinkObj( $title );
+ }
+
+ $userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinks( $s->log_user, $s->user_name );
+ $comment = $this->skin->commentBlock( $s->log_comment );
+ $paramArray = LogPage::extractParams( $s->log_params );
+ $revert = '';
+ if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
+ $specialTitle = Title::makeTitle( NS_SPECIAL, 'Movepage' );
+ $destTitle = Title::newFromText( $paramArray[0] );
+ if ( $destTitle ) {
+ $revert = '(' . $this->skin->makeKnownLinkObj( $specialTitle, wfMsg( 'revertmove' ),
+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
+ '&wpMovetalk=0' ) . ')';
+ }
+ }
+
+ $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true );
+ $out = "<li>$time $userLink $action $comment $revert</li>\n";
+ return $out;
+ }
+
+ /**
+ * @param OutputPage &$out where to send output
+ * @private
+ */
+ function showHeader( &$out ) {
+ $type = $this->reader->queryType();
+ if( LogPage::isLogType( $type ) ) {
+ $out->setPageTitle( LogPage::logName( $type ) );
+ $out->addWikiText( LogPage::logHeader( $type ) );
+ }
+ }
+
+ /**
+ * @param OutputPage &$out where to send output
+ * @private
+ */
+ function showOptions( &$out ) {
+ global $wgScript;
+ $action = htmlspecialchars( $wgScript );
+ $title = Title::makeTitle( NS_SPECIAL, 'Log' );
+ $special = htmlspecialchars( $title->getPrefixedDBkey() );
+ $out->addHTML( "<form action=\"$action\" method=\"get\">\n" .
+ "<input type='hidden' name='title' value=\"$special\" />\n" .
+ $this->getTypeMenu() .
+ $this->getUserInput() .
+ $this->getTitleInput() .
+ "<input type='submit' value=\"" . wfMsg( 'allpagessubmit' ) . "\" />" .
+ "</form>" );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getTypeMenu() {
+ $out = "<select name='type'>\n";
+ foreach( LogPage::validTypes() as $type ) {
+ $text = htmlspecialchars( LogPage::logName( $type ) );
+ $selected = ($type == $this->reader->queryType()) ? ' selected="selected"' : '';
+ $out .= "<option value=\"$type\"$selected>$text</option>\n";
+ }
+ $out .= "</select>\n";
+ return $out;
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getUserInput() {
+ $user = htmlspecialchars( $this->reader->queryUser() );
+ return wfMsg('specialloguserlabel') . "<input type='text' name='user' size='12' value=\"$user\" />\n";
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getTitleInput() {
+ $title = htmlspecialchars( $this->reader->queryTitle() );
+ return wfMsg('speciallogtitlelabel') . "<input type='text' name='page' size='20' value=\"$title\" />\n";
+ }
+
+ /**
+ * @param OutputPage &$out where to send output
+ * @private
+ */
+ function showPrevNext( &$out ) {
+ global $wgContLang,$wgRequest;
+ $pieces = array();
+ $pieces[] = 'type=' . urlencode( $this->reader->queryType() );
+ $pieces[] = 'user=' . urlencode( $this->reader->queryUser() );
+ $pieces[] = 'page=' . urlencode( $this->reader->queryTitle() );
+ $bits = implode( '&', $pieces );
+ list( $limit, $offset ) = $wgRequest->getLimitOffset();
+
+ # TODO: use timestamps instead of offsets to make it more natural
+ # to go huge distances in time
+ $html = wfViewPrevNext( $offset, $limit,
+ $wgContLang->specialpage( 'Log' ),
+ $bits,
+ $this->numResults < $limit);
+ $out->addHTML( '<p>' . $html . '</p>' );
+ }
+}
+
+
+?>
diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php
new file mode 100644
index 00000000..326ae54d
--- /dev/null
+++ b/includes/SpecialLonelypages.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class LonelyPagesPage extends PageQueryPage {
+
+ function getName() {
+ return "Lonelypages";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'pagelinks' ) );
+
+ return
+ "SELECT 'Lonelypages' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $pagelinks
+ ON page_namespace=pl_namespace AND page_title=pl_title
+ WHERE pl_namespace IS NULL
+ AND page_namespace=".NS_MAIN."
+ AND page_is_redirect=0";
+
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialLonelypages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LonelyPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php
new file mode 100644
index 00000000..af56c17c
--- /dev/null
+++ b/includes/SpecialLongpages.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'SpecialShortpages.php' );
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class LongPagesPage extends ShortPagesPage {
+
+ function getName() {
+ return "Longpages";
+ }
+
+ function sortDescending() {
+ return true;
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialLongpages()
+{
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LongPagesPage();
+
+ $lpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php
new file mode 100644
index 00000000..cbbe6f93
--- /dev/null
+++ b/includes/SpecialMIMEsearch.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * A special page to search for files by MIME type as defined in the
+ * img_major_mime and img_minor_mime fields in the image table
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MIMEsearchPage extends QueryPage {
+ var $major, $minor;
+
+ function MIMEsearchPage( $major, $minor ) {
+ $this->major = $major;
+ $this->minor = $minor;
+ }
+
+ function getName() { return 'MIMEsearch'; }
+
+ /**
+ * Due to this page relying upon extra fields being passed in the SELECT it
+ * will fail if it's set as expensive and misermode is on
+ */
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ $arr = array( $this->major, $this->minor );
+ $mime = implode( '/', $arr );
+ return array( 'mime' => $mime );
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $major = $dbr->addQuotes( $this->major );
+ $minor = $dbr->addQuotes( $this->minor );
+
+ return
+ "SELECT 'MIMEsearch' AS type,
+ " . NS_IMAGE . " AS namespace,
+ img_name AS title,
+ img_major_mime AS value,
+
+ img_size,
+ img_width,
+ img_height,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_major_mime = $major AND img_minor_mime = $minor
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+ $download = $skin->makeMediaLink( $nt->getText(), 'fuck me!', wfMsgHtml( 'download' ) );
+ $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->img_size ) );
+ $dimensions = wfMsg( 'widthheight', $wgLang->formatNum( $result->img_width ),
+ $wgLang->formatNum( $result->img_height ) );
+ $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $time = $wgLang->timeanddate( $result->img_timestamp );
+
+ return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMIMEsearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut;
+
+ $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
+
+ $wgOut->addHTML(
+ wfElement( 'form',
+ array(
+ 'id' => 'specialmimesearch',
+ 'method' => 'get',
+ 'action' => $wgTitle->escapeLocalUrl()
+ ),
+ null
+ ) .
+ wfOpenElement( 'label' ) .
+ wfMsgHtml( 'mimetype' ) .
+ wfElement( 'input', array(
+ 'type' => 'text',
+ 'size' => 20,
+ 'name' => 'mime',
+ 'value' => $mime
+ ),
+ ''
+ ) .
+ ' ' .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'value' => wfMsg( 'ilsubmit' )
+ ),
+ ''
+ ) .
+ wfCloseElement( 'label' ) .
+ wfCloseElement( 'form' )
+ );
+
+ list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
+ if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
+ return;
+ $wpp = new MIMEsearchPage( $major, $minor );
+
+ list( $limit, $offset ) = wfCheckLimits();
+ $wpp->doQuery( $offset, $limit );
+}
+
+function wfSpecialMIMEsearchParse( $str ) {
+ wfSuppressWarnings();
+ list( $major, $minor ) = explode( '/', $str, 2 );
+ wfRestoreWarnings();
+
+ return array(
+ ltrim( $major, ' ' ),
+ rtrim( $minor, ' ' )
+ );
+}
+
+function wfSpecialMIMEsearchValidType( $type ) {
+ // From maintenance/tables.sql => img_major_mime
+ $types = array(
+ 'unknown',
+ 'application',
+ 'audio',
+ 'image',
+ 'text',
+ 'video',
+ 'message',
+ 'model',
+ 'multipart'
+ );
+
+ return in_array( $type, $types );
+}
+?>
diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php
new file mode 100644
index 00000000..5591bbc4
--- /dev/null
+++ b/includes/SpecialMostcategories.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MostcategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'categorylinks', 'page' ) );
+ return
+ "
+ SELECT
+ 'Mostcategories' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_from = page_id
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY cl_from
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLink( $nt->getPrefixedText(), $text );
+
+ $nl = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+
+ $nlink = $skin->makeKnownLink( $wgContLang->specialPage( 'Categories' ),
+ $nl, 'article=' . $nt->getPrefixedURL() );
+
+ return wfSpecialList($plink, $nlink);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostcategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php
new file mode 100644
index 00000000..30fbdddf
--- /dev/null
+++ b/includes/SpecialMostimages.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MostimagesPage extends QueryPage {
+
+ function getName() { return 'Mostimages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'imagelinks' ) );
+ return
+ "
+ SELECT
+ 'Mostimages' as type,
+ " . NS_IMAGE . " as namespace,
+ il_to as title,
+ COUNT(*) as value
+ FROM $imagelinks
+ GROUP BY il_to
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLink( $nt->getPrefixedText(), $text );
+
+ $nl = wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+ $wgLang->formatNum ( $result->value ) );
+ $nlink = $skin->makeKnownLink( $nt->getPrefixedText() . '#filelinks', $nl );
+
+ return wfSpecialList($plink, $nlink);
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialMostimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostimagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php
new file mode 100644
index 00000000..ccccc1a4
--- /dev/null
+++ b/includes/SpecialMostlinked.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * A special page to show pages ordered by the number of pages linking to them
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MostlinkedPage extends QueryPage {
+
+ function getName() { return 'Mostlinked'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ /**
+ * Note: Getting page_namespace only works if $this->isCached() is false
+ */
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'pagelinks', 'page' ) );
+ return
+ "SELECT 'Mostlinked' AS type,
+ pl_namespace AS namespace,
+ pl_title AS title,
+ COUNT(*) AS value,
+ page_namespace
+ FROM $pagelinks
+ LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title
+ GROUP BY pl_namespace,pl_title
+ HAVING COUNT(*) > 1";
+ }
+
+ /**
+ * Pre-fill the link cache
+ */
+ function preprocessResults( &$dbr, $res ) {
+ if( $dbr->numRows( $res ) > 0 ) {
+ $linkBatch = new LinkBatch();
+ while( $row = $dbr->fetchObject( $res ) )
+ $linkBatch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ $dbr->dataSeek( $res, 0 );
+ $linkBatch->execute();
+ }
+ }
+
+ /**
+ * Make a link to "what links here" for the specified title
+ *
+ * @param $title Title being queried
+ * @param $skin Skin to use
+ * @return string
+ */
+ function makeWlhLink( &$title, $caption, &$skin ) {
+ $wlh = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' );
+ return $skin->makeKnownLinkObj( $wlh, $caption, 'target=' . $title->getPrefixedUrl() );
+ }
+
+ /**
+ * Make links to the page corresponding to the item, and the "what links here" page for it
+ *
+ * @param $skin Skin to be used
+ * @param $result Result row
+ * @return string
+ */
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ $link = $skin->makeLinkObj( $title );
+ $wlh = $this->makeWlhLink( $title,
+ wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) ), $skin );
+ return wfSpecialList( $link, $wlh );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinked() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php
new file mode 100644
index 00000000..0944d2f8
--- /dev/null
+++ b/includes/SpecialMostlinkedcategories.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * A querypage to show categories ordered in descending order by the pages in them
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MostlinkedCategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostlinkedcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'categorylinks', 'page' ) );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ GROUP BY cl_to
+ ";
+ }
+
+ function sortDescending() { return true; }
+
+ /**
+ * Fetch user page links and cache their existence
+ */
+ function preprocessResults( &$db, &$res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) )
+ $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ $batch->execute();
+
+ // Back to start for display
+ if ( $db->numRows( $res ) > 0 )
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+
+ $plink = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinkedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php
new file mode 100644
index 00000000..81a49c99
--- /dev/null
+++ b/includes/SpecialMostrevisions.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * A special page to show pages in the
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MostrevisionsPage extends QueryPage {
+
+ function getName() { return 'Mostrevisions'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'revision', 'page' ) );
+ return
+ "
+ SELECT
+ 'Mostrevisions' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $revision
+ LEFT JOIN $page ON page_id = rev_page
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY rev_page
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+
+ return wfSpecialList($plink, $nlink);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostrevisions() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostrevisionsPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php
new file mode 100644
index 00000000..39397129
--- /dev/null
+++ b/includes/SpecialMovepage.php
@@ -0,0 +1,283 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMovepage( $par = null ) {
+ global $wgUser, $wgOut, $wgRequest, $action, $wgOnlySysopMayMove;
+
+ # check rights. We don't want newbies to move pages to prevents possible attack
+ if ( !$wgUser->isAllowed( 'move' ) or $wgUser->isBlocked() or ($wgOnlySysopMayMove and $wgUser->isNewbie())) {
+ $wgOut->showErrorPage( "movenologin", "movenologintext" );
+ return;
+ }
+ # We don't move protected pages
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $f = new MovePageForm( $par );
+
+ if ( 'success' == $action ) {
+ $f->showSuccess();
+ } else if ( 'submit' == $action && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( '' );
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class MovePageForm {
+ var $oldTitle, $newTitle, $reason; # Text input
+ var $moveTalk, $deleteAndMove;
+
+ function MovePageForm( $par ) {
+ global $wgRequest;
+ $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+ $this->oldTitle = $wgRequest->getText( 'wpOldTitle', $target );
+ $this->newTitle = $wgRequest->getText( 'wpNewTitle' );
+ $this->reason = $wgRequest->getText( 'wpReason' );
+ $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
+ $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( 'movepage' ) );
+
+ $ot = Title::newFromURL( $this->oldTitle );
+ if( is_null( $ot ) ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+ $oldTitle = $ot->getPrefixedText();
+
+ $encOldTitle = htmlspecialchars( $oldTitle );
+ if( $this->newTitle == '' ) {
+ # Show the current title as a default
+ # when the form is first opened.
+ $encNewTitle = $encOldTitle;
+ } else {
+ if( $err == '' ) {
+ $nt = Title::newFromURL( $this->newTitle );
+ if( $nt ) {
+ # If a title was supplied, probably from the move log revert
+ # link, check for validity. We can then show some diagnostic
+ # information and save a click.
+ $newerr = $ot->isValidMoveOperation( $nt );
+ if( is_string( $newerr ) ) {
+ $err = $newerr;
+ }
+ }
+ }
+ $encNewTitle = htmlspecialchars( $this->newTitle );
+ }
+ $encReason = htmlspecialchars( $this->reason );
+
+ if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) );
+ $movepagebtn = wfMsgHtml( 'delete_and_move' );
+ $confirmText = wfMsgHtml( 'delete_and_move_confirm' );
+ $submitVar = 'wpDeleteAndMove';
+ $confirm = "
+ <tr>
+ <td align='right'>
+ <input type='checkbox' name='wpConfirm' id='wpConfirm' value=\"true\" />
+ </td>
+ <td align='left'><label for='wpConfirm'>{$confirmText}</label></td>
+ </tr>";
+ $err = '';
+ } else {
+ $wgOut->addWikiText( wfMsg( 'movepagetext' ) );
+ $movepagebtn = wfMsgHtml( 'movepagebtn' );
+ $submitVar = 'wpMove';
+ $confirm = false;
+ }
+
+ $oldTalk = $ot->getTalkPage();
+ $considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() );
+
+ if ( $considerTalk ) {
+ $wgOut->addWikiText( wfMsg( 'movepagetalktext' ) );
+ }
+
+ $movearticle = wfMsgHtml( 'movearticle' );
+ $newtitle = wfMsgHtml( 'newtitle' );
+ $movetalk = wfMsgHtml( 'movetalk' );
+ $movereason = wfMsgHtml( 'movereason' );
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' );
+ $action = $titleObj->escapeLocalURL( 'action=submit' );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ if ( $err != '' ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ $wgOut->addWikiText( '<p class="error">' . wfMsg($err) . "</p>\n" );
+ }
+
+ $moveTalkChecked = $this->moveTalk ? ' checked="checked"' : '';
+
+ $wgOut->addHTML( "
+<form id=\"movepage\" method=\"post\" action=\"{$action}\">
+ <table border='0'>
+ <tr>
+ <td align='right'>{$movearticle}:</td>
+ <td align='left'><strong>{$oldTitle}</strong></td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpNewTitle'>{$newtitle}:</label></td>
+ <td align='left'>
+ <input type='text' size='40' name='wpNewTitle' id='wpNewTitle' value=\"{$encNewTitle}\" />
+ <input type='hidden' name=\"wpOldTitle\" value=\"{$encOldTitle}\" />
+ </td>
+ </tr>
+ <tr>
+ <td align='right' valign='top'><br /><label for='wpReason'>{$movereason}:</label></td>
+ <td align='left' valign='top'><br />
+ <textarea cols='60' rows='2' name='wpReason' id='wpReason'>{$encReason}</textarea>
+ </td>
+ </tr>" );
+
+ if ( $considerTalk ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td align='right'>
+ <input type='checkbox' id=\"wpMovetalk\" name=\"wpMovetalk\"{$moveTalkChecked} value=\"1\" />
+ </td>
+ <td><label for=\"wpMovetalk\">{$movetalk}</label></td>
+ </tr>" );
+ }
+ $wgOut->addHTML( "
+ {$confirm}
+ <tr>
+ <td>&nbsp;</td>
+ <td align='left'>
+ <input type='submit' name=\"{$submitVar}\" value=\"{$movepagebtn}\" />
+ </td>
+ </tr>
+ </table>
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+</form>\n" );
+
+ $this->showLogFragment( $ot, $wgOut );
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgRequest;
+ $fname = "MovePageForm::doSubmit";
+
+ if ( $wgUser->pingLimiter( 'move' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ # Variables beginning with 'o' for old article 'n' for new article
+
+ $ot = Title::newFromText( $this->oldTitle );
+ $nt = Title::newFromText( $this->newTitle );
+
+ # Delete to make way if requested
+ if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
+ $article = new Article( $nt );
+ // This may output an error message and exit
+ $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
+ }
+
+ # don't allow moving to pages with # in
+ if ( !$nt || $nt->getFragment() != '' ) {
+ $this->showForm( 'badtitletext' );
+ return;
+ }
+
+ $error = $ot->moveTo( $nt, true, $this->reason );
+ if ( $error !== true ) {
+ $this->showForm( $error );
+ return;
+ }
+
+ wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
+
+ # Move the talk page if relevant, if it exists, and if we've been told to
+ $ott = $ot->getTalkPage();
+ if( $ott->exists() ) {
+ if( $wgRequest->getVal( 'wpMovetalk' ) == 1 && !$ot->isTalkPage() && !$nt->isTalkPage() ) {
+ $ntt = $nt->getTalkPage();
+
+ # Attempt the move
+ $error = $ott->moveTo( $ntt, true, $this->reason );
+ if ( $error === true ) {
+ $talkmoved = 1;
+ wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ott , &$ntt ) ) ;
+ } else {
+ $talkmoved = $error;
+ }
+ } else {
+ # Stay silent on the subject of talk.
+ $talkmoved = '';
+ }
+ } else {
+ $talkmoved = 'notalkpage';
+ }
+
+ # Give back result to user.
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' );
+ $success = $titleObj->getFullURL(
+ 'action=success&oldtitle=' . wfUrlencode( $ot->getPrefixedText() ) .
+ '&newtitle=' . wfUrlencode( $nt->getPrefixedText() ) .
+ '&talkmoved='.$talkmoved );
+
+ $wgOut->redirect( $success );
+ }
+
+ function showSuccess() {
+ global $wgOut, $wgRequest, $wgRawHtml;
+
+ $wgOut->setPagetitle( wfMsg( 'movepage' ) );
+ $wgOut->setSubtitle( wfMsg( 'pagemovedsub' ) );
+
+ $oldText = $wgRequest->getVal('oldtitle');
+ $newText = $wgRequest->getVal('newtitle');
+ $talkmoved = $wgRequest->getVal('talkmoved');
+
+ $text = wfMsg( 'pagemovedtext', $oldText, $newText );
+
+ $allowHTML = $wgRawHtml;
+ $wgRawHtml = false;
+ $wgOut->addWikiText( $text );
+ $wgRawHtml = $allowHTML;
+
+ if ( $talkmoved == 1 ) {
+ $wgOut->addWikiText( wfMsg( 'talkpagemoved' ) );
+ } elseif( 'articleexists' == $talkmoved ) {
+ $wgOut->addWikiText( wfMsg( 'talkexists' ) );
+ } else {
+ $oldTitle = Title::newFromText( $oldText );
+ if ( !$oldTitle->isTalkPage() && $talkmoved != 'notalkpage' ) {
+ $wgOut->addWikiText( wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ) );
+ }
+ }
+ }
+
+ function showLogFragment( $title, &$out ) {
+ $out->addHtml( wfElement( 'h2', NULL, LogPage::logName( 'move' ) ) );
+ $request = new FauxRequest( array( 'page' => $title->getPrefixedText(), 'type' => 'move' ) );
+ $viewer = new LogViewer( new LogReader( $request ) );
+ $viewer->showList( $out );
+ }
+
+}
+?>
diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php
new file mode 100644
index 00000000..976611a3
--- /dev/null
+++ b/includes/SpecialNewimages.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialNewimages( $par, $specialPage ) {
+ global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgGroupPermissions;
+
+ $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
+ $dbr =& wfGetDB( DB_SLAVE );
+ $sk = $wgUser->getSkin();
+ $shownav = !$specialPage->including();
+ $hidebots = $wgRequest->getBool('hidebots',1);
+
+ if($hidebots) {
+
+ /** Make a list of group names which have the 'bot' flag
+ set.
+ */
+ $botconds=array();
+ foreach ($wgGroupPermissions as $groupname=>$perms) {
+ if(array_key_exists('bot',$perms) && $perms['bot']) {
+ $botconds[]="ug_group='$groupname'";
+ }
+ }
+ $isbotmember=$dbr->makeList($botconds, LIST_OR);
+
+ /** This join, in conjunction with WHERE ug_group
+ IS NULL, returns only those rows from IMAGE
+ where the uploading user is not a member of
+ a group which has the 'bot' permission set.
+ */
+ $ug = $dbr->tableName('user_groups');
+ $joinsql=" LEFT OUTER JOIN $ug ON img_user=ug_user AND ("
+ . $isbotmember.')';
+ }
+
+ $image = $dbr->tableName('image');
+
+ $sql="SELECT img_timestamp from $image";
+ if($hidebots) {
+ $sql.=$joinsql.' WHERE ug_group IS NULL';
+ }
+ $sql.=' ORDER BY img_timestamp DESC LIMIT 1';
+ $res = $dbr->query($sql, 'wfSpecialNewImages');
+ $row = $dbr->fetchRow($res);
+ if($row!==false) {
+ $ts=$row[0];
+ } else {
+ $ts=false;
+ }
+ $dbr->freeResult($res);
+ $sql='';
+
+ /** If we were clever, we'd use this to cache. */
+ $latestTimestamp = wfTimestamp( TS_MW, $ts);
+
+ /** Hardcode this for now. */
+ $limit = 48;
+
+ if ( $parval = intval( $par ) )
+ if ( $parval <= $limit && $parval > 0 )
+ $limit = $parval;
+
+ $where = array();
+ $searchpar = '';
+ if ( $wpIlMatch != '' ) {
+ $nt = Title::newFromUrl( $wpIlMatch );
+ if($nt ) {
+ $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+ $m = str_replace( '%', "\\%", $m );
+ $m = str_replace( '_', "\\_", $m );
+ $where[] = "LCASE(img_name) LIKE '%{$m}%'";
+ $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
+ }
+ }
+
+ $invertSort = false;
+ if( $until = $wgRequest->getVal( 'until' ) ) {
+ $where[] = 'img_timestamp < ' . $dbr->timestamp( $until );
+ }
+ if( $from = $wgRequest->getVal( 'from' ) ) {
+ $where[] = 'img_timestamp >= ' . $dbr->timestamp( $from );
+ $invertSort = true;
+ }
+ $sql='SELECT img_size, img_name, img_user, img_user_text,'.
+ "img_description,img_timestamp FROM $image";
+
+ if($hidebots) {
+ $sql.=$joinsql;
+ $where[]='ug_group IS NULL';
+ }
+ if(count($where)) {
+ $sql.=' WHERE '.$dbr->makeList($where, LIST_AND);
+ }
+ $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
+ $sql.=' LIMIT '.($limit+1);
+ $res = $dbr->query($sql, 'wfSpecialNewImages');
+
+ /**
+ * We have to flip things around to get the last N after a certain date
+ */
+ $images = array();
+ while ( $s = $dbr->fetchObject( $res ) ) {
+ if( $invertSort ) {
+ array_unshift( $images, $s );
+ } else {
+ array_push( $images, $s );
+ }
+ }
+ $dbr->freeResult( $res );
+
+ $gallery = new ImageGallery();
+ $firstTimestamp = null;
+ $lastTimestamp = null;
+ $shownImages = 0;
+ foreach( $images as $s ) {
+ if( ++$shownImages > $limit ) {
+ # One extra just to test for whether to show a page link;
+ # don't actually show it.
+ break;
+ }
+
+ $name = $s->img_name;
+ $ut = $s->img_user_text;
+
+ $nt = Title::newFromText( $name, NS_IMAGE );
+ $img = Image::newFromTitle( $nt );
+ $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
+
+ $gallery->add( $img, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
+
+ $timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
+ if( empty( $firstTimestamp ) ) {
+ $firstTimestamp = $timestamp;
+ }
+ $lastTimestamp = $timestamp;
+ }
+
+ $bydate = wfMsg( 'bydate' );
+ $lt = $wgLang->formatNum( min( $shownImages, $limit ) );
+ if ($shownav) {
+ $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate );
+ $wgOut->addHTML( $text . "\n" );
+ }
+
+ $sub = wfMsg( 'ilsubmit' );
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Newimages' );
+ $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' );
+ if ($shownav) {
+ $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
+ "{$action}\">" .
+ "<input type='text' size='20' name=\"wpIlMatch\" value=\"" .
+ htmlspecialchars( $wpIlMatch ) . "\" /> " .
+ "<input type='submit' name=\"wpIlSubmit\" value=\"{$sub}\" /></form>" );
+ }
+ $here = $wgContLang->specialPage( 'Newimages' );
+
+ /**
+ * Paging controls...
+ */
+
+ # If we change bot visibility, this needs to be carried along.
+ if(!$hidebots) {
+ $botpar='&hidebots=0';
+ } else {
+ $botpar='';
+ }
+ $now = wfTimestampNow();
+ $date = $wgLang->timeanddate( $now, true );
+ $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsg( 'sp-newimages-showfrom', $date ), 'from='.$now.$botpar.$searchpar );
+
+ $botLink = $sk->makeKnownLinkObj($titleObj, wfMsg( 'showhidebots', ($hidebots ? wfMsg('show') : wfMsg('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar);
+
+ $prevLink = wfMsg( 'prevn', $wgLang->formatNum( $limit ) );
+ if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
+ $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
+ }
+
+ $nextLink = wfMsg( 'nextn', $wgLang->formatNum( $limit ) );
+ if( $shownImages > $limit && $lastTimestamp ) {
+ $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
+ }
+
+ $prevnext = '<p>' . $botLink . ' '. wfMsg( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
+
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+
+ if( count( $images ) ) {
+ $wgOut->addHTML( $gallery->toHTML() );
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+ } else {
+ $wgOut->addWikiText( wfMsg( 'noimages' ) );
+ }
+}
+
+?>
diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php
new file mode 100644
index 00000000..c0c6ba96
--- /dev/null
+++ b/includes/SpecialNewpages.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class NewPagesPage extends QueryPage {
+ var $namespace;
+
+ function NewPagesPage( $namespace = NS_MAIN ) {
+ $this->namespace = $namespace;
+ }
+
+ function getName() {
+ return 'Newpages';
+ }
+
+ function isExpensive() {
+ # Indexed on RC, and will *not* work with querycache yet.
+ return false;
+ }
+
+ function getSQL() {
+ global $wgUser, $wgUseRCPatrol;
+ $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0;
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'recentchanges', 'page', 'text' ) );
+
+ # FIXME: text will break with compression
+ return
+ "SELECT 'Newpages' as type,
+ rc_namespace AS namespace,
+ rc_title AS title,
+ rc_cur_id AS cur_id,
+ rc_user AS user,
+ rc_user_text AS user_text,
+ rc_comment as comment,
+ rc_timestamp AS timestamp,
+ rc_timestamp AS value,
+ '{$usepatrol}' as usepatrol,
+ rc_patrolled AS patrolled,
+ rc_id AS rcid,
+ page_len as length,
+ page_latest as rev_id
+ FROM $recentchanges,$page
+ WHERE rc_cur_id=page_id AND rc_new=1
+ AND rc_namespace=" . $this->namespace . " AND page_is_redirect=0";
+ }
+
+ function preprocessResults( &$dbo, &$res ) {
+ # Do a batch existence check on the user and talk pages
+ $linkBatch = new LinkBatch();
+ while( $row = $dbo->fetchObject( $res ) ) {
+ $linkBatch->addObj( Title::makeTitleSafe( NS_USER, $row->user_text ) );
+ $linkBatch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_text ) );
+ }
+ $linkBatch->execute();
+ # Seek to start
+ if( $dbo->numRows( $res ) > 0 )
+ $dbo->dataSeek( $res, 0 );
+ }
+
+ /**
+ * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
+ *
+ * @param $skin Skin to use
+ * @param $result Result row
+ * @return string
+ */
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ $time = $wgLang->timeAndDate( $result->timestamp, true );
+ $plink = $skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rcid : '' );
+ $hist = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $length = wfMsgHtml( 'nbytes', $wgLang->formatNum( htmlspecialchars( $result->length ) ) );
+ $ulink = $skin->userLink( $result->user, $result->user_text ) . $skin->userToolLinks( $result->user, $result->user_text );
+ $comment = $skin->commentBlock( $result->comment );
+
+ return "{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}";
+ }
+
+ /**
+ * Should a specific result row provide "patrollable" links?
+ *
+ * @param $result Result row
+ * @return bool
+ */
+ function patrollable( $result ) {
+ global $wgUser, $wgUseRCPatrol;
+ return $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) && !$result->patrolled;
+ }
+
+ function feedItemDesc( $row ) {
+ if( isset( $row->rev_id ) ) {
+ $revision = Revision::newFromId( $row->rev_id );
+ if( $revision ) {
+ return '<p>' . htmlspecialchars( wfMsg( 'summary' ) ) . ': ' .
+ htmlspecialchars( $revision->getComment() ) . "</p>\n<hr />\n<div>" .
+ nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ }
+ }
+ return parent::feedItemDesc( $row );
+ }
+
+ /**
+ * Show a namespace selection form for filtering
+ *
+ * @return string
+ */
+ function getPageHeader() {
+ $thisTitle = Title::makeTitle( NS_SPECIAL, $this->getName() );
+ $form = wfOpenElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $thisTitle->getLocalUrl() ) );
+ $form .= wfElement( 'label', array( 'for' => 'namespace' ),
+ wfMsg( 'namespace' ) ) . ' ';
+ $form .= HtmlNamespaceSelector( $this->namespace );
+ # Preserve the offset and limit
+ $form .= wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'offset',
+ 'value' => $this->offset ) );
+ $form .= wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'limit',
+ 'value' => $this->limit ) );
+ $form .= wfElement( 'input', array(
+ 'type' => 'submit',
+ 'name' => 'submit',
+ 'id' => 'submit',
+ 'value' => wfMsg( 'allpagessubmit' ) ) );
+ $form .= wfCloseElement( 'form' );
+ return( $form );
+ }
+
+ /**
+ * Link parameters
+ *
+ * @return array
+ */
+ function linkParameters() {
+ return( array( 'namespace' => $this->namespace ) );
+ }
+
+}
+
+/**
+ * constructor
+ */
+function wfSpecialNewpages($par, $specialPage) {
+ global $wgRequest, $wgContLang;
+
+ list( $limit, $offset ) = wfCheckLimits();
+ $namespace = NS_MAIN;
+
+ if ( $par ) {
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'shownav' == $bit )
+ $shownavigation = true;
+ if ( is_numeric( $bit ) )
+ $limit = $bit;
+
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
+ $limit = intval($m[1]);
+ if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) )
+ $offset = intval($m[1]);
+ if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
+ $ns = $wgContLang->getNsIndex( $m[1] );
+ if( $ns !== false ) {
+ $namespace = $ns;
+ }
+ }
+ }
+ } else {
+ if( $ns = $wgRequest->getInt( 'namespace', 0 ) )
+ $namespace = $ns;
+ }
+
+ if ( ! isset( $shownavigation ) )
+ $shownavigation = ! $specialPage->including();
+
+ $npp = new NewPagesPage( $namespace );
+
+ if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) )
+ $npp->doQuery( $offset, $limit, $shownavigation );
+}
+
+?>
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
new file mode 100644
index 00000000..ffcd51fa
--- /dev/null
+++ b/includes/SpecialPage.php
@@ -0,0 +1,575 @@
+<?php
+/**
+ * SpecialPage: handling special pages and lists thereof.
+ *
+ * To add a special page in an extension, add to $wgSpecialPages either
+ * an object instance or an array containing the name and constructor
+ * parameters. The latter is preferred for performance reasons.
+ *
+ * The object instantiated must be either an instance of SpecialPage or a
+ * sub-class thereof. It must have an execute() method, which sends the HTML
+ * for the special page to $wgOut. The parent class has an execute() method
+ * which distributes the call to the historical global functions. Additionally,
+ * execute() also checks if the user has the necessary access privileges
+ * and bails out if not.
+ *
+ * To add a core special page, use the similar static list in
+ * SpecialPage::$mList. To remove a core static special page at runtime, use
+ * a SpecialPage_initList hook.
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * @access private
+ */
+
+/**
+ * Parent special page class, also static functions for handling the special
+ * page list
+ * @package MediaWiki
+ */
+class SpecialPage
+{
+ /**#@+
+ * @access private
+ */
+ /**
+ * The name of the class, used in the URL.
+ * Also used for the default <h1> heading, @see getDescription()
+ */
+ var $mName;
+ /**
+ * Minimum user level required to access this page, or "" for anyone.
+ * Also used to categorise the pages in Special:Specialpages
+ */
+ var $mRestriction;
+ /**
+ * Listed in Special:Specialpages?
+ */
+ var $mListed;
+ /**
+ * Function name called by the default execute()
+ */
+ var $mFunction;
+ /**
+ * File which needs to be included before the function above can be called
+ */
+ var $mFile;
+ /**
+ * Whether or not this special page is being included from an article
+ */
+ var $mIncluding;
+ /**
+ * Whether the special page can be included in an article
+ */
+ var $mIncludable;
+
+ static public $mList = array(
+ 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ),
+ 'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ),
+ 'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ),
+
+ 'Userlogin' => array( 'SpecialPage', 'Userlogin' ),
+ 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ),
+ 'Preferences' => array( 'SpecialPage', 'Preferences' ),
+ 'Watchlist' => array( 'SpecialPage', 'Watchlist' ),
+
+ 'Recentchanges' => array( 'IncludableSpecialPage', 'Recentchanges' ),
+ 'Upload' => array( 'SpecialPage', 'Upload' ),
+ 'Imagelist' => array( 'SpecialPage', 'Imagelist' ),
+ 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
+ 'Listusers' => array( 'SpecialPage', 'Listusers' ),
+ 'Statistics' => array( 'SpecialPage', 'Statistics' ),
+ 'Random' => array( 'SpecialPage', 'Randompage' ),
+ 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ),
+ 'Uncategorizedpages'=> array( 'SpecialPage', 'Uncategorizedpages' ),
+ 'Uncategorizedcategories'=> array( 'SpecialPage', 'Uncategorizedcategories' ),
+ 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ),
+ 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ),
+ 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
+ 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ),
+ 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ),
+ 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ),
+ 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ),
+ 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ),
+ 'Mostimages' => array( 'SpecialPage', 'Mostimages' ),
+ 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ),
+ 'Shortpages' => array( 'SpecialPage', 'Shortpages' ),
+ 'Longpages' => array( 'SpecialPage', 'Longpages' ),
+ 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ),
+ 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ),
+ 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ),
+ 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ),
+ 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) ,
+ 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ),
+ 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ),
+ 'Contributions' => array( 'UnlistedSpecialPage', 'Contributions' ),
+ 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ),
+ 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ),
+ 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ),
+ 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ),
+ 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
+ 'Booksources' => array( 'SpecialPage', 'Booksources' ),
+ 'Categories' => array( 'SpecialPage', 'Categories' ),
+ 'Export' => array( 'SpecialPage', 'Export' ),
+ 'Version' => array( 'SpecialPage', 'Version' ),
+ 'Allmessages' => array( 'SpecialPage', 'Allmessages' ),
+ 'Log' => array( 'SpecialPage', 'Log' ),
+ 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ),
+ 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
+ "Import" => array( 'SpecialPage', "Import", 'import' ),
+ 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
+ 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
+ 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ),
+ 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ),
+ 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ),
+ 'Listredirects' => array( 'SpecialPage', 'Listredirects' ),
+ 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ),
+ 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ),
+ 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ),
+ );
+
+ static public $mListInitialised = false;
+
+ /**#@-*/
+
+ /**
+ * Initialise the special page list
+ * This must be called before accessing SpecialPage::$mList
+ */
+ static function initList() {
+ global $wgSpecialPages;
+ global $wgDisableCounters, $wgDisableInternalSearch, $wgEmailAuthentication;
+
+ if ( self::$mListInitialised ) {
+ return;
+ }
+ wfProfileIn( __METHOD__ );
+
+ if( !$wgDisableCounters ) {
+ self::$mList['Popularpages'] = array( 'SpecialPage', 'Popularpages' );
+ }
+
+ if( !$wgDisableInternalSearch ) {
+ self::$mList['Search'] = array( 'SpecialPage', 'Search' );
+ }
+
+ if( $wgEmailAuthentication ) {
+ self::$mList['Confirmemail'] = array( 'UnlistedSpecialPage', 'Confirmemail' );
+ }
+
+ # Add extension special pages
+ self::$mList = array_merge( self::$mList, $wgSpecialPages );
+
+ # Better to set this now, to avoid infinite recursion in carelessly written hooks
+ self::$mListInitialised = true;
+
+ # Run hooks
+ # This hook can be used to remove undesired built-in special pages
+ wfRunHooks( 'SpecialPage_initList', array( &self::$mList ) );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Add a page to the list of valid special pages. This used to be the preferred
+ * method for adding special pages in extensions. It's now suggested that you add
+ * an associative record to $wgSpecialPages. This avoids autoloading SpecialPage.
+ *
+ * @param mixed $page Must either be an array specifying a class name and
+ * constructor parameters, or an object. The object,
+ * when constructed, must have an execute() method which
+ * sends HTML to $wgOut.
+ * @static
+ */
+ static function addPage( &$page ) {
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ self::$mList[$page->mName] = $page;
+ }
+
+ /**
+ * Remove a special page from the list
+ * Formerly used to disable expensive or dangerous special pages. The
+ * preferred method is now to add a SpecialPage_initList hook.
+ *
+ * @static
+ */
+ static function removePage( $name ) {
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ unset( self::$mList[$name] );
+ }
+
+ /**
+ * Find the object with a given name and return it (or NULL)
+ * @static
+ * @param string $name
+ */
+ static function getPage( $name ) {
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ if ( array_key_exists( $name, self::$mList ) ) {
+ $rec = self::$mList[$name];
+ if ( is_string( $rec ) ) {
+ $className = $rec;
+ self::$mList[$name] = new $className;
+ } elseif ( is_array( $rec ) ) {
+ $className = array_shift( $rec );
+ self::$mList[$name] = wfCreateObject( $className, $rec );
+ }
+ return self::$mList[$name];
+ } else {
+ return NULL;
+ }
+ }
+
+
+ /**
+ * @static
+ * @param string $name
+ * @return mixed Title object if the redirect exists, otherwise NULL
+ */
+ static function getRedirect( $name ) {
+ global $wgUser;
+
+ $redirects = array(
+ 'Mypage' => Title::makeTitle( NS_USER, $wgUser->getName() ),
+ 'Mytalk' => Title::makeTitle( NS_USER_TALK, $wgUser->getName() ),
+ 'Mycontributions' => Title::makeTitle( NS_SPECIAL, 'Contributions/' . $wgUser->getName() ),
+ 'Listadmins' => Title::makeTitle( NS_SPECIAL, 'Listusers/sysop' ), # @bug 2832
+ 'Logs' => Title::makeTitle( NS_SPECIAL, 'Log' ),
+ 'Randompage' => Title::makeTitle( NS_SPECIAL, 'Random' ),
+ 'Userlist' => Title::makeTitle( NS_SPECIAL, 'Listusers' )
+ );
+ wfRunHooks( 'SpecialPageGetRedirect', array( &$redirects ) );
+
+ return isset( $redirects[$name] ) ? $redirects[$name] : null;
+ }
+
+ /**
+ * Return part of the request string for a special redirect page
+ * This allows passing, e.g. action=history to Special:Mypage, etc.
+ *
+ * @param $name Name of the redirect page
+ * @return string
+ */
+ function getRedirectParams( $name ) {
+ global $wgRequest;
+
+ $args = array();
+ switch( $name ) {
+ case 'Mypage':
+ case 'Mytalk':
+ case 'Randompage':
+ $args = array( 'action' );
+ }
+
+ $params = array();
+ foreach( $args as $arg ) {
+ if( $val = $wgRequest->getVal( $arg, false ) )
+ $params[] = $arg . '=' . $val;
+ }
+
+ return count( $params ) ? implode( '&', $params ) : false;
+ }
+
+ /**
+ * Return categorised listable special pages
+ * Returns a 2d array where the first index is the restriction name
+ * @static
+ */
+ static function getPages() {
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ $pages = array(
+ '' => array(),
+ 'sysop' => array(),
+ 'developer' => array()
+ );
+
+ foreach ( self::$mList as $name => $rec ) {
+ $page = self::getPage( $name );
+ if ( $page->isListed() ) {
+ $pages[$page->getRestriction()][$page->getName()] = $page;
+ }
+ }
+ return $pages;
+ }
+
+ /**
+ * Execute a special page path.
+ * The path may contain parameters, e.g. Special:Name/Params
+ * Extracts the special page name and call the execute method, passing the parameters
+ *
+ * Returns a title object if the page is redirected, false if there was no such special
+ * page, and true if it was successful.
+ *
+ * @param $title a title object
+ * @param $including output is being captured for use in {{special:whatever}}
+ */
+ function executePath( &$title, $including = false ) {
+ global $wgOut, $wgTitle;
+ $fname = 'SpecialPage::executePath';
+ wfProfileIn( $fname );
+
+ $bits = split( "/", $title->getDBkey(), 2 );
+ $name = $bits[0];
+ if( !isset( $bits[1] ) ) { // bug 2087
+ $par = NULL;
+ } else {
+ $par = $bits[1];
+ }
+
+ $page = SpecialPage::getPage( $name );
+ if ( is_null( $page ) ) {
+ if ( $including ) {
+ wfProfileOut( $fname );
+ return false;
+ } else {
+ $redir = SpecialPage::getRedirect( $name );
+ if ( isset( $redir ) ) {
+ if( $par )
+ $redir = Title::makeTitle( $redir->getNamespace(), $redir->getText() . '/' . $par );
+ $params = SpecialPage::getRedirectParams( $name );
+ if( $params ) {
+ $url = $redir->getFullUrl( $params );
+ } else {
+ $url = $redir->getFullUrl();
+ }
+ $wgOut->redirect( $url );
+ $retVal = $redir;
+ $wgOut->redirect( $url );
+ $retVal = $redir;
+ } else {
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setStatusCode( 404 );
+ $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
+ $retVal = false;
+ }
+ }
+ } else {
+ if ( $including && !$page->includable() ) {
+ wfProfileOut( $fname );
+ return false;
+ } elseif ( !$including ) {
+ if($par !== NULL) {
+ $wgTitle = Title::makeTitle( NS_SPECIAL, $name );
+ } else {
+ $wgTitle = $title;
+ }
+ }
+ $page->including( $including );
+
+ $profName = 'Special:' . $page->getName();
+ wfProfileIn( $profName );
+ $page->execute( $par );
+ wfProfileOut( $profName );
+ $retVal = true;
+ }
+ wfProfileOut( $fname );
+ return $retVal;
+ }
+
+ /**
+ * Just like executePath() except it returns the HTML instead of outputting it
+ * Returns false if there was no such special page, or a title object if it was
+ * a redirect.
+ * @static
+ */
+ static function capturePath( &$title ) {
+ global $wgOut, $wgTitle;
+
+ $oldTitle = $wgTitle;
+ $oldOut = $wgOut;
+ $wgOut = new OutputPage;
+
+ $ret = SpecialPage::executePath( $title, true );
+ if ( $ret === true ) {
+ $ret = $wgOut->getHTML();
+ }
+ $wgTitle = $oldTitle;
+ $wgOut = $oldOut;
+ return $ret;
+ }
+
+ /**
+ * Default constructor for special pages
+ * Derivative classes should call this from their constructor
+ * Note that if the user does not have the required level, an error message will
+ * be displayed by the default execute() method, without the global function ever
+ * being called.
+ *
+ * If you override execute(), you can recover the default behaviour with userCanExecute()
+ * and displayRestrictionError()
+ *
+ * @param string $name Name of the special page, as seen in links and URLs
+ * @param string $restriction Minimum user level required, e.g. "sysop" or "developer".
+ * @param boolean $listed Whether the page is listed in Special:Specialpages
+ * @param string $function Function called by execute(). By default it is constructed from $name
+ * @param string $file File which is included by execute(). It is also constructed from $name by default
+ */
+ function SpecialPage( $name = '', $restriction = '', $listed = true, $function = false, $file = 'default', $includable = false ) {
+ $this->mName = $name;
+ $this->mRestriction = $restriction;
+ $this->mListed = $listed;
+ $this->mIncludable = $includable;
+ if ( $function == false ) {
+ $this->mFunction = 'wfSpecial'.$name;
+ } else {
+ $this->mFunction = $function;
+ }
+ if ( $file === 'default' ) {
+ $this->mFile = "Special{$name}.php";
+ } else {
+ $this->mFile = $file;
+ }
+ }
+
+ /**#@+
+ * Accessor
+ *
+ * @deprecated
+ */
+ function getName() { return $this->mName; }
+ function getRestriction() { return $this->mRestriction; }
+ function getFile() { return $this->mFile; }
+ function isListed() { return $this->mListed; }
+ /**#@-*/
+
+ /**#@+
+ * Accessor and mutator
+ */
+ function name( $x = NULL ) { return wfSetVar( $this->mName, $x ); }
+ function restrictions( $x = NULL) { return wfSetVar( $this->mRestrictions, $x ); }
+ function listed( $x = NULL) { return wfSetVar( $this->mListed, $x ); }
+ function func( $x = NULL) { return wfSetVar( $this->mFunction, $x ); }
+ function file( $x = NULL) { return wfSetVar( $this->mFile, $x ); }
+ function includable( $x = NULL ) { return wfSetVar( $this->mIncludable, $x ); }
+ function including( $x = NULL ) { return wfSetVar( $this->mIncluding, $x ); }
+ /**#@-*/
+
+ /**
+ * Checks if the given user (identified by an object) can execute this
+ * special page (as defined by $mRestriction)
+ */
+ function userCanExecute( &$user ) {
+ if ( $this->mRestriction == "" ) {
+ return true;
+ } else {
+ if ( in_array( $this->mRestriction, $user->getRights() ) ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Output an error message telling the user what access level they have to have
+ */
+ function displayRestrictionError() {
+ global $wgOut;
+ $wgOut->permissionRequired( $this->mRestriction );
+ }
+
+ /**
+ * Sets headers - this should be called from the execute() method of all derived classes!
+ */
+ function setHeaders() {
+ global $wgOut;
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotPolicy( "noindex,nofollow" );
+ $wgOut->setPageTitle( $this->getDescription() );
+ }
+
+ /**
+ * Default execute method
+ * Checks user permissions, calls the function given in mFunction
+ */
+ function execute( $par ) {
+ global $wgUser;
+
+ $this->setHeaders();
+
+ if ( $this->userCanExecute( $wgUser ) ) {
+ $func = $this->mFunction;
+ // only load file if the function does not exist
+ if(!function_exists($func) and $this->mFile) {
+ require_once( $this->mFile );
+ }
+ if ( wfRunHooks( 'SpecialPageExecuteBeforeHeader', array( &$this, &$par, &$func ) ) )
+ $this->outputHeader();
+ if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) )
+ return;
+ $func( $par, $this );
+ if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) )
+ return;
+ } else {
+ $this->displayRestrictionError();
+ }
+ }
+
+ function outputHeader() {
+ global $wgOut, $wgContLang;
+
+ $msg = $wgContLang->lc( $this->name() ) . '-summary';
+ $out = wfMsg( $msg );
+ if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() )
+ $wgOut->addWikiText( $out );
+
+ }
+
+ # Returns the name that goes in the <h1> in the special page itself, and also the name that
+ # will be listed in Special:Specialpages
+ #
+ # Derived classes can override this, but usually it is easier to keep the default behaviour.
+ # Messages can be added at run-time, see MessageCache.php
+ function getDescription() {
+ return wfMsg( strtolower( $this->mName ) );
+ }
+
+ /**
+ * Get a self-referential title object
+ */
+ function getTitle() {
+ return Title::makeTitle( NS_SPECIAL, $this->mName );
+ }
+
+ /**
+ * Set whether this page is listed in Special:Specialpages, at run-time
+ */
+ function setListed( $listed ) {
+ return wfSetVar( $this->mListed, $listed );
+ }
+
+}
+
+/**
+ * Shortcut to construct a special page which is unlisted by default
+ * @package MediaWiki
+ */
+class UnlistedSpecialPage extends SpecialPage
+{
+ function UnlistedSpecialPage( $name, $restriction = '', $function = false, $file = 'default' ) {
+ SpecialPage::SpecialPage( $name, $restriction, false, $function, $file );
+ }
+}
+
+/**
+ * Shortcut to construct an includable special page
+ * @package MediaWiki
+ */
+class IncludableSpecialPage extends SpecialPage
+{
+ function IncludableSpecialPage( $name, $restriction = '', $listed = true, $function = false, $file = 'default' ) {
+ SpecialPage::SpecialPage( $name, $restriction, $listed, $function, $file, true );
+ }
+}
+?>
diff --git a/includes/SpecialPopularpages.php b/includes/SpecialPopularpages.php
new file mode 100644
index 00000000..77d41437
--- /dev/null
+++ b/includes/SpecialPopularpages.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class PopularPagesPage extends QueryPage {
+
+ function getName() {
+ return "Popularpages";
+ }
+
+ function isExpensive() {
+ # page_counter is not indexed
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+
+ return
+ "SELECT 'Popularpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_counter as value
+ FROM $page
+ WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $title = Title::makeTitle( $result->namespace, $result->title );
+ $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+ $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($link, $nv);
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialPopularpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $ppp = new PopularPagesPage();
+
+ return $ppp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php
new file mode 100644
index 00000000..c6003b7c
--- /dev/null
+++ b/includes/SpecialPreferences.php
@@ -0,0 +1,937 @@
+<?php
+/**
+ * Hold things related to displaying and saving user preferences.
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Entry point that create the "Preferences" object
+ */
+function wfSpecialPreferences() {
+ global $wgRequest;
+
+ $form = new PreferencesForm( $wgRequest );
+ $form->execute();
+}
+
+/**
+ * Preferences form handling
+ * This object will show the preferences form and can save it as well.
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class PreferencesForm {
+ var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $mStubs;
+ var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
+ var $mUserLanguage, $mUserVariant;
+ var $mSearch, $mRecent, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
+ var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize;
+ var $mUnderline, $mWatchlistEdits;
+
+ /**
+ * Constructor
+ * Load some values
+ */
+ function PreferencesForm( &$request ) {
+ global $wgLang, $wgContLang, $wgUser, $wgAllowRealName;
+
+ $this->mQuickbar = $request->getVal( 'wpQuickbar' );
+ $this->mOldpass = $request->getVal( 'wpOldpass' );
+ $this->mNewpass = $request->getVal( 'wpNewpass' );
+ $this->mRetypePass =$request->getVal( 'wpRetypePass' );
+ $this->mStubs = $request->getVal( 'wpStubs' );
+ $this->mRows = $request->getVal( 'wpRows' );
+ $this->mCols = $request->getVal( 'wpCols' );
+ $this->mSkin = $request->getVal( 'wpSkin' );
+ $this->mMath = $request->getVal( 'wpMath' );
+ $this->mDate = $request->getVal( 'wpDate' );
+ $this->mUserEmail = $request->getVal( 'wpUserEmail' );
+ $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : '';
+ $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1;
+ $this->mNick = $request->getVal( 'wpNick' );
+ $this->mUserLanguage = $request->getVal( 'wpUserLanguage' );
+ $this->mUserVariant = $request->getVal( 'wpUserVariant' );
+ $this->mSearch = $request->getVal( 'wpSearch' );
+ $this->mRecent = $request->getVal( 'wpRecent' );
+ $this->mHourDiff = $request->getVal( 'wpHourDiff' );
+ $this->mSearchLines = $request->getVal( 'wpSearchLines' );
+ $this->mSearchChars = $request->getVal( 'wpSearchChars' );
+ $this->mImageSize = $request->getVal( 'wpImageSize' );
+ $this->mThumbSize = $request->getInt( 'wpThumbSize' );
+ $this->mUnderline = $request->getInt( 'wpOpunderline' );
+ $this->mAction = $request->getVal( 'action' );
+ $this->mReset = $request->getCheck( 'wpReset' );
+ $this->mPosted = $request->wasPosted();
+ $this->mSuccess = $request->getCheck( 'success' );
+ $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
+ $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
+
+ $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
+ $this->mPosted &&
+ $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+
+ # User toggles (the big ugly unsorted list of checkboxes)
+ $this->mToggles = array();
+ if ( $this->mPosted ) {
+ $togs = $wgLang->getUserToggles();
+ foreach ( $togs as $tname ) {
+ $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0;
+ }
+ }
+
+ $this->mUsedToggles = array();
+
+ # Search namespace options
+ # Note: namespaces don't necessarily have consecutive keys
+ $this->mSearchNs = array();
+ if ( $this->mPosted ) {
+ $namespaces = $wgContLang->getNamespaces();
+ foreach ( $namespaces as $i => $namespace ) {
+ if ( $i >= 0 ) {
+ $this->mSearchNs[$i] = $request->getCheck( "wpNs$i" ) ? 1 : 0;
+ }
+ }
+ }
+
+ # Validate language
+ if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) {
+ $this->mUserLanguage = 'nolanguage';
+ }
+ }
+
+ function execute() {
+ global $wgUser, $wgOut;
+
+ if ( $wgUser->isAnon() ) {
+ $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
+ return;
+ }
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if ( $this->mReset ) {
+ $this->resetPrefs();
+ $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
+ } else if ( $this->mSaveprefs ) {
+ $this->savePreferences();
+ } else {
+ $this->resetPrefs();
+ $this->mainPrefsForm( '' );
+ }
+ }
+ /**
+ * @access private
+ */
+ function validateInt( &$val, $min=0, $max=0x7fffffff ) {
+ $val = intval($val);
+ $val = min($val, $max);
+ $val = max($val, $min);
+ return $val;
+ }
+
+ /**
+ * @access private
+ */
+ function validateFloat( &$val, $min, $max=0x7fffffff ) {
+ $val = floatval( $val );
+ $val = min( $val, $max );
+ $val = max( $val, $min );
+ return( $val );
+ }
+
+ /**
+ * @access private
+ */
+ function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) {
+ $val = trim($val);
+ if($val === '') {
+ return $val;
+ } else {
+ return $this->validateInt( $val, $min, $max );
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function validateDate( &$val, $min = 0, $max=0x7fffffff ) {
+ if ( ( sprintf('%d', $val) === $val && $val >= $min && $val <= $max ) || $val == 'ISO 8601' )
+ return $val;
+ else
+ return 0;
+ }
+
+ /**
+ * Used to validate the user inputed timezone before saving it as
+ * 'timeciorrection', will return '00:00' if fed bogus data.
+ * Note: It's not a 100% correct implementation timezone-wise, it will
+ * accept stuff like '14:30',
+ * @access private
+ * @param string $s the user input
+ * @return string
+ */
+ function validateTimeZone( $s ) {
+ if ( $s !== '' ) {
+ if ( strpos( $s, ':' ) ) {
+ # HH:MM
+ $array = explode( ':' , $s );
+ $hour = intval( $array[0] );
+ $minute = intval( $array[1] );
+ } else {
+ $minute = intval( $s * 60 );
+ $hour = intval( $minute / 60 );
+ $minute = abs( $minute ) % 60;
+ }
+ # Max is +14:00 and min is -12:00, see:
+ # http://en.wikipedia.org/wiki/Timezone
+ $hour = min( $hour, 14 );
+ $hour = max( $hour, -12 );
+ $minute = min( $minute, 59 );
+ $minute = max( $minute, 0 );
+ $s = sprintf( "%02d:%02d", $hour, $minute );
+ }
+ return $s;
+ }
+
+ /**
+ * @access private
+ */
+ function savePreferences() {
+ global $wgUser, $wgOut, $wgParser;
+ global $wgEnableUserEmail, $wgEnableEmail;
+ global $wgEmailAuthentication, $wgMinimalPasswordLength;
+ global $wgAuth;
+
+
+ if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
+ if ( $this->mNewpass != $this->mRetypePass ) {
+ $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
+ return;
+ }
+
+ if ( strlen( $this->mNewpass ) < $wgMinimalPasswordLength ) {
+ $this->mainPrefsForm( 'error', wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
+ return;
+ }
+
+ if (!$wgUser->checkPassword( $this->mOldpass )) {
+ $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
+ return;
+ }
+ if (!$wgAuth->setPassword( $wgUser, $this->mNewpass )) {
+ $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+ return;
+ }
+ $wgUser->setPassword( $this->mNewpass );
+ $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
+
+ }
+ $wgUser->setRealName( $this->mRealName );
+
+ if( $wgUser->getOption( 'language' ) !== $this->mUserLanguage ) {
+ $needRedirect = true;
+ } else {
+ $needRedirect = false;
+ }
+
+ # Validate the signature and clean it up as needed
+ if( $this->mToggles['fancysig'] ) {
+ if( Parser::validateSig( $this->mNick ) !== false ) {
+ $this->mNick = $wgParser->cleanSig( $this->mNick );
+ } else {
+ $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
+ }
+ } else {
+ // When no fancy sig used, make sure ~{3,5} get removed.
+ $this->mNick = $wgParser->cleanSigInSig( $this->mNick );
+ }
+
+ $wgUser->setOption( 'language', $this->mUserLanguage );
+ $wgUser->setOption( 'variant', $this->mUserVariant );
+ $wgUser->setOption( 'nickname', $this->mNick );
+ $wgUser->setOption( 'quickbar', $this->mQuickbar );
+ $wgUser->setOption( 'skin', $this->mSkin );
+ global $wgUseTeX;
+ if( $wgUseTeX ) {
+ $wgUser->setOption( 'math', $this->mMath );
+ }
+ $wgUser->setOption( 'date', $this->validateDate( $this->mDate, 0, 20 ) );
+ $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) );
+ $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) );
+ $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) );
+ $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) );
+ $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) );
+ $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) );
+ $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) );
+ $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) );
+ $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) );
+ $wgUser->setOption( 'imagesize', $this->mImageSize );
+ $wgUser->setOption( 'thumbsize', $this->mThumbSize );
+ $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
+ $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
+
+ # Set search namespace options
+ foreach( $this->mSearchNs as $i => $value ) {
+ $wgUser->setOption( "searchNs{$i}", $value );
+ }
+
+ if( $wgEnableEmail && $wgEnableUserEmail ) {
+ $wgUser->setOption( 'disablemail', $this->mEmailFlag );
+ }
+
+ # Set user toggles
+ foreach ( $this->mToggles as $tname => $tvalue ) {
+ $wgUser->setOption( $tname, $tvalue );
+ }
+ if (!$wgAuth->updateExternalDB($wgUser)) {
+ $this->mainPrefsForm( wfMsg( 'externaldberror' ) );
+ return;
+ }
+ $wgUser->setCookies();
+ $wgUser->saveSettings();
+
+ $error = false;
+ if( $wgEnableEmail ) {
+ $newadr = $this->mUserEmail;
+ $oldadr = $wgUser->getEmail();
+ if( ($newadr != '') && ($newadr != $oldadr) ) {
+ # the user has supplied a new email address on the login page
+ if( $wgUser->isValidEmailAddr( $newadr ) ) {
+ $wgUser->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record
+ $wgUser->mEmailAuthenticated = null; # but flag as "dirty" = unauthenticated
+ $wgUser->saveSettings();
+ if ($wgEmailAuthentication) {
+ # Mail a temporary password to the dirty address.
+ # User can come back through the confirmation URL to re-enable email.
+ $result = $wgUser->sendConfirmationMail();
+ if( WikiError::isError( $result ) ) {
+ $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
+ } else {
+ $error = wfMsg( 'eauthentsent', $wgUser->getName() );
+ }
+ }
+ } else {
+ $error = wfMsg( 'invalidemailaddress' );
+ }
+ } else {
+ $wgUser->setEmail( $this->mUserEmail );
+ $wgUser->setCookies();
+ $wgUser->saveSettings();
+ }
+ }
+
+ if( $needRedirect && $error === false ) {
+ $title =& Title::makeTitle( NS_SPECIAL, "Preferences" );
+ $wgOut->redirect($title->getFullURL('success'));
+ return;
+ }
+
+ $wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) );
+ $po = ParserOptions::newFromUser( $wgUser );
+ $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
+ }
+
+ /**
+ * @access private
+ */
+ function resetPrefs() {
+ global $wgUser, $wgLang, $wgContLang, $wgAllowRealName;
+
+ $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
+ $this->mUserEmail = $wgUser->getEmail();
+ $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
+ $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
+ $this->mUserLanguage = $wgUser->getOption( 'language' );
+ if( empty( $this->mUserLanguage ) ) {
+ # Quick hack for conversions, where this value is blank
+ global $wgContLanguageCode;
+ $this->mUserLanguage = $wgContLanguageCode;
+ }
+ $this->mUserVariant = $wgUser->getOption( 'variant');
+ $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
+ $this->mNick = $wgUser->getOption( 'nickname' );
+
+ $this->mQuickbar = $wgUser->getOption( 'quickbar' );
+ $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) );
+ $this->mMath = $wgUser->getOption( 'math' );
+ $this->mDate = $wgUser->getOption( 'date' );
+ $this->mRows = $wgUser->getOption( 'rows' );
+ $this->mCols = $wgUser->getOption( 'cols' );
+ $this->mStubs = $wgUser->getOption( 'stubthreshold' );
+ $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
+ $this->mSearch = $wgUser->getOption( 'searchlimit' );
+ $this->mSearchLines = $wgUser->getOption( 'contextlines' );
+ $this->mSearchChars = $wgUser->getOption( 'contextchars' );
+ $this->mImageSize = $wgUser->getOption( 'imagesize' );
+ $this->mThumbSize = $wgUser->getOption( 'thumbsize' );
+ $this->mRecent = $wgUser->getOption( 'rclimit' );
+ $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' );
+ $this->mUnderline = $wgUser->getOption( 'underline' );
+ $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
+
+ $togs = $wgLang->getUserToggles();
+ foreach ( $togs as $tname ) {
+ $ttext = wfMsg('tog-'.$tname);
+ $this->mToggles[$tname] = $wgUser->getOption( $tname );
+ }
+
+ $namespaces = $wgContLang->getNamespaces();
+ foreach ( $namespaces as $i => $namespace ) {
+ if ( $i >= NS_MAIN ) {
+ $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i );
+ }
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function namespacesCheckboxes() {
+ global $wgContLang;
+
+ # Determine namespace checkboxes
+ $namespaces = $wgContLang->getNamespaces();
+ $r1 = null;
+
+ foreach ( $namespaces as $i => $name ) {
+ if ($i < 0)
+ continue;
+ $checked = $this->mSearchNs[$i] ? "checked='checked'" : '';
+ $name = str_replace( '_', ' ', $namespaces[$i] );
+
+ if ( empty($name) )
+ $name = wfMsg( 'blanknamespace' );
+
+ $r1 .= "<input type='checkbox' value='1' name='wpNs$i' id='wpNs$i' {$checked}/> <label for='wpNs$i'>{$name}</label><br />\n";
+ }
+ return $r1;
+ }
+
+
+ function getToggle( $tname, $trailer = false, $disabled = false ) {
+ global $wgUser, $wgLang;
+
+ $this->mUsedToggles[$tname] = true;
+ $ttext = $wgLang->getUserToggle( $tname );
+
+ $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : '';
+ $disabled = $disabled ? ' disabled="disabled"' : '';
+ $trailer = $trailer ? $trailer : '';
+ return "<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\"$checked$disabled />" .
+ " <span class='toggletext'><label for=\"$tname\">$ttext</label>$trailer</span></div>\n";
+ }
+
+ function getToggles( $items ) {
+ $out = "";
+ foreach( $items as $item ) {
+ if( $item === false )
+ continue;
+ if( is_array( $item ) ) {
+ list( $key, $trailer ) = $item;
+ } else {
+ $key = $item;
+ $trailer = false;
+ }
+ $out .= $this->getToggle( $key, $trailer );
+ }
+ return $out;
+ }
+
+ function addRow($td1, $td2) {
+ return "<tr><td align='right'>$td1</td><td align='left'>$td2</td></tr>";
+ }
+
+ /**
+ * @access private
+ */
+ function mainPrefsForm( $status , $message = '' ) {
+ global $wgUser, $wgOut, $wgLang, $wgContLang;
+ global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
+ global $wgDisableLangConversion;
+ global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
+ global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
+ global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
+ global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
+
+ $wgOut->setPageTitle( wfMsg( 'preferences' ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+ if ( $this->mSuccess || 'success' == $status ) {
+ $wgOut->addWikitext( '<div class="successbox"><strong>'. wfMsg( 'savedprefs' ) . '</strong></div>' );
+ } else if ( 'error' == $status ) {
+ $wgOut->addWikitext( '<div class="errorbox"><strong>' . $message . '</strong></div>' );
+ } else if ( '' != $status ) {
+ $wgOut->addWikitext( $message . "\n----" );
+ }
+
+ $qbs = $wgLang->getQuickbarSettings();
+ $skinNames = $wgLang->getSkinNames();
+ $mathopts = $wgLang->getMathNames();
+ $dateopts = $wgLang->getDateFormats();
+ $togs = $wgLang->getUserToggles();
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Preferences' );
+ $action = $titleObj->escapeLocalURL();
+
+ # Pre-expire some toggles so they won't show if disabled
+ $this->mUsedToggles[ 'shownumberswatching' ] = true;
+ $this->mUsedToggles[ 'showupdated' ] = true;
+ $this->mUsedToggles[ 'enotifwatchlistpages' ] = true;
+ $this->mUsedToggles[ 'enotifusertalkpages' ] = true;
+ $this->mUsedToggles[ 'enotifminoredits' ] = true;
+ $this->mUsedToggles[ 'enotifrevealaddr' ] = true;
+ $this->mUsedToggles[ 'uselivepreview' ] = true;
+
+ # Enotif
+ # <FIXME>
+ $this->mUserEmail = htmlspecialchars( $this->mUserEmail );
+ $this->mRealName = htmlspecialchars( $this->mRealName );
+ $rawNick = $this->mNick;
+ $this->mNick = htmlspecialchars( $this->mNick );
+ if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
+ else { $emfc = ''; }
+
+
+ if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
+ if( $wgUser->getEmailAuthenticationTimestamp() ) {
+ $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
+ $disableEmailPrefs = false;
+ } else {
+ $disableEmailPrefs = true;
+ $skin = $wgUser->getSkin();
+ $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
+ $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Confirmemail' ),
+ wfMsg( 'emailconfirmlink' ) );
+ }
+ } else {
+ $emailauthenticated = '';
+ $disableEmailPrefs = false;
+ }
+
+ if ($this->mUserEmail == '') {
+ $emailauthenticated = wfMsg( 'noemailprefs' );
+ }
+
+ $ps = $this->namespacesCheckboxes();
+
+ $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : '';
+ $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : '';
+ $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : '';
+ $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : '';
+ $prefs_help_email_enotif = ( $wgEnotifWatchlist || $wgEnotifUserTalk) ? ' ' . wfMsg('prefs-help-email-enotif') : '';
+ $prefs_help_realname = '';
+
+ # </FIXME>
+
+ $wgOut->addHTML( "<form action=\"$action\" method='post'>" );
+ $wgOut->addHTML( "<div id='preferences'>" );
+
+ # User data
+ #
+
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('prefs-personal') . "</legend>\n<table>\n");
+
+ $wgOut->addHTML(
+ $this->addRow(
+ wfMsg( 'username'),
+ $wgUser->getName()
+ )
+ );
+
+ $wgOut->addHTML(
+ $this->addRow(
+ wfMsg( 'uid' ),
+ $wgUser->getID()
+ )
+ );
+
+
+ if ($wgAllowRealName) {
+ $wgOut->addHTML(
+ $this->addRow(
+ '<label for="wpRealName">' . wfMsg('yourrealname') . '</label>',
+ "<input type='text' name='wpRealName' id='wpRealName' value=\"{$this->mRealName}\" size='25' />"
+ )
+ );
+ }
+ if ($wgEnableEmail) {
+ $wgOut->addHTML(
+ $this->addRow(
+ '<label for="wpUserEmail">' . wfMsg( 'youremail' ) . '</label>',
+ "<input type='text' name='wpUserEmail' id='wpUserEmail' value=\"{$this->mUserEmail}\" size='25' />"
+ )
+ );
+ }
+
+ global $wgParser;
+ if( !empty( $this->mToggles['fancysig'] ) &&
+ false === $wgParser->validateSig( $rawNick ) ) {
+ $invalidSig = $this->addRow(
+ '&nbsp;',
+ '<span class="error">' . wfMsgHtml( 'badsig' ) . '<span>'
+ );
+ } else {
+ $invalidSig = '';
+ }
+
+ $wgOut->addHTML(
+ $this->addRow(
+ '<label for="wpNick">' . wfMsg( 'yournick' ) . '</label>',
+ "<input type='text' name='wpNick' id='wpNick' value=\"{$this->mNick}\" size='25' />"
+ ) .
+ $invalidSig .
+ # FIXME: The <input> part should be where the &nbsp; is, getToggle() needs
+ # to be changed to out return its output in two parts. -ævar
+ $this->addRow(
+ '&nbsp;',
+ $this->getToggle( 'fancysig' )
+ )
+ );
+
+ /**
+ * Make sure the site language is in the list; a custom language code
+ * might not have a defined name...
+ */
+ $languages = $wgLang->getLanguageNames();
+ if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
+ $languages[$wgContLanguageCode] = $wgContLanguageCode;
+ }
+ ksort( $languages );
+
+ /**
+ * If a bogus value is set, default to the content language.
+ * Otherwise, no default is selected and the user ends up
+ * with an Afrikaans interface since it's first in the list.
+ */
+ $selectedLang = isset( $languages[$this->mUserLanguage] ) ? $this->mUserLanguage : $wgContLanguageCode;
+ $selbox = null;
+ foreach($languages as $code => $name) {
+ global $IP;
+ /* only add languages that have a file */
+ $langfile="$IP/languages/Language".str_replace('-', '_', ucfirst($code)).".php";
+ if(file_exists($langfile) || $code == $wgContLanguageCode) {
+ $sel = ($code == $selectedLang)? ' selected="selected"' : '';
+ $selbox .= "<option value=\"$code\"$sel>$code - $name</option>\n";
+ }
+ }
+ $wgOut->addHTML(
+ $this->addRow(
+ '<label for="wpUserLanguage">' . wfMsg('yourlanguage') . '</label>',
+ "<select name='wpUserLanguage' id='wpUserLanguage'>$selbox</select>"
+ )
+ );
+
+ /* see if there are multiple language variants to choose from*/
+ if(!$wgDisableLangConversion) {
+ $variants = $wgContLang->getVariants();
+ $variantArray = array();
+
+ foreach($variants as $v) {
+ $v = str_replace( '_', '-', strtolower($v));
+ if( array_key_exists( $v, $languages ) ) {
+ // If it doesn't have a name, we'll pretend it doesn't exist
+ $variantArray[$v] = $languages[$v];
+ }
+ }
+
+ $selbox = null;
+ foreach($variantArray as $code => $name) {
+ $sel = $code == $this->mUserVariant ? 'selected="selected"' : '';
+ $selbox .= "<option value=\"$code\" $sel>$code - $name</option>";
+ }
+
+ if(count($variantArray) > 1) {
+ $wgOut->addHtml(
+ $this->addRow( wfMsg( 'yourvariant' ), "<select name='wpUserVariant'>$selbox</select>" )
+ );
+ }
+ }
+ $wgOut->addHTML('</table>');
+
+ # Password
+ if( $wgAuth->allowPasswordChange() ) {
+ $this->mOldpass = htmlspecialchars( $this->mOldpass );
+ $this->mNewpass = htmlspecialchars( $this->mNewpass );
+ $this->mRetypePass = htmlspecialchars( $this->mRetypePass );
+
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'changepassword' ) . '</legend><table>');
+ $wgOut->addHTML(
+ $this->addRow(
+ '<label for="wpOldpass">' . wfMsg( 'oldpassword' ) . '</label>',
+ "<input type='password' name='wpOldpass' id='wpOldpass' value=\"{$this->mOldpass}\" size='20' />"
+ ) .
+ $this->addRow(
+ '<label for="wpNewpass">' . wfMsg( 'newpassword' ) . '</label>',
+ "<input type='password' name='wpNewpass' id='wpNewpass' value=\"{$this->mNewpass}\" size='20' />"
+ ) .
+ $this->addRow(
+ '<label for="wpRetypePass">' . wfMsg( 'retypenew' ) . '</label>',
+ "<input type='password' name='wpRetypePass' id='wpRetypePass' value=\"{$this->mRetypePass}\" size='20' />"
+ ) .
+ "</table>\n" .
+ $this->getToggle( "rememberpassword" ) . "</fieldset>\n\n" );
+ }
+
+ # <FIXME>
+ # Enotif
+ if ($wgEnableEmail) {
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'email' ) . '</legend>' );
+ $wgOut->addHTML(
+ $emailauthenticated.
+ $enotifrevealaddr.
+ $enotifwatchlistpages.
+ $enotifusertalkpages.
+ $enotifminoredits );
+ if ($wgEnableUserEmail) {
+ $emf = wfMsg( 'allowemail' );
+ $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
+ $wgOut->addHTML(
+ "<div><input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label></div>" );
+ }
+
+ $wgOut->addHTML( '</fieldset>' );
+ }
+ # </FIXME>
+
+ # Show little "help" tips for the real name and email address options
+ if( $wgAllowRealName || $wgEnableEmail ) {
+ if( $wgAllowRealName )
+ $tips[] = wfMsg( 'prefs-help-realname' );
+ if( $wgEnableEmail )
+ $tips[] = wfMsg( 'prefs-help-email' );
+ $wgOut->addHtml( '<div class="prefsectiontip">' . implode( '<br />', $tips ) . '</div>' );
+ }
+
+ $wgOut->addHTML( '</fieldset>' );
+
+ # Quickbar
+ #
+ if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') {
+ $wgOut->addHtml( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" );
+ for ( $i = 0; $i < count( $qbs ); ++$i ) {
+ if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; }
+ else { $checked = ""; }
+ $wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" );
+ }
+ $wgOut->addHtml( "</fieldset>\n\n" );
+ } else {
+ # Need to output a hidden option even if the relevant skin is not in use,
+ # otherwise the preference will get reset to 0 on submit
+ $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) );
+ }
+
+ # Skin
+ #
+ $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" );
+ $mptitle = Title::newMainPage();
+ $previewtext = wfMsg('skinpreview');
+ # Only show members of Skin::getSkinNames() rather than
+ # $skinNames (skins is all skin names from Language.php)
+ $validSkinNames = Skin::getSkinNames();
+ foreach ($validSkinNames as $skinkey => $skinname ) {
+ if ( in_array( $skinkey, $wgSkipSkins ) ) {
+ continue;
+ }
+ $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
+ $sn = isset( $skinNames[$skinkey] ) ? $skinNames[$skinkey] : $skinname;
+
+ $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey"));
+ $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
+ if( $skinkey == $wgDefaultSkin )
+ $sn .= ' (' . wfMsg( 'default' ) . ')';
+ $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" );
+ }
+ $wgOut->addHTML( "</fieldset>\n\n" );
+
+ # Math
+ #
+ global $wgUseTeX;
+ if( $wgUseTeX ) {
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' );
+ foreach ( $mathopts as $k => $v ) {
+ $checked = $k == $this->mMath ? ' checked="checked"' : '';
+ $wgOut->addHTML( "<div><label><input type='radio' name='wpMath' value=\"$k\"$checked /> ".wfMsg($v)."</label></div>\n" );
+ }
+ $wgOut->addHTML( "</fieldset>\n\n" );
+ }
+
+ # Files
+ #
+ $wgOut->addHTML("<fieldset>
+ <legend>" . wfMsg( 'files' ) . "</legend>
+ <div><label for='wpImageSize'>" . wfMsg('imagemaxsize') . "</label> <select id='wpImageSize' name='wpImageSize'>");
+
+ $imageLimitOptions = null;
+ foreach ( $wgImageLimits as $index => $limits ) {
+ $selected = ($index == $this->mImageSize) ? 'selected="selected"' : '';
+ $imageLimitOptions .= "<option value=\"{$index}\" {$selected}>{$limits[0]}×{$limits[1]}". wfMsgHtml('unit-pixel') ."</option>\n";
+ }
+
+ $imageThumbOptions = null;
+ $wgOut->addHTML( "{$imageLimitOptions}</select></div>
+ <div><label for='wpThumbSize'>" . wfMsg('thumbsize') . "</label> <select name='wpThumbSize' id='wpThumbSize'>");
+ foreach ( $wgThumbLimits as $index => $size ) {
+ $selected = ($index == $this->mThumbSize) ? 'selected="selected"' : '';
+ $imageThumbOptions .= "<option value=\"{$index}\" {$selected}>{$size}". wfMsgHtml('unit-pixel') ."</option>\n";
+ }
+ $wgOut->addHTML( "{$imageThumbOptions}</select></div></fieldset>\n\n");
+
+ # Date format
+ #
+ # Date/Time
+ #
+
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'datetime' ) . "</legend>\n" );
+
+ if ($dateopts) {
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'dateformat' ) . "</legend>\n" );
+ $idCnt = 0;
+ $epoch = '20010408091234';
+ foreach($dateopts as $key => $option) {
+ if( $key == MW_DATE_DEFAULT ) {
+ $formatted = wfMsgHtml( 'datedefault' );
+ } else {
+ $formatted = htmlspecialchars( $wgLang->timeanddate( $epoch, false, $key ) );
+ }
+ ($key == $this->mDate) ? $checked = ' checked="checked"' : $checked = '';
+ $wgOut->addHTML( "<div><input type='radio' name=\"wpDate\" id=\"wpDate$idCnt\" ".
+ "value=\"$key\"$checked /> <label for=\"wpDate$idCnt\">$formatted</label></div>\n" );
+ $idCnt++;
+ }
+ $wgOut->addHTML( "</fieldset>\n" );
+ }
+
+ $nowlocal = $wgLang->time( $now = wfTimestampNow(), true );
+ $nowserver = $wgLang->time( $now, false );
+
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'timezonelegend' ). '</legend><table>' .
+ $this->addRow( wfMsg( 'servertime' ), $nowserver ) .
+ $this->addRow( wfMsg( 'localtime' ), $nowlocal ) .
+ $this->addRow(
+ '<label for="wpHourDiff">' . wfMsg( 'timezoneoffset' ) . '</label>',
+ "<input type='text' name='wpHourDiff' id='wpHourDiff' value=\"" . htmlspecialchars( $this->mHourDiff ) . "\" size='6' />"
+ ) . "<tr><td colspan='2'>
+ <input type='button' value=\"" . wfMsg( 'guesstimezone' ) ."\"
+ onclick='javascript:guessTimezone()' id='guesstimezonebutton' style='display:none;' />
+ </td></tr></table></fieldset>
+ <div class='prefsectiontip'>¹" . wfMsg( 'timezonetext' ) . "</div>
+ </fieldset>\n\n" );
+
+ # Editing
+ #
+ global $wgLivePreview, $wgUseRCPatrol;
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend>
+ <div>' .
+ wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) .
+ ' ' .
+ wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
+ "</div>" .
+ $this->getToggles( array(
+ 'editsection',
+ 'editsectiononrightclick',
+ 'editondblclick',
+ 'editwidth',
+ 'showtoolbar',
+ 'previewonfirst',
+ 'previewontop',
+ 'watchcreations',
+ 'watchdefault',
+ 'minordefault',
+ 'externaleditor',
+ 'externaldiff',
+ $wgLivePreview ? 'uselivepreview' : false,
+ $wgUser->isAllowed( 'patrol' ) && $wgUseRCPatrol ? 'autopatrol' : false,
+ 'forceeditsummary',
+ ) ) . '</fieldset>'
+ );
+ $this->mUsedToggles['autopatrol'] = true; # Don't show this up for users who can't; the handler below is dumb and doesn't know it
+
+ $wgOut->addHTML( '<fieldset><legend>' . htmlspecialchars(wfMsg('prefs-rc')) . '</legend>' .
+ wfInputLabel( wfMsg( 'recentchangescount' ),
+ 'wpRecent', 'wpRecent', 3, $this->mRecent ) .
+ $this->getToggles( array(
+ 'hideminor',
+ $wgRCShowWatchingUsers ? 'shownumberswatching' : false,
+ 'usenewrc' )
+ ) . '</fieldset>'
+ );
+
+ # Watchlist
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' );
+
+ $wgOut->addHTML( wfInputLabel( wfMsg( 'prefs-watchlist-days' ),
+ 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) );
+ $wgOut->addHTML( '<br /><br />' ); # Spacing
+ $wgOut->addHTML( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'extendwatchlist' ) ) );
+ $wgOut->addHTML( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ),
+ 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
+
+ $wgOut->addHTML( '</fieldset>' );
+
+ # Search
+ $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' .
+ $this->addRow(
+ wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ),
+ wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
+ ) .
+ $this->addRow(
+ wfLabel( wfMsg( 'contextlines' ), 'wpSearchLines' ),
+ wfInput( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) )
+ ) .
+ $this->addRow(
+ wfLabel( wfMsg( 'contextchars' ), 'wpSearchChars' ),
+ wfInput( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
+ ) .
+ "</table><fieldset><legend>" . wfMsg( 'defaultns' ) . "</legend>$ps</fieldset></fieldset>" );
+
+ # Misc
+ #
+ $wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
+ $wgOut->addHTML( wfInputLabel( wfMsg( 'stubthreshold' ),
+ 'wpStubs', 'wpStubs', 6, $this->mStubs ) );
+ $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
+ $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
+ $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
+ $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) );
+ $uopt = $wgUser->getOption("underline");
+ $s0 = $uopt == 0 ? ' selected="selected"' : '';
+ $s1 = $uopt == 1 ? ' selected="selected"' : '';
+ $s2 = $uopt == 2 ? ' selected="selected"' : '';
+ $wgOut->addHTML("
+<div class='toggle'><label for='wpOpunderline'>$msgUnderline</label>
+<select name='wpOpunderline' id='wpOpunderline'>
+<option value=\"0\"$s0>$msgUnderlinenever</option>
+<option value=\"1\"$s1>$msgUnderlinealways</option>
+<option value=\"2\"$s2>$msgUnderlinedefault</option>
+</select>
+</div>
+");
+ foreach ( $togs as $tname ) {
+ if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
+ $wgOut->addHTML( $this->getToggle( $tname ) );
+ }
+ }
+ $wgOut->addHTML( '</fieldset>' );
+
+ $token = $wgUser->editToken();
+ $wgOut->addHTML( "
+ <div id='prefsubmit'>
+ <div>
+ <input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . "\" accesskey=\"".
+ wfMsgHtml('accesskey-save')."\" title=\"".wfMsgHtml('tooltip-save')."\" />
+ <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" />
+ </div>
+
+ </div>
+
+ <input type='hidden' name='wpEditToken' value='{$token}' />
+ </div></form>\n" );
+
+ $wgOut->addWikiText( '<div class="prefcache">' . wfMsg('clearyourcache') . '</div>' );
+
+ }
+}
+?>
diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php
new file mode 100644
index 00000000..bbfc2782
--- /dev/null
+++ b/includes/SpecialPrefixindex.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+require_once 'SpecialAllpages.php';
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL)
+ * @param $specialPage SpecialPage object.
+ */
+function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
+ global $wgRequest, $wgOut, $wgContLang;
+
+ # GET values
+ $from = $wgRequest->getVal( 'from' );
+ $prefix = $wgRequest->getVal( 'prefix' );
+ $namespace = $wgRequest->getInt( 'namespace' );
+
+ $namespaces = $wgContLang->getNamespaces();
+
+ $indexPage = new SpecialPrefixIndex();
+
+ if( !in_array($namespace, array_keys($namespaces)) )
+ $namespace = 0;
+
+ $wgOut->setPagetitle( $namespace > 0 ?
+ wfMsg( 'allinnamespace', $namespaces[$namespace] ) :
+ wfMsg( 'allarticles' )
+ );
+
+
+
+ if ( isset($par) ) {
+ $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from );
+ } elseif ( isset($prefix) ) {
+ $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from );
+ } elseif ( isset($from) ) {
+ $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from );
+ } else {
+ $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null ));
+ }
+}
+
+class SpecialPrefixindex extends SpecialAllpages {
+ var $maxPerPage=960;
+ var $topLevelMax=50;
+ var $name='Prefixindex';
+ # Determines, which message describes the input field 'nsfrom', used in function namespaceForm (see superclass SpecialAllpages)
+ var $nsfromMsg='allpagesprefix';
+
+/**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ */
+function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) {
+ global $wgOut, $wgUser, $wgContLang;
+
+ $fname = 'indexShowChunk';
+
+ $sk = $wgUser->getSkin();
+
+ if (!isset($from)) $from = $prefix;
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+
+ if ( !$prefixList || !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } else {
+ list( $namespace, $prefixKey, $prefix ) = $prefixList;
+ list( $fromNs, $fromKey, $from ) = $fromList;
+
+ ### FIXME: should complain if $fromNs != $namespace
+
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+ 'page_title >= ' . $dbr->addQuotes( $fromKey ),
+ ),
+ $fname,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ ### FIXME: side link to previous
+
+ $n = 0;
+ $out = '<table style="background: inherit;" border="0" width="100%">';
+
+ $namespaces = $wgContLang->getFormattedNamespaces();
+ while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $t = Title::makeTitle( $s->page_namespace, $s->page_title );
+ if( $t ) {
+ $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+ $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+ ($s->page_is_redirect ? '</div>' : '' );
+ } else {
+ $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+ }
+ if( $n % 3 == 0 ) {
+ $out .= '<tr>';
+ }
+ $out .= "<td>$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $nsForm = $this->namespaceForm ( $namespace, $prefix );
+ $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+ $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
+ $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">' .
+ $sk->makeKnownLink( $wgContLang->specialPage( $this->name ),
+ wfMsg ( 'allpages' ) );
+ if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+ $out2 .= " | " . $sk->makeKnownLink(
+ $wgContLang->specialPage( $this->name ),
+ wfMsg ( 'nextpage', $s->page_title ),
+ "from=" . wfUrlEncode ( $s->page_title ) .
+ "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
+ }
+ $out2 .= "</td></tr></table><hr />";
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+}
+}
+
+?>
diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php
new file mode 100644
index 00000000..9d38abcb
--- /dev/null
+++ b/includes/SpecialRandompage.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Constructor
+ *
+ * @param $par The namespace to get a random page from (default NS_MAIN),
+ * used as e.g. Special:Randompage/Category
+ */
+function wfSpecialRandompage( $par = NS_MAIN ) {
+ global $wgOut, $wgExtraRandompageSQL, $wgContLang, $wgLang;
+ $fname = 'wfSpecialRandompage';
+
+ # Determine namespace
+ $t = Title::newFromText ( $par . ":Dummy" ) ;
+ $namespace = $t->getNamespace () ;
+
+ # NOTE! We use a literal constant in the SQL instead of the RAND()
+ # function because RAND() will return a different value for every row
+ # in the table. That's both very slow and returns results heavily
+ # biased towards low values, as rows later in the table will likely
+ # never be reached for comparison.
+ #
+ # Using a literal constant means the whole thing gets optimized on
+ # the index, and the comparison is both fast and fair.
+
+ # interpolation and sprintf() can muck up with locale-specific decimal separator
+ $randstr = wfRandom();
+
+ $db =& wfGetDB( DB_SLAVE );
+ $use_index = $db->useIndexClause( 'page_random' );
+ $page = $db->tableName( 'page' );
+
+ $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : '';
+ $sql = "SELECT page_id,page_title
+ FROM $page $use_index
+ WHERE page_namespace=$namespace AND page_is_redirect=0 $extra
+ AND page_random>$randstr
+ ORDER BY page_random";
+ $sql = $db->limitResult($sql, 1, 0);
+ $res = $db->query( $sql, $fname );
+
+ $title = null;
+ if( $s = $db->fetchObject( $res ) ) {
+ $title =& Title::makeTitle( $namespace, $s->page_title );
+ }
+ if( is_null( $title ) ) {
+ # That's not supposed to happen :)
+ $title = Title::newFromText( wfMsg( 'mainpage' ) );
+ }
+ $wgOut->reportTime(); # for logfile
+ $wgOut->redirect( $title->getFullUrl() );
+}
+
+?>
diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php
new file mode 100644
index 00000000..512553c0
--- /dev/null
+++ b/includes/SpecialRandomredirect.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Special page to direct the user to a random redirect page (minus the second redirect)
+ *
+ * @package MediaWiki
+ * @subpackage Special pages
+ * @author Rob Church <robchur@gmail.com>
+ * @licence GNU General Public Licence 2.0 or later
+ */
+
+/**
+ * Main execution point
+ * @param $par Namespace to select the redirect from
+ */
+function wfSpecialRandomredirect( $par = NULL ) {
+ global $wgOut, $wgExtraRandompageSQL, $wgContLang;
+ $fname = 'wfSpecialRandomredirect';
+
+ # Validate the namespace
+ $namespace = $wgContLang->getNsIndex( $par );
+ if( $namespace === false || $namespace < NS_MAIN )
+ $namespace = NS_MAIN;
+
+ # Same logic as RandomPage
+ $randstr = wfRandom();
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $use_index = $dbr->useIndexClause( 'page_random' );
+ $page = $dbr->tableName( 'page' );
+
+ $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : '';
+ $sql = "SELECT page_id,page_title
+ FROM $page $use_index
+ WHERE page_namespace = $namespace AND page_is_redirect = 1 $extra
+ AND page_random > $randstr
+ ORDER BY page_random";
+
+ $sql = $dbr->limitResult( $sql, 1, 0 );
+ $res = $dbr->query( $sql, $fname );
+
+ $title = NULL;
+ if( $row = $dbr->fetchObject( $res ) )
+ $title = Title::makeTitleSafe( $namespace, $row->page_title );
+
+ # Catch dud titles and return to the main page
+ if( is_null( $title ) )
+ $title = Title::newFromText( wfMsg( 'mainpage' ) );
+
+ $wgOut->reportTime();
+ $wgOut->redirect( $title->getFullUrl( 'redirect=no' ) );
+}
+
+?>
diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php
new file mode 100644
index 00000000..97f810d9
--- /dev/null
+++ b/includes/SpecialRecentchanges.php
@@ -0,0 +1,709 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'ChangesList.php' );
+
+/**
+ * Constructor
+ */
+function wfSpecialRecentchanges( $par, $specialPage ) {
+ global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol, $wgDBtype;
+ global $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
+ global $wgAllowCategorizedRecentChanges ;
+ $fname = 'wfSpecialRecentchanges';
+
+ # Get query parameters
+ $feedFormat = $wgRequest->getVal( 'feed' );
+
+ /* Checkbox values can't be true be default, because
+ * we cannot differentiate between unset and not set at all
+ */
+ $defaults = array(
+ /* int */ 'days' => $wgUser->getDefaultOption('rcdays'),
+ /* int */ 'limit' => $wgUser->getDefaultOption('rclimit'),
+ /* bool */ 'hideminor' => false,
+ /* bool */ 'hidebots' => true,
+ /* bool */ 'hideanons' => false,
+ /* bool */ 'hideliu' => false,
+ /* bool */ 'hidepatrolled' => false,
+ /* bool */ 'hidemyself' => false,
+ /* text */ 'from' => '',
+ /* text */ 'namespace' => null,
+ /* bool */ 'invert' => false,
+ /* bool */ 'categories_any' => false,
+ );
+
+ extract($defaults);
+
+
+ $days = $wgUser->getOption( 'rcdays' );
+ if ( !$days ) { $days = $defaults['days']; }
+ $days = $wgRequest->getInt( 'days', $days );
+
+ $limit = $wgUser->getOption( 'rclimit' );
+ if ( !$limit ) { $limit = $defaults['limit']; }
+
+ # list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' );
+ $limit = $wgRequest->getInt( 'limit', $limit );
+
+ /* order of selection: url > preferences > default */
+ $hideminor = $wgRequest->getBool( 'hideminor', $wgUser->getOption( 'hideminor') ? true : $defaults['hideminor'] );
+
+ # As a feed, use limited settings only
+ if( $feedFormat ) {
+ global $wgFeedLimit;
+ if( $limit > $wgFeedLimit ) {
+ $options['limit'] = $wgFeedLimit;
+ }
+
+ } else {
+
+ $namespace = $wgRequest->getIntOrNull( 'namespace' );
+ $invert = $wgRequest->getBool( 'invert', $defaults['invert'] );
+ $hidebots = $wgRequest->getBool( 'hidebots', $defaults['hidebots'] );
+ $hideanons = $wgRequest->getBool( 'hideanons', $defaults['hideanons'] );
+ $hideliu = $wgRequest->getBool( 'hideliu', $defaults['hideliu'] );
+ $hidepatrolled = $wgRequest->getBool( 'hidepatrolled', $defaults['hidepatrolled'] );
+ $hidemyself = $wgRequest->getBool ( 'hidemyself', $defaults['hidemyself'] );
+ $from = $wgRequest->getVal( 'from', $defaults['from'] );
+
+ # Get query parameters from path
+ if( $par ) {
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'hidebots' == $bit ) $hidebots = 1;
+ if ( 'bots' == $bit ) $hidebots = 0;
+ if ( 'hideminor' == $bit ) $hideminor = 1;
+ if ( 'minor' == $bit ) $hideminor = 0;
+ if ( 'hideliu' == $bit ) $hideliu = 1;
+ if ( 'hidepatrolled' == $bit ) $hidepatrolled = 1;
+ if ( 'hideanons' == $bit ) $hideanons = 1;
+ if ( 'hidemyself' == $bit ) $hidemyself = 1;
+
+ if ( is_numeric( $bit ) ) {
+ $limit = $bit;
+ }
+
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
+ $limit = $m[1];
+ }
+
+ if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
+ $days = $m[1];
+ }
+ }
+ }
+ }
+
+ if ( $limit < 0 || $limit > 5000 ) $limit = $defaults['limit'];
+
+
+ # Database connection and caching
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'recentchanges', 'watchlist' ) );
+
+
+ $cutoff_unixtime = time() - ( $days * 86400 );
+ $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
+ $cutoff = $dbr->timestamp( $cutoff_unixtime );
+ if(preg_match('/^[0-9]{14}$/', $from) and $from > wfTimestamp(TS_MW,$cutoff)) {
+ $cutoff = $dbr->timestamp($from);
+ } else {
+ $from = $defaults['from'];
+ }
+
+ # 10 seconds server-side caching max
+ $wgOut->setSquidMaxage( 10 );
+
+ # Get last modified date, for client caching
+ # Don't use this if we are using the patrol feature, patrol changes don't update the timestamp
+ $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, $fname );
+ if ( $feedFormat || !$wgUseRCPatrol ) {
+ if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
+ # Client cache fresh and headers sent, nothing more to do.
+ return;
+ }
+ }
+
+ # It makes no sense to hide both anons and logged-in users
+ # Where this occurs, force anons to be shown
+ if( $hideanons && $hideliu )
+ $hideanons = false;
+
+ # Form WHERE fragments for all the options
+ $hidem = $hideminor ? 'AND rc_minor = 0' : '';
+ $hidem .= $hidebots ? ' AND rc_bot = 0' : '';
+ $hidem .= $hideliu ? ' AND rc_user = 0' : '';
+ $hidem .= ( $wgUseRCPatrol && $hidepatrolled ) ? ' AND rc_patrolled = 0' : '';
+ $hidem .= $hideanons ? ' AND rc_user != 0' : '';
+
+ if( $hidemyself ) {
+ if( $wgUser->getID() ) {
+ $hidem .= ' AND rc_user != ' . $wgUser->getID();
+ } else {
+ $hidem .= ' AND rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() );
+ }
+ }
+
+ # Namespace filtering
+ $hidem .= is_null( $namespace ) ? '' : ' AND rc_namespace' . ($invert ? '!=' : '=') . $namespace;
+
+ // This is the big thing!
+
+ $uid = $wgUser->getID();
+
+ // Perform query
+ $forceclause = $dbr->useIndexClause("rc_timestamp");
+ $sql2 = "SELECT * FROM $recentchanges $forceclause".
+ ($uid ? "LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") .
+ "WHERE rc_timestamp >= '{$cutoff}' {$hidem} " .
+ "ORDER BY rc_timestamp DESC";
+ $sql2 = $dbr->limitResult($sql2, $limit, 0);
+ $res = $dbr->query( $sql2, $fname );
+
+ // Fetch results, prepare a batch link existence check query
+ $rows = array();
+ $batch = new LinkBatch;
+ while( $row = $dbr->fetchObject( $res ) ){
+ $rows[] = $row;
+ if ( !$feedFormat ) {
+ // User page link
+ $title = Title::makeTitleSafe( NS_USER, $row->rc_user_text );
+ $batch->addObj( $title );
+
+ // User talk
+ $title = Title::makeTitleSafe( NS_USER_TALK, $row->rc_user_text );
+ $batch->addObj( $title );
+ }
+
+ }
+ $dbr->freeResult( $res );
+
+ if( $feedFormat ) {
+ rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod );
+ } else {
+
+ # Web output...
+
+ // Run existence checks
+ $batch->execute();
+ $any = $wgRequest->getBool( 'categories_any', $defaults['categories_any']);
+
+ // Output header
+ if ( !$specialPage->including() ) {
+ $wgOut->addWikiText( wfMsgForContent( "recentchangestext" ) );
+
+ // Dump everything here
+ $nondefaults = array();
+
+ wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'limit', $limit , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideminor', $hideminor, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hidebots', $hidebots, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideanons', $hideanons, $defaults, $nondefaults );
+ wfAppendToArrayIfNotDefault( 'hideliu', $hideliu, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hidepatrolled', $hidepatrolled, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hidemyself', $hidemyself, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'from', $from, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'namespace', $namespace, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'invert', $invert, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'categories_any', $any, $defaults, $nondefaults);
+
+ // Add end of the texts
+ $wgOut->addHTML( '<div class="rcoptions">' . rcOptionsPanel( $defaults, $nondefaults ) . "\n" );
+ $wgOut->addHTML( rcNamespaceForm( $namespace, $invert, $nondefaults, $any ) . '</div>'."\n");
+ }
+
+ // And now for the content
+ $sk = $wgUser->getSkin();
+ $wgOut->setSyndicated( true );
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ if ( $wgAllowCategorizedRecentChanges ) {
+ $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+ $categories = str_replace ( "|" , "\n" , $categories ) ;
+ $categories = explode ( "\n" , $categories ) ;
+ rcFilterByCategories ( $rows , $categories , $any ) ;
+ }
+
+ $s = $list->beginRecentChangesList();
+ $counter = 1;
+ foreach( $rows as $obj ){
+ if( $limit == 0) {
+ break;
+ }
+
+ if ( ! ( $hideminor && $obj->rc_minor ) &&
+ ! ( $hidepatrolled && $obj->rc_patrolled ) ) {
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+
+ if ($wgShowUpdatedMarker
+ && !empty( $obj->wl_notificationtimestamp )
+ && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) {
+ $rc->notificationtimestamp = true;
+ } else {
+ $rc->notificationtimestamp = false;
+ }
+
+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" . $dbr->strencode($obj->rc_title) ."' AND wl_namespace=$obj->rc_namespace" ;
+ $res3 = $dbr->query( $sql3, 'wfSpecialRecentChanges');
+ $x = $dbr->fetchObject( $res3 );
+ $rc->numberofWatchingusers = $x->n;
+ } else {
+ $rc->numberofWatchingusers = 0;
+ }
+ $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
+ --$limit;
+ }
+ }
+ $s .= $list->endRecentChangesList();
+ $wgOut->addHTML( $s );
+ }
+}
+
+function rcFilterByCategories ( &$rows , $categories , $any ) {
+ require_once ( 'Categoryfinder.php' ) ;
+
+ # Filter categories
+ $cats = array () ;
+ foreach ( $categories AS $cat ) {
+ $cat = trim ( $cat ) ;
+ if ( $cat == "" ) continue ;
+ $cats[] = $cat ;
+ }
+
+ # Filter articles
+ $articles = array () ;
+ $a2r = array () ;
+ foreach ( $rows AS $k => $r ) {
+ $nt = Title::newFromText ( $r->rc_title , $r->rc_namespace ) ;
+ $id = $nt->getArticleID() ;
+ if ( $id == 0 ) continue ; # Page might have been deleted...
+ if ( !in_array ( $id , $articles ) ) {
+ $articles[] = $id ;
+ }
+ if ( !isset ( $a2r[$id] ) ) {
+ $a2r[$id] = array() ;
+ }
+ $a2r[$id][] = $k ;
+ }
+
+ # Shortcut?
+ if ( count ( $articles ) == 0 OR count ( $cats ) == 0 )
+ return ;
+
+ # Look up
+ $c = new Categoryfinder ;
+ $c->seed ( $articles , $cats , $any ? "OR" : "AND" ) ;
+ $match = $c->run () ;
+
+ # Filter
+ $newrows = array () ;
+ foreach ( $match AS $id ) {
+ foreach ( $a2r[$id] AS $rev ) {
+ $k = $rev ;
+ $newrows[$k] = $rows[$k] ;
+ }
+ }
+ $rows = $newrows ;
+}
+
+function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
+ global $messageMemc, $wgDBname, $wgFeedCacheTimeout;
+ global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
+
+ if( !isset( $wgFeedClasses[$feedFormat] ) ) {
+ wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
+ return false;
+ }
+
+ $timekey = "$wgDBname:rcfeed:$feedFormat:timestamp";
+ $key = "$wgDBname:rcfeed:$feedFormat:limit:$limit:minor:$hideminor";
+
+ $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchanges' ) .
+ ' [' . $wgContLanguageCode . ']';
+ $feed = new $wgFeedClasses[$feedFormat](
+ $feedTitle,
+ htmlspecialchars( wfMsgForContent( 'recentchangestext' ) ),
+ $wgTitle->getFullUrl() );
+
+ /**
+ * Bumping around loading up diffs can be pretty slow, so where
+ * possible we want to cache the feed output so the next visitor
+ * gets it quick too.
+ */
+ $cachedFeed = false;
+ if( ( $wgFeedCacheTimeout > 0 ) && ( $feedLastmod = $messageMemc->get( $timekey ) ) ) {
+ /**
+ * If the cached feed was rendered very recently, we may
+ * go ahead and use it even if there have been edits made
+ * since it was rendered. This keeps a swarm of requests
+ * from being too bad on a super-frequently edited wiki.
+ */
+ if( time() - wfTimestamp( TS_UNIX, $feedLastmod )
+ < $wgFeedCacheTimeout
+ || wfTimestamp( TS_UNIX, $feedLastmod )
+ > wfTimestamp( TS_UNIX, $lastmod ) ) {
+ wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
+ $cachedFeed = $messageMemc->get( $key );
+ } else {
+ wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
+ }
+ }
+ if( is_string( $cachedFeed ) ) {
+ wfDebug( "RC: Outputting cached feed\n" );
+ $feed->httpHeaders();
+ echo $cachedFeed;
+ } else {
+ wfDebug( "RC: rendering new feed and caching it\n" );
+ ob_start();
+ rcDoOutputFeed( $rows, $feed );
+ $cachedFeed = ob_get_contents();
+ ob_end_flush();
+
+ $expire = 3600 * 24; # One day
+ $messageMemc->set( $key, $cachedFeed );
+ $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
+ }
+ return true;
+}
+
+function rcDoOutputFeed( $rows, &$feed ) {
+ $fname = 'rcDoOutputFeed';
+ wfProfileIn( $fname );
+
+ $feed->outHeader();
+
+ # Merge adjacent edits by one user
+ $sorted = array();
+ $n = 0;
+ foreach( $rows as $obj ) {
+ if( $n > 0 &&
+ $obj->rc_namespace >= 0 &&
+ $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id &&
+ $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) {
+ $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid;
+ } else {
+ $sorted[$n] = $obj;
+ $n++;
+ }
+ $first = false;
+ }
+
+ foreach( $sorted as $obj ) {
+ $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
+ $talkpage = $title->getTalkPage();
+ $item = new FeedItem(
+ $title->getPrefixedText(),
+ rcFormatDiff( $obj ),
+ $title->getFullURL(),
+ $obj->rc_timestamp,
+ $obj->rc_user_text,
+ $talkpage->getFullURL()
+ );
+ $feed->outItem( $item );
+ }
+ $feed->outFooter();
+ wfProfileOut( $fname );
+}
+
+/**
+ *
+ */
+function rcCountLink( $lim, $d, $page='Recentchanges', $more='' ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
+ ($lim ? $wgLang->formatNum( "{$lim}" ) : wfMsg( 'recentchangesall' ) ), "{$more}" .
+ ($d ? "days={$d}&" : '') . 'limit='.$lim );
+ return $s;
+}
+
+/**
+ *
+ */
+function rcDaysLink( $lim, $d, $page='Recentchanges', $more='' ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
+ ($d ? $wgLang->formatNum( "{$d}" ) : wfMsg( 'recentchangesall' ) ), $more.'days='.$d .
+ ($lim ? '&limit='.$lim : '') );
+ return $s;
+}
+
+/**
+ * Used by Recentchangeslinked
+ */
+function rcDayLimitLinks( $days, $limit, $page='Recentchanges', $more='', $doall = false, $minorLink = '',
+ $botLink = '', $liuLink = '', $patrLink = '', $myselfLink = '' ) {
+ if ($more != '') $more .= '&';
+ $cl = rcCountLink( 50, $days, $page, $more ) . ' | ' .
+ rcCountLink( 100, $days, $page, $more ) . ' | ' .
+ rcCountLink( 250, $days, $page, $more ) . ' | ' .
+ rcCountLink( 500, $days, $page, $more ) .
+ ( $doall ? ( ' | ' . rcCountLink( 0, $days, $page, $more ) ) : '' );
+ $dl = rcDaysLink( $limit, 1, $page, $more ) . ' | ' .
+ rcDaysLink( $limit, 3, $page, $more ) . ' | ' .
+ rcDaysLink( $limit, 7, $page, $more ) . ' | ' .
+ rcDaysLink( $limit, 14, $page, $more ) . ' | ' .
+ rcDaysLink( $limit, 30, $page, $more ) .
+ ( $doall ? ( ' | ' . rcDaysLink( $limit, 0, $page, $more ) ) : '' );
+
+ $linkParts = array( 'minorLink' => 'minor', 'botLink' => 'bots', 'liuLink' => 'liu', 'patrLink' => 'patr', 'myselfLink' => 'mine' );
+ foreach( $linkParts as $linkVar => $linkMsg ) {
+ if( $$linkVar != '' )
+ $links[] = wfMsgHtml( 'rcshowhide' . $linkMsg, $$linkVar );
+ }
+
+ $shm = implode( ' | ', $links );
+ $note = wfMsg( 'rclinks', $cl, $dl, $shm );
+ return $note;
+}
+
+
+/**
+ * Makes change an option link which carries all the other options
+ * @param $title @see Title
+ * @param $override
+ * @param $options
+ */
+function makeOptionsLink( $title, $override, $options ) {
+ global $wgUser, $wgContLang;
+ $sk = $wgUser->getSkin();
+ return $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchanges' ),
+ $title, wfArrayToCGI( $override, $options ) );
+}
+
+/**
+ * Creates the options panel.
+ * @param $defaults
+ * @param $nondefaults
+ */
+function rcOptionsPanel( $defaults, $nondefaults ) {
+ global $wgLang, $wgUseRCPatrol;
+
+ $options = $nondefaults + $defaults;
+
+ if( $options['from'] )
+ $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
+ $wgLang->formatNum( $options['limit'] ),
+ $wgLang->timeanddate( $options['from'], true ) );
+ else
+ $note = wfMsgExt( 'rcnote', array( 'parseinline' ),
+ $wgLang->formatNum( $options['limit'] ),
+ $wgLang->formatNum( $options['days'] ),
+ $wgLang->timeAndDate( wfTimestampNow(), true ) );
+
+ // limit links
+ $options_limit = array(50, 100, 250, 500);
+ foreach( $options_limit as $value ) {
+ $cl[] = makeOptionsLink( $wgLang->formatNum( $value ),
+ array( 'limit' => $value ), $nondefaults) ;
+ }
+ $cl = implode( ' | ', $cl);
+
+ // day links, reset 'from' to none
+ $options_days = array(1, 3, 7, 14, 30);
+ foreach( $options_days as $value ) {
+ $dl[] = makeOptionsLink( $wgLang->formatNum( $value ),
+ array( 'days' => $value, 'from' => '' ), $nondefaults) ;
+ }
+ $dl = implode( ' | ', $dl);
+
+
+ // show/hide links
+ $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ));
+ $minorLink = makeOptionsLink( $showhide[1-$options['hideminor']],
+ array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults);
+ $botLink = makeOptionsLink( $showhide[1-$options['hidebots']],
+ array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults);
+ $anonsLink = makeOptionsLink( $showhide[ 1 - $options['hideanons'] ],
+ array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults );
+ $liuLink = makeOptionsLink( $showhide[1-$options['hideliu']],
+ array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults);
+ $patrLink = makeOptionsLink( $showhide[1-$options['hidepatrolled']],
+ array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults);
+ $myselfLink = makeOptionsLink( $showhide[1-$options['hidemyself']],
+ array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults);
+
+ $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink );
+ $links[] = wfMsgHtml( 'rcshowhidebots', $botLink );
+ $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink );
+ $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink );
+ if( $wgUseRCPatrol )
+ $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink );
+ $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink );
+ $hl = implode( ' | ', $links );
+
+ // show from this onward link
+ $now = $wgLang->timeanddate( wfTimestampNow(), true );
+ $tl = makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
+
+ $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
+ $cl, $dl, $hl );
+ $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
+ return "$note<br />$rclinks<br />$rclistfrom";
+
+}
+
+/**
+ * Creates the choose namespace selection
+ *
+ * @private
+ *
+ * @param $namespace Mixed: the key of the currently selected namespace, empty string
+ * if there is none
+ * @param $invert Bool: whether to invert the namespace selection
+ * @param $nondefaults Array: an array of non default options to be remembered
+ * @param $categories_any Bool: Default value for the checkbox
+ *
+ * @return string
+ */
+function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) {
+ global $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest;
+ $t = Title::makeTitle( NS_SPECIAL, 'Recentchanges' );
+
+ $namespaceselect = HTMLnamespaceselector($namespace, '');
+ $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . "\" />\n";
+ $invertbox = "<input type='checkbox' name='invert' value='1' id='nsinvert'" . ( $invert ? ' checked="checked"' : '' ) . ' />';
+
+ if ( $wgAllowCategorizedRecentChanges ) {
+ $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+ $cb_arr = array( 'type' => 'checkbox', 'name' => 'categories_any', 'value' => "1" ) ;
+ if ( $categories_any ) $cb_arr['checked'] = "checked" ;
+ $catbox = "<br />" ;
+ $catbox .= wfMsgExt('rc_categories', array('parseinline')) . " ";
+ $catbox .= wfElement('input', array( 'type' => 'text', 'name' => 'categories', 'value' => $categories));
+ $catbox .= " &nbsp;" ;
+ $catbox .= wfElement('input', $cb_arr );
+ $catbox .= wfMsgExt('rc_categories_any', array('parseinline'));
+ } else {
+ $catbox = "" ;
+ }
+
+ $out = "<div class='namespacesettings'><form method='get' action='{$wgScript}'>\n";
+
+ foreach ( $nondefaults as $key => $value ) {
+ if ($key != 'namespace' && $key != 'invert')
+ $out .= wfElement('input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value));
+ }
+
+ $out .= '<input type="hidden" name="title" value="'.$t->getPrefixedText().'" />';
+ $out .= "
+<div id='nsselect' class='recentchanges'>
+ <label for='namespace'>" . wfMsgHtml('namespace') . "</label>
+ {$namespaceselect}{$submitbutton}{$invertbox} <label for='nsinvert'>" . wfMsgHtml('invert') . "</label>{$catbox}\n</div>";
+ $out .= '</form></div>';
+ return $out;
+}
+
+
+/**
+ * Format a diff for the newsfeed
+ */
+function rcFormatDiff( $row ) {
+ $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+ return rcFormatDiffRow( $titleObj,
+ $row->rc_last_oldid, $row->rc_this_oldid,
+ $row->rc_timestamp,
+ $row->rc_comment );
+}
+
+function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) {
+ global $wgFeedDiffCutoff, $wgContLang, $wgUser;
+ $fname = 'rcFormatDiff';
+ wfProfileIn( $fname );
+
+ require_once( 'DifferenceEngine.php' );
+ $skin = $wgUser->getSkin();
+ $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
+
+ if( $title->getNamespace() >= 0 ) {
+ if( $oldid ) {
+ wfProfileIn( "$fname-dodiff" );
+
+ $de = new DifferenceEngine( $title, $oldid, $newid );
+ #$diffText = $de->getDiff( wfMsg( 'revisionasof',
+ # $wgContLang->timeanddate( $timestamp ) ),
+ # wfMsg( 'currentrev' ) );
+ $diffText = $de->getDiff(
+ wfMsg( 'previousrevision' ), // hack
+ wfMsg( 'revisionasof',
+ $wgContLang->timeanddate( $timestamp ) ) );
+
+
+ if ( strlen( $diffText ) > $wgFeedDiffCutoff ) {
+ // Omit large diffs
+ $diffLink = $title->escapeFullUrl(
+ 'diff=' . $newid .
+ '&oldid=' . $oldid );
+ $diffText = '<a href="' .
+ $diffLink .
+ '">' .
+ htmlspecialchars( wfMsgForContent( 'difference' ) ) .
+ '</a>';
+ } elseif ( $diffText === false ) {
+ // Error in diff engine, probably a missing revision
+ $diffText = "<p>Can't load revision $newid</p>";
+ } else {
+ // Diff output fine, clean up any illegal UTF-8
+ $diffText = UtfNormal::cleanUp( $diffText );
+ $diffText = rcApplyDiffStyle( $diffText );
+ }
+ wfProfileOut( "$fname-dodiff" );
+ } else {
+ $rev = Revision::newFromId( $newid );
+ if( is_null( $rev ) ) {
+ $newtext = '';
+ } else {
+ $newtext = $rev->getText();
+ }
+ $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' .
+ '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
+ }
+ $completeText .= $diffText;
+ }
+
+ wfProfileOut( $fname );
+ return $completeText;
+}
+
+/**
+ * Hacky application of diff styles for the feeds.
+ * Might be 'cleaner' to use DOM or XSLT or something,
+ * but *gack* it's a pain in the ass.
+ *
+ * @param $text String:
+ * @return string
+ * @private
+ */
+function rcApplyDiffStyle( $text ) {
+ $styles = array(
+ 'diff' => 'background-color: white;',
+ 'diff-otitle' => 'background-color: white;',
+ 'diff-ntitle' => 'background-color: white;',
+ 'diff-addedline' => 'background: #cfc; font-size: smaller;',
+ 'diff-deletedline' => 'background: #ffa; font-size: smaller;',
+ 'diff-context' => 'background: #eee; font-size: smaller;',
+ 'diffchange' => 'color: red; font-weight: bold;',
+ );
+
+ foreach( $styles as $class => $style ) {
+ $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/",
+ "\\1style=\"$style\"\\3", $text );
+ }
+
+ return $text;
+}
+
+?>
diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php
new file mode 100644
index 00000000..2a611c4d
--- /dev/null
+++ b/includes/SpecialRecentchangeslinked.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * This is to display changes made to all articles linked in an article.
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'SpecialRecentchanges.php' );
+
+/**
+ * Entrypoint
+ * @param string $par parent page we will look at
+ */
+function wfSpecialRecentchangeslinked( $par = NULL ) {
+ global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest;
+ $fname = 'wfSpecialRecentchangeslinked';
+
+ $days = $wgRequest->getInt( 'days' );
+ $target = isset($par) ? $par : $wgRequest->getText( 'target' );
+ $hideminor = $wgRequest->getBool( 'hideminor' ) ? 1 : 0;
+
+ $wgOut->setPagetitle( wfMsg( 'recentchangeslinked' ) );
+ $sk = $wgUser->getSkin();
+
+ # Validate the title
+ $nt = Title::newFromURL( $target );
+ if( !is_object( $nt ) ) {
+ $wgOut->errorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+
+ # Check for existence
+ # Do a quiet redirect back to the page itself if it doesn't
+ if( !$nt->exists() ) {
+ $wgOut->redirect( $nt->getLocalUrl() );
+ return;
+ }
+
+ $id = $nt->getArticleId();
+
+ $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) );
+
+ if ( ! $days ) {
+ $days = $wgUser->getOption( 'rcdays' );
+ if ( ! $days ) { $days = 7; }
+ }
+ $days = (int)$days;
+ list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) );
+
+ $hideminor = ($hideminor ? 1 : 0);
+ if ( $hideminor ) {
+ $mlink = $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchangeslinked' ),
+ wfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) .
+ "&days={$days}&limit={$limit}&hideminor=0" );
+ } else {
+ $mlink = $sk->makeKnownLink( $wgContLang->specialPage( "Recentchangeslinked" ),
+ wfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) .
+ "&days={$days}&limit={$limit}&hideminor=1" );
+ }
+ if ( $hideminor ) {
+ $cmq = 'AND rc_minor=0';
+ } else { $cmq = ''; }
+
+ extract( $dbr->tableNames( 'recentchanges', 'categorylinks', 'pagelinks', 'revision', 'page' , "watchlist" ) );
+
+ $uid = $wgUser->getID();
+
+ // If target is a Category, use categorylinks and invert from and to
+ if( $nt->getNamespace() == NS_CATEGORY ) {
+ $catkey = $dbr->addQuotes( $nt->getDBKey() );
+ $sql = "SELECT /* wfSpecialRecentchangeslinked */
+ rc_id,
+ rc_cur_id,
+ rc_namespace,
+ rc_title,
+ rc_this_oldid,
+ rc_last_oldid,
+ rc_user,
+ rc_comment,
+ rc_user_text,
+ rc_timestamp,
+ rc_minor,
+ rc_bot,
+ rc_new,
+ rc_patrolled,
+ rc_type
+" . ($uid ? ",wl_user" : "") . "
+ FROM $categorylinks, $recentchanges
+" . ($uid ? "LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . "
+ WHERE rc_timestamp > '{$cutoff}'
+ {$cmq}
+ AND cl_from=rc_cur_id
+ AND cl_to=$catkey
+ GROUP BY rc_cur_id,rc_namespace,rc_title,
+ rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,
+ rc_new
+ ORDER BY rc_timestamp DESC
+ LIMIT {$limit};
+ ";
+ } else {
+ $sql =
+"SELECT /* wfSpecialRecentchangeslinked */
+ rc_id,
+ rc_cur_id,
+ rc_namespace,
+ rc_title,
+ rc_user,
+ rc_comment,
+ rc_user_text,
+ rc_this_oldid,
+ rc_last_oldid,
+ rc_timestamp,
+ rc_minor,
+ rc_bot,
+ rc_new,
+ rc_patrolled,
+ rc_type
+" . ($uid ? ",wl_user" : "") . "
+ FROM $pagelinks, $recentchanges
+" . ($uid ? " LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . "
+ WHERE rc_timestamp > '{$cutoff}'
+ {$cmq}
+ AND pl_namespace=rc_namespace
+ AND pl_title=rc_title
+ AND pl_from=$id
+GROUP BY rc_cur_id,rc_namespace,rc_title,
+ rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,
+ rc_new
+ORDER BY rc_timestamp DESC
+ LIMIT {$limit}";
+ }
+ $res = $dbr->query( $sql, $fname );
+
+ $wgOut->addHTML("&lt; ".$sk->makeKnownLinkObj($nt, "", "redirect=no" )."<br />\n");
+ $note = wfMsg( "rcnote", $limit, $days, $wgLang->timeAndDate( wfTimestampNow(), true ) );
+ $wgOut->addHTML( "<hr />\n{$note}\n<br />" );
+
+ $note = rcDayLimitlinks( $days, $limit, "Recentchangeslinked",
+ "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}",
+ false, $mlink );
+
+ $wgOut->addHTML( $note."\n" );
+
+ $list = ChangesList::newFromUser( $wgUser );
+ $s = $list->beginRecentChangesList();
+ $count = $dbr->numRows( $res );
+
+ $counter = 1;
+ while ( $limit ) {
+ if ( 0 == $count ) { break; }
+ $obj = $dbr->fetchObject( $res );
+ --$count;
+# print_r ( $obj ) ;
+# print "<br/>\n" ;
+
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+ $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) );
+ --$limit;
+ }
+ $s .= $list->endRecentChangesList();
+
+ $dbr->freeResult( $res );
+ $wgOut->addHTML( $s );
+}
+
+?>
diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php
new file mode 100644
index 00000000..7fa8bbb4
--- /dev/null
+++ b/includes/SpecialRevisiondelete.php
@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * Not quite ready for production use yet; need to fix up the restricted mode,
+ * and provide for preservation across delete/undelete of the page.
+ *
+ * To try this out, set up extra permissions something like:
+ * $wgGroupPermissions['sysop']['deleterevision'] = true;
+ * $wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+ */
+
+function wfSpecialRevisiondelete( $par = null ) {
+ global $wgOut, $wgRequest, $wgUser;
+
+ $target = $wgRequest->getVal( 'target' );
+ $oldid = $wgRequest->getInt( 'oldid' );
+
+ $sk = $wgUser->getSkin();
+ $page = Title::newFromUrl( $target );
+
+ if( is_null( $page ) ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+
+ $form = new RevisionDeleteForm( $wgRequest );
+ if( $wgRequest->wasPosted() ) {
+ $form->submit( $wgRequest );
+ } else {
+ $form->show( $wgRequest );
+ }
+}
+
+class RevisionDeleteForm {
+ /**
+ * @param Title $page
+ * @param int $oldid
+ */
+ function __construct( $request ) {
+ global $wgUser;
+
+ $target = $request->getVal( 'target' );
+ $this->page = Title::newFromUrl( $target );
+
+ $this->revisions = $request->getIntArray( 'oldid', array() );
+
+ $this->skin = $wgUser->getSkin();
+ $this->checks = array(
+ array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT ),
+ array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
+ array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ),
+ array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED ) );
+ }
+
+ /**
+ * @param WebRequest $request
+ */
+ function show( $request ) {
+ global $wgOut, $wgUser;
+
+ $first = $this->revisions[0];
+
+ $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) );
+
+ $wgOut->addHtml( "<ul>" );
+ foreach( $this->revisions as $revid ) {
+ $rev = Revision::newFromTitle( $this->page, $revid );
+ $wgOut->addHtml( $this->historyLine( $rev ) );
+ $bitfields[] = $rev->mDeleted; // FIXME
+ }
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiText( wfMsg( 'revdelete-text' ) );
+
+ $items = array(
+ wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+ wfSubmitButton( wfMsg( 'revdelete-submit' ) ) );
+ $hidden = array(
+ wfHidden( 'wpEditToken', $wgUser->editToken() ),
+ wfHidden( 'target', $this->page->getPrefixedText() ) );
+ foreach( $this->revisions as $revid ) {
+ $hidden[] = wfHidden( 'oldid[]', $revid );
+ }
+
+ $special = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' );
+ $wgOut->addHtml( wfElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $special->getLocalUrl( 'action=submit' ) ) ) );
+
+ $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ $wgOut->addHtml( '<div>' .
+ wfCheckLabel( wfMsg( $message), $name, $name, $rev->isDeleted( $field ) ) .
+ '</div>' );
+ }
+ $wgOut->addHtml( '</fieldset>' );
+ foreach( $items as $item ) {
+ $wgOut->addHtml( '<p>' . $item . '</p>' );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+
+ $wgOut->addHtml( '</form>' );
+ }
+
+ /**
+ * @param Revision $rev
+ * @returns string
+ */
+ function historyLine( $rev ) {
+ global $wgContLang;
+ $date = $wgContLang->timeanddate( $rev->getTimestamp() );
+ return
+ "<li>" .
+ $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ) .
+ " " .
+ $this->skin->revUserLink( $rev ) .
+ " " .
+ $this->skin->revComment( $rev ) .
+ "</li>";
+ }
+
+ /**
+ * @param WebRequest $request
+ */
+ function submit( $request ) {
+ $bitfield = $this->extractBitfield( $request );
+ $comment = $request->getText( 'wpReason' );
+ if( $this->save( $bitfield, $comment ) ) {
+ return $this->success( $request );
+ } else {
+ return $this->show( $request );
+ }
+ }
+
+ function success( $request ) {
+ global $wgOut;
+ $wgOut->addWikiText( 'woo' );
+ }
+
+ /**
+ * Put together a rev_deleted bitfield from the submitted checkboxes
+ * @param WebRequest $request
+ * @return int
+ */
+ function extractBitfield( $request ) {
+ $bitfield = 0;
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ if( $request->getCheck( $name ) ) {
+ $bitfield |= $field;
+ }
+ }
+ return $bitfield;
+ }
+
+ function save( $bitfield, $reason ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $deleter = new RevisionDeleter( $dbw );
+ $ok = $deleter->setVisibility( $this->revisions, $bitfield, $reason );
+ }
+}
+
+
+class RevisionDeleter {
+ function __construct( $db ) {
+ $this->db = $db;
+ }
+
+ /**
+ * @param array $items list of revision ID numbers
+ * @param int $bitfield new rev_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setVisibility( $items, $bitfield, $comment ) {
+ $pages = array();
+
+ // To work!
+ foreach( $items as $revid ) {
+ $rev = Revision::newFromId( $revid );
+ $this->updateRevision( $rev, $bitfield );
+ $this->updateRecentChanges( $rev, $bitfield );
+
+ // For logging, maintain a count of revisions per page
+ $pageid = $rev->getPage();
+ if( isset( $pages[$pageid] ) ) {
+ $pages[$pageid]++;
+ } else {
+ $pages[$pageid] = 1;
+ }
+ }
+
+ // Clear caches...
+ foreach( $pages as $pageid => $count ) {
+ $title = Title::newFromId( $pageid );
+ $this->updatePage( $title );
+ $this->updateLog( $title, $count, $bitfield, $comment );
+ }
+
+ return true;
+ }
+
+ /**
+ * Update the revision's rev_deleted field
+ * @param Revision $rev
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateRevision( $rev, $bitfield ) {
+ $this->db->update( 'revision',
+ array( 'rev_deleted' => $bitfield ),
+ array( 'rev_id' => $rev->getId() ),
+ 'RevisionDeleter::updateRevision' );
+ }
+
+ /**
+ * Update the revision's recentchanges record if fields have been hidden
+ * @param Revision $rev
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateRecentChanges( $rev, $bitfield ) {
+ $this->db->update( 'recentchanges',
+ array(
+ 'rc_user' => ($bitfield & Revision::DELETED_USER) ? 0 : $rev->getUser(),
+ 'rc_user_text' => ($bitfield & Revision::DELETED_USER) ? wfMsg( 'rev-deleted-user' ) : $rev->getUserText(),
+ 'rc_comment' => ($bitfield & Revision::DELETED_COMMENT) ? wfMsg( 'rev-deleted-comment' ) : $rev->getComment() ),
+ array(
+ 'rc_this_oldid' => $rev->getId() ),
+ 'RevisionDeleter::updateRecentChanges' );
+ }
+
+ /**
+ * Touch the page's cache invalidation timestamp; this forces cached
+ * history views to refresh, so any newly hidden or shown fields will
+ * update properly.
+ * @param Title $title
+ */
+ function updatePage( $title ) {
+ $title->invalidateCache();
+ }
+
+ /**
+ * Record a log entry on the action
+ * @param Title $title
+ * @param int $count the number of revisions altered for this page
+ * @param int $bitfield the new rev_deleted value
+ * @param string $comment
+ */
+ function updateLog( $title, $count, $bitfield, $comment ) {
+ $log = new LogPage( 'delete' );
+ $reason = "changed $count revisions to $bitfield";
+ $reason .= ": $comment";
+ $log->addEntry( 'revision', $title, $reason );
+ }
+}
+
+?>
diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php
new file mode 100644
index 00000000..4db27e87
--- /dev/null
+++ b/includes/SpecialSearch.php
@@ -0,0 +1,413 @@
+<?php
+# 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
+
+/**
+ * Run text & title search and display the output
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Entry point
+ *
+ * @param $par String: (default '')
+ */
+function wfSpecialSearch( $par = '' ) {
+ global $wgRequest, $wgUser;
+
+ $search = $wgRequest->getText( 'search', $par );
+ $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+ if( $wgRequest->getVal( 'fulltext' ) ||
+ !is_null( $wgRequest->getVal( 'offset' ) ) ||
+ !is_null ($wgRequest->getVal( 'searchx' ) ) ) {
+ $searchPage->showResults( $search );
+ } else {
+ $searchPage->goResult( $search );
+ }
+}
+
+/**
+ * @todo document
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class SpecialSearch {
+
+ /**
+ * Set up basic search parameters from the request and user settings.
+ * Typically you'll pass $wgRequest and $wgUser.
+ *
+ * @param WebRequest $request
+ * @param User $user
+ * @public
+ */
+ function SpecialSearch( &$request, &$user ) {
+ list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+
+ if( $request->getCheck( 'searchx' ) ) {
+ $this->namespaces = $this->powerSearch( $request );
+ } else {
+ $this->namespaces = $this->userNamespaces( $user );
+ }
+
+ $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
+ }
+
+ /**
+ * If an exact title match can be found, jump straight ahead to
+ * @param string $term
+ * @public
+ */
+ function goResult( $term ) {
+ global $wgOut;
+ global $wgGoToEdit;
+
+ $this->setupPage( $term );
+
+ # Try to go to page as entered.
+ #
+ $t = Title::newFromText( $term );
+
+ # If the string cannot be used to create a title
+ if( is_null( $t ) ){
+ return $this->showResults( $term );
+ }
+
+ # If there's an exact or very near match, jump right there.
+ $t = SearchEngine::getNearMatch( $term );
+ if( !is_null( $t ) ) {
+ $wgOut->redirect( $t->getFullURL() );
+ return;
+ }
+
+ # No match, generate an edit URL
+ $t = Title::newFromText( $term );
+ if( is_null( $t ) ) {
+ $editurl = ''; # hrm...
+ } else {
+ wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
+ # If the feature is enabled, go straight to the edit page
+ if ( $wgGoToEdit ) {
+ $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
+ return;
+ } else {
+ $editurl = $t->escapeLocalURL( 'action=edit' );
+ }
+ }
+ $wgOut->addWikiText( wfMsg( 'noexactmatch', $term ) );
+
+ return $this->showResults( $term );
+ }
+
+ /**
+ * @param string $term
+ * @public
+ */
+ function showResults( $term ) {
+ $fname = 'SpecialSearch::showResults';
+ wfProfileIn( $fname );
+
+ $this->setupPage( $term );
+
+ global $wgUser, $wgOut;
+ $sk = $wgUser->getSkin();
+ $wgOut->addWikiText( wfMsg( 'searchresulttext' ) );
+
+ #if ( !$this->parseQuery() ) {
+ if( '' === trim( $term ) ) {
+ $wgOut->setSubtitle( '' );
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ wfProfileOut( $fname );
+ return;
+ }
+
+ global $wgDisableTextSearch;
+ if ( $wgDisableTextSearch ) {
+ global $wgForwardSearchUrl;
+ if( $wgForwardSearchUrl ) {
+ $url = str_replace( '$1', urlencode( $term ), $wgForwardSearchUrl );
+ $wgOut->redirect( $url );
+ return;
+ }
+ global $wgInputEncoding;
+ $wgOut->addHTML( wfMsg( 'searchdisabled' ) );
+ $wgOut->addHTML(
+ wfMsg( 'googlesearch',
+ htmlspecialchars( $term ),
+ htmlspecialchars( $wgInputEncoding ),
+ htmlspecialchars( wfMsg( 'search' ) )
+ )
+ );
+ wfProfileOut( $fname );
+ return;
+ }
+
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $this->limit, $this->offset );
+ $search->setNamespaces( $this->namespaces );
+ $search->showRedirects = $this->searchRedirects;
+ $titleMatches = $search->searchTitle( $term );
+ $textMatches = $search->searchText( $term );
+
+ $num = ( $titleMatches ? $titleMatches->numRows() : 0 )
+ + ( $textMatches ? $textMatches->numRows() : 0);
+ if ( $num >= $this->limit ) {
+ $top = wfShowingResults( $this->offset, $this->limit );
+ } else {
+ $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
+ }
+ $wgOut->addHTML( "<p>{$top}</p>\n" );
+
+ if( $num || $this->offset ) {
+ $prevnext = wfViewPrevNext( $this->offset, $this->limit,
+ 'Special:Search',
+ wfArrayToCGI(
+ $this->powerSearchOptions(),
+ array( 'search' => $term ) ) );
+ $wgOut->addHTML( "<br />{$prevnext}\n" );
+ }
+
+ if( $titleMatches ) {
+ if( $titleMatches->numRows() ) {
+ $wgOut->addWikiText( '==' . wfMsg( 'titlematches' ) . "==\n" );
+ $wgOut->addHTML( $this->showMatches( $titleMatches ) );
+ } else {
+ $wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" );
+ }
+ }
+
+ if( $textMatches ) {
+ if( $textMatches->numRows() ) {
+ $wgOut->addWikiText( '==' . wfMsg( 'textmatches' ) . "==\n" );
+ $wgOut->addHTML( $this->showMatches( $textMatches ) );
+ } elseif( $num == 0 ) {
+ # Don't show the 'no text matches' if we received title matches
+ $wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" );
+ }
+ }
+
+ if ( $num == 0 ) {
+ $wgOut->addWikiText( wfMsg( 'nonefound' ) );
+ }
+ if( $num || $this->offset ) {
+ $wgOut->addHTML( "<p>{$prevnext}</p>\n" );
+ }
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ wfProfileOut( $fname );
+ }
+
+ #------------------------------------------------------------------
+ # Private methods below this line
+
+ /**
+ *
+ */
+ function setupPage( $term ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+ $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
+ $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ }
+
+ /**
+ * Extract default namespaces to search from the given user's
+ * settings, returning a list of index numbers.
+ *
+ * @param User $user
+ * @return array
+ * @private
+ */
+ function userNamespaces( &$user ) {
+ $arr = array();
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if( $user->getOption( 'searchNs' . $ns ) ) {
+ $arr[] = $ns;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Extract "power search" namespace settings from the request object,
+ * returning a list of index numbers to search.
+ *
+ * @param WebRequest $request
+ * @return array
+ * @private
+ */
+ function powerSearch( &$request ) {
+ $arr = array();
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if( $request->getCheck( 'ns' . $ns ) ) {
+ $arr[] = $ns;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Reconstruct the 'power search' options for links
+ * @return array
+ * @private
+ */
+ function powerSearchOptions() {
+ $opt = array();
+ foreach( $this->namespaces as $n ) {
+ $opt['ns' . $n] = 1;
+ }
+ $opt['redirs'] = $this->searchRedirects ? 1 : 0;
+ $opt['searchx'] = 1;
+ return $opt;
+ }
+
+ /**
+ * @param SearchResultSet $matches
+ * @param string $terms partial regexp for highlighting terms
+ */
+ function showMatches( &$matches ) {
+ $fname = 'SpecialSearch::showMatches';
+ wfProfileIn( $fname );
+
+ global $wgContLang;
+ $tm = $wgContLang->convertForSearchResult( $matches->termMatches() );
+ $terms = implode( '|', $tm );
+
+ $off = $this->offset + 1;
+ $out = "<ol start='{$off}'>\n";
+
+ while( $result = $matches->next() ) {
+ $out .= $this->showHit( $result, $terms );
+ }
+ $out .= "</ol>\n";
+
+ // convert the whole thing to desired language variant
+ global $wgContLang;
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( $fname );
+ return $out;
+ }
+
+ /**
+ * Format a single hit result
+ * @param SearchResult $result
+ * @param string $terms partial regexp for highlighting terms
+ */
+ function showHit( $result, $terms ) {
+ $fname = 'SpecialSearch::showHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ $t = $result->getTitle();
+ if( is_null( $t ) ) {
+ wfProfileOut( $fname );
+ return "<!-- Broken link in search result -->\n";
+ }
+ $sk =& $wgUser->getSkin();
+
+ $contextlines = $wgUser->getOption( 'contextlines' );
+ if ( '' == $contextlines ) { $contextlines = 5; }
+ $contextchars = $wgUser->getOption( 'contextchars' );
+ if ( '' == $contextchars ) { $contextchars = 50; }
+
+ $link = $sk->makeKnownLinkObj( $t );
+ $revision = Revision::newFromTitle( $t );
+ $text = $revision->getText();
+ $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( strlen( $text ) ) );
+
+ $lines = explode( "\n", $text );
+
+ $max = intval( $contextchars ) + 1;
+ $pat1 = "/(.*)($terms)(.{0,$max})/i";
+
+ $lineno = 0;
+
+ $extract = '';
+ wfProfileIn( "$fname-extract" );
+ foreach ( $lines as $line ) {
+ if ( 0 == $contextlines ) {
+ break;
+ }
+ ++$lineno;
+ if ( ! preg_match( $pat1, $line, $m ) ) {
+ continue;
+ }
+ --$contextlines;
+ $pre = $wgContLang->truncate( $m[1], -$contextchars, '...' );
+
+ if ( count( $m ) < 3 ) {
+ $post = '';
+ } else {
+ $post = $wgContLang->truncate( $m[3], $contextchars, '...' );
+ }
+
+ $found = $m[2];
+
+ $line = htmlspecialchars( $pre . $found . $post );
+ $pat2 = '/(' . $terms . ")/i";
+ $line = preg_replace( $pat2,
+ "<span class='searchmatch'>\\1</span>", $line );
+
+ $extract .= "<br /><small>{$lineno}: {$line}</small>\n";
+ }
+ wfProfileOut( "$fname-extract" );
+ wfProfileOut( $fname );
+ return "<li>{$link} ({$size}){$extract}</li>\n";
+ }
+
+ function powerSearchBox( $term ) {
+ $namespaces = '';
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ $checked = in_array( $ns, $this->namespaces )
+ ? ' checked="checked"'
+ : '';
+ $name = str_replace( '_', ' ', $name );
+ if( '' == $name ) {
+ $name = wfMsg( 'blanknamespace' );
+ }
+ $namespaces .= " <label><input type='checkbox' value=\"1\" name=\"" .
+ "ns{$ns}\"{$checked} />{$name}</label>\n";
+ }
+
+ $checked = $this->searchRedirects
+ ? ' checked="checked"'
+ : '';
+ $redirect = "<input type='checkbox' value='1' name=\"redirs\"{$checked} />\n";
+
+ $searchField = '<input type="text" name="search" value="' .
+ htmlspecialchars( $term ) ."\" size=\"16\" />\n";
+
+ $searchButton = '<input type="submit" name="searchx" value="' .
+ htmlspecialchars( wfMsg('powersearch') ) . "\" />\n";
+
+ $ret = wfMsg( 'powersearchtext',
+ $namespaces, $redirect, $searchField,
+ '', '', '', '', '', # Dummy placeholders
+ $searchButton );
+
+ $title = Title::makeTitle( NS_SPECIAL, 'Search' );
+ $action = $title->escapeLocalURL();
+ return "<br /><br />\n<form id=\"powersearch\" method=\"get\" " .
+ "action=\"$action\">\n{$ret}\n</form>\n";
+ }
+}
+
+?>
diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php
new file mode 100644
index 00000000..d8e13c7b
--- /dev/null
+++ b/includes/SpecialShortpages.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * SpecialShortpages extends QueryPage. It is used to return the shortest
+ * pages in the database.
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class ShortPagesPage extends QueryPage {
+
+ function getName() {
+ return "Shortpages";
+ }
+
+ /**
+ * This query is indexed as of 1.5
+ */
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ $forceindex = $dbr->useIndexClause("page_len");
+ return
+ "SELECT $name as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_len AS value
+ FROM $page $forceindex
+ WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0";
+ }
+
+ function preprocessResults( &$dbo, $res ) {
+ # There's no point doing a batch check if we aren't caching results;
+ # the page must exist for it to have been pulled out of the table
+ if( $this->isCached() ) {
+ $batch = new LinkBatch();
+ while( $row = $dbo->fetchObject( $res ) )
+ $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ $batch->execute();
+ if( $dbo->numRows( $res ) > 0 )
+ $dbo->dataSeek( $res, 0 );
+ }
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $plink = $this->isCached()
+ ? $skin->makeLinkObj( $title )
+ : $skin->makeKnownLinkObj( $title );
+ $size = wfMsgHtml( 'nbytes', $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
+
+ return $title->exists()
+ ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
+ : "<s>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</s>";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialShortpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $spp = new ShortPagesPage();
+
+ return $spp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php
new file mode 100644
index 00000000..0b53db73
--- /dev/null
+++ b/includes/SpecialSpecialpages.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialSpecialpages() {
+ global $wgOut, $wgUser, $wgAvailableRights;
+
+ $wgOut->setRobotpolicy( 'index,nofollow' );
+ $sk = $wgUser->getSkin();
+
+ # Get listable pages, in a 2-d array with the first dimension being user right
+ $pages = SpecialPage::getPages();
+
+ /** Pages available to all */
+ wfSpecialSpecialpages_gen($pages[''],'spheading',$sk);
+
+ /** Restricted special pages */
+ $rpages = array();
+ foreach($wgAvailableRights as $right) {
+ /** only show pages a user can access */
+ if( $wgUser->isAllowed($right) ) {
+ /** some rights might not have any special page associated */
+ if(isset($pages[$right])) {
+ $rpages = array_merge( $rpages, $pages[$right] );
+ }
+ }
+ }
+ wfSpecialSpecialpages_gen( $rpages, 'restrictedpheading', $sk );
+}
+
+/**
+ * sub function generating the list of pages
+ * @param $pages the list of pages
+ * @param $heading header to be used
+ * @param $sk skin object ???
+ */
+function wfSpecialSpecialpages_gen($pages,$heading,$sk) {
+ global $wgOut, $wgSortSpecialPages;
+
+ if( count( $pages ) == 0 ) {
+ # Yeah, that was pointless. Thanks for coming.
+ return;
+ }
+
+ /** Put them into a sortable array */
+ $sortedPages = array();
+ foreach ( $pages as $name => $page ) {
+ if ( $page->isListed() ) {
+ $sortedPages[$page->getDescription()] = $page->getTitle();
+ }
+ }
+
+ /** Sort */
+ if ( $wgSortSpecialPages ) {
+ ksort( $sortedPages );
+ }
+
+ /** Now output the HTML */
+ $wgOut->addHTML( '<h2>' . wfMsgHtml( $heading ) . "</h2>\n<ul>" );
+ foreach ( $sortedPages as $desc => $title ) {
+ $link = $sk->makeKnownLinkObj( $title, $desc );
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+ $wgOut->addHTML( "</ul>\n" );
+}
+
+?>
diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php
new file mode 100644
index 00000000..5903546a
--- /dev/null
+++ b/includes/SpecialStatistics.php
@@ -0,0 +1,86 @@
+<?php
+/**
+*
+* @package MediaWiki
+* @subpackage SpecialPage
+*/
+
+/**
+* constructor
+*/
+function wfSpecialStatistics() {
+ global $wgOut, $wgLang, $wgRequest;
+ $fname = 'wfSpecialStatistics';
+
+ $action = $wgRequest->getVal( 'action' );
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'site_stats', 'user', 'user_groups' ) );
+
+ $row = $dbr->selectRow( 'site_stats', '*', false, $fname );
+ $views = $row->ss_total_views;
+ $edits = $row->ss_total_edits;
+ $good = $row->ss_good_articles;
+ $images = $row->ss_images;
+
+ # This code is somewhat schema-agnostic, because I'm changing it in a minor release -- TS
+ if ( isset( $row->ss_total_pages ) && $row->ss_total_pages == -1 ) {
+ # Update schema
+ $u = new SiteStatsUpdate( 0, 0, 0 );
+ $u->doUpdate();
+ $row = $dbr->selectRow( 'site_stats', '*', false, $fname );
+ }
+
+ if ( isset( $row->ss_total_pages ) ) {
+ $total = $row->ss_total_pages;
+ } else {
+ $sql = "SELECT COUNT(page_namespace) AS total FROM $page";
+ $res = $dbr->query( $sql, $fname );
+ $pageRow = $dbr->fetchObject( $res );
+ $total = $pageRow->total;
+ }
+
+ if ( isset( $row->ss_users ) ) {
+ $users = $row->ss_users;
+ } else {
+ $sql = "SELECT MAX(user_id) AS total FROM $user";
+ $res = $dbr->query( $sql, $fname );
+ $userRow = $dbr->fetchObject( $res );
+ $users = $userRow->total;
+ }
+
+ $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), $fname );
+ $numJobs = $dbr->selectField( 'job', 'COUNT(*)', '', $fname );
+
+ if ($action == 'raw') {
+ $wgOut->disable();
+ header( 'Pragma: nocache' );
+ echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n";
+ return;
+ } else {
+ $text = '==' . wfMsg( 'sitestats' ) . "==\n" ;
+ $text .= wfMsg( 'sitestatstext',
+ $wgLang->formatNum( $total ),
+ $wgLang->formatNum( $good ),
+ $wgLang->formatNum( $views ),
+ $wgLang->formatNum( $edits ),
+ $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ),
+ $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
+ $wgLang->formatNum( $numJobs ),
+ $wgLang->formatNum( $images )
+ );
+
+ $text .= "\n==" . wfMsg( 'userstats' ) . "==\n";
+
+ $text .= wfMsg( 'userstatstext',
+ $wgLang->formatNum( $users ),
+ $wgLang->formatNum( $admins ),
+ '[[' . wfMsgForContent( 'administrators' ) . ']]',
+ // should logically be after #admins, damn backwards compatability!
+ $wgLang->formatNum( sprintf( '%.2f', $admins / $users * 100 ) )
+ );
+
+ $wgOut->addWikiText( $text );
+ }
+}
+?>
diff --git a/includes/SpecialUncategorizedcategories.php b/includes/SpecialUncategorizedcategories.php
new file mode 100644
index 00000000..ba399f0c
--- /dev/null
+++ b/includes/SpecialUncategorizedcategories.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( "SpecialUncategorizedpages.php" );
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UncategorizedCategoriesPage extends UncategorizedPagesPage {
+ function UncategorizedCategoriesPage() {
+ $this->requestedNamespace = NS_CATEGORY;
+ }
+
+ function getName() {
+ return "Uncategorizedcategories";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedCategoriesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialUncategorizedimages.php b/includes/SpecialUncategorizedimages.php
new file mode 100644
index 00000000..38156976
--- /dev/null
+++ b/includes/SpecialUncategorizedimages.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Special page lists images which haven't been categorised
+ *
+ * @package MediaWiki
+ * @subpackage Special pages
+ * @author Rob Church <robchur@gmail.com>
+ */
+
+class UncategorizedImagesPage extends QueryPage {
+
+ function getName() {
+ return 'Uncategorizedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'categorylinks' ) );
+ $ns = NS_IMAGE;
+
+ return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
+ page_title AS title, page_title AS value
+ FROM {$page} LEFT JOIN {$categorylinks} ON page_id = cl_from
+ WHERE cl_from IS NULL AND page_namespace = {$ns} AND page_is_redirect = 0";
+ }
+
+ function formatResult( &$skin, $row ) {
+ global $wgContLang;
+ $title = Title::makeTitleSafe( NS_IMAGE, $row->title );
+ $label = htmlspecialchars( $wgContLang->convert( $title->getText() ) );
+ return $skin->makeKnownLinkObj( $title, $label );
+ }
+
+}
+
+function wfSpecialUncategorizedimages() {
+ $uip = new UncategorizedImagesPage();
+ list( $limit, $offset ) = wfCheckLimits();
+ return $uip->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialUncategorizedpages.php b/includes/SpecialUncategorizedpages.php
new file mode 100644
index 00000000..0ecc5d07
--- /dev/null
+++ b/includes/SpecialUncategorizedpages.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UncategorizedPagesPage extends PageQueryPage {
+ var $requestedNamespace = NS_MAIN;
+
+ function getName() {
+ return "Uncategorizedpages";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'categorylinks' ) );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ return
+ "
+ SELECT
+ $name as type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_id=cl_from
+ WHERE cl_from IS NULL AND page_namespace={$this->requestedNamespace} AND page_is_redirect=0
+ ";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php
new file mode 100644
index 00000000..695c8c29
--- /dev/null
+++ b/includes/SpecialUndelete.php
@@ -0,0 +1,737 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and restore deleted content
+ *
+ * @package MediaWiki
+ * @subpackage Special pages
+ */
+
+/**
+ *
+ */
+function wfSpecialUndelete( $par ) {
+ global $wgRequest;
+
+ $form = new UndeleteForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class PageArchive {
+ var $title;
+
+ function PageArchive( &$title ) {
+ if( is_null( $title ) ) {
+ throw new MWException( 'Archiver() given a null title.');
+ }
+ $this->title =& $title;
+ }
+
+ /**
+ * List all deleted pages recorded in the archive table. Returns result
+ * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+ * namespace/title. Can be called staticaly.
+ *
+ * @return ResultWrapper
+ */
+ /* static */ function listAllPages() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $archive = $dbr->tableName( 'archive' );
+
+ $sql = "SELECT ar_namespace,ar_title, COUNT(*) AS count FROM $archive " .
+ "GROUP BY ar_namespace,ar_title ORDER BY ar_namespace,ar_title";
+
+ return $dbr->resultObject( $dbr->query( $sql, 'PageArchive::listAllPages' ) );
+ }
+
+ /**
+ * List the revisions of the given page. Returns result wrapper with
+ * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+ *
+ * @return ResultWrapper
+ */
+ function listRevisions() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'archive',
+ array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::listRevisions',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+
+ /**
+ * List the deleted file revisions for this page, if it's a file page.
+ * Returns a result wrapper with various filearchive fields, or null
+ * if not a file page.
+ *
+ * @return ResultWrapper
+ * @fixme Does this belong in Image for fuller encapsulation?
+ */
+ function listFiles() {
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'filearchive',
+ array(
+ 'fa_id',
+ 'fa_name',
+ 'fa_storage_key',
+ 'fa_size',
+ 'fa_width',
+ 'fa_height',
+ 'fa_description',
+ 'fa_user',
+ 'fa_user_text',
+ 'fa_timestamp' ),
+ array( 'fa_name' => $this->title->getDbKey() ),
+ $fname,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+ return null;
+ }
+
+ /**
+ * Fetch (and decompress if necessary) the stored text for the deleted
+ * revision of the page with the given timestamp.
+ *
+ * @return string
+ */
+ function getRevisionText( $timestamp ) {
+ $fname = 'PageArchive::getRevisionText';
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array( 'ar_text', 'ar_flags', 'ar_text_id' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDbkey(),
+ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
+ $fname );
+ if( $row ) {
+ return $this->getTextFromRow( $row );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+ */
+ function getTextFromRow( $row ) {
+ $fname = 'PageArchive::getTextFromRow';
+
+ if( is_null( $row->ar_text_id ) ) {
+ // An old row from MediaWiki 1.4 or previous.
+ // Text is embedded in this row in classic compression format.
+ return Revision::getRevisionText( $row, "ar_" );
+ } else {
+ // New-style: keyed to the text storage backend.
+ $dbr =& wfGetDB( DB_SLAVE );
+ $text = $dbr->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $row->ar_text_id ),
+ $fname );
+ return Revision::getRevisionText( $text );
+ }
+ }
+
+
+ /**
+ * Fetch (and decompress if necessary) the stored text of the most
+ * recently edited deleted revision of the page.
+ *
+ * If there are no archived revisions for the page, returns NULL.
+ *
+ * @return string
+ */
+ function getLastRevisionText() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array( 'ar_text', 'ar_flags', 'ar_text_id' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::getLastRevisionText',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ if( $row ) {
+ return $this->getTextFromRow( $row );
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Quick check if any archived revisions are present for the page.
+ * @return bool
+ */
+ function isDeleted() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ) );
+ return ($n > 0);
+ }
+
+ /**
+ * Restore the given (or all) text and file revisions for the page.
+ * Once restored, the items will be removed from the archive tables.
+ * The deletion log will be updated with an undeletion notice.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ *
+ * @return true on success.
+ */
+ function undelete( $timestamps, $comment = '', $fileVersions = array() ) {
+ // If both the set of text revisions and file revisions are empty,
+ // restore everything. Otherwise, just restore the requested items.
+ $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+ $restoreText = $restoreAll || !empty( $timestamps );
+ $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+ if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+ $img = new Image( $this->title );
+ $filesRestored = $img->restore( $fileVersions );
+ } else {
+ $filesRestored = 0;
+ }
+
+ if( $restoreText ) {
+ $textRestored = $this->undeleteRevisions( $timestamps );
+ } else {
+ $textRestored = 0;
+ }
+
+ // Touch the log!
+ global $wgContLang;
+ $log = new LogPage( 'delete' );
+
+ if( $textRestored && $filesRestored ) {
+ $reason = wfMsgForContent( 'undeletedrevisions-files',
+ $wgContLang->formatNum( $textRestored ),
+ $wgContLang->formatNum( $filesRestored ) );
+ } elseif( $textRestored ) {
+ $reason = wfMsgForContent( 'undeletedrevisions',
+ $wgContLang->formatNum( $textRestored ) );
+ } elseif( $filesRestored ) {
+ $reason = wfMsgForContent( 'undeletedfiles',
+ $wgContLang->formatNum( $filesRestored ) );
+ } else {
+ wfDebug( "Undelete: nothing undeleted...\n" );
+ return false;
+ }
+
+ if( trim( $comment ) != '' )
+ $reason .= ": {$comment}";
+ $log->addEntry( 'restore', $this->title, $reason );
+
+ return true;
+ }
+
+ /**
+ * This is the meaty bit -- restores archived revisions of the given page
+ * to the cur/old tables. If the page currently exists, all revisions will
+ * be stuffed into old, otherwise the most recent will go into cur.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ *
+ * @return int number of revisions restored
+ */
+ private function undeleteRevisions( $timestamps ) {
+ global $wgParser, $wgDBtype;
+
+ $fname = __CLASS__ . '::' . __FUNCTION__;
+ $restoreAll = empty( $timestamps );
+
+ $dbw =& wfGetDB( DB_MASTER );
+ extract( $dbw->tableNames( 'page', 'archive' ) );
+
+ # Does this page already exist? We'll have to update it...
+ $article = new Article( $this->title );
+ $options = ( $wgDBtype == 'postgres' )
+ ? '' // pg doesn't support this?
+ : 'FOR UPDATE';
+ $page = $dbw->selectRow( 'page',
+ array( 'page_id', 'page_latest' ),
+ array( 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey() ),
+ $fname,
+ $options );
+ if( $page ) {
+ # Page already exists. Import the history, and if necessary
+ # we'll update the latest revision field in the record.
+ $newid = 0;
+ $pageId = $page->page_id;
+ $previousRevId = $page->page_latest;
+ } else {
+ # Have to create a new article...
+ $newid = $article->insertOn( $dbw );
+ $pageId = $newid;
+ $previousRevId = 0;
+ }
+
+ if( $restoreAll ) {
+ $oldones = '1 = 1'; # All revisions...
+ } else {
+ $oldts = implode( ',',
+ array_map( array( &$dbw, 'addQuotes' ),
+ array_map( array( &$dbw, 'timestamp' ),
+ $timestamps ) ) );
+
+ $oldones = "ar_timestamp IN ( {$oldts} )";
+ }
+
+ /**
+ * Restore each revision...
+ */
+ $result = $dbw->select( 'archive',
+ /* fields */ array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id' ),
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ $fname,
+ /* options */ array(
+ 'ORDER BY' => 'ar_timestamp' )
+ );
+ if( $dbw->numRows( $result ) < count( $timestamps ) ) {
+ wfDebug( "$fname: couldn't find all requested rows\n" );
+ return false;
+ }
+
+ $revision = null;
+ $newRevId = $previousRevId;
+ $restored = 0;
+
+ while( $row = $dbw->fetchObject( $result ) ) {
+ if( $row->ar_text_id ) {
+ // Revision was deleted in 1.5+; text is in
+ // the regular text table, use the reference.
+ // Specify null here so the so the text is
+ // dereferenced for page length info if needed.
+ $revText = null;
+ } else {
+ // Revision was deleted in 1.4 or earlier.
+ // Text is squashed into the archive row, and
+ // a new text table entry will be created for it.
+ $revText = Revision::getRevisionText( $row, 'ar_' );
+ }
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'id' => $row->ar_rev_id,
+ 'text' => $revText,
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'text_id' => $row->ar_text_id,
+ ) );
+ $newRevId = $revision->insertOn( $dbw );
+ $restored++;
+ }
+
+ if( $revision ) {
+ # FIXME: Update latest if newer as well...
+ if( $newid ) {
+ # FIXME: update article count if changed...
+ $article->updateRevisionOn( $dbw, $revision, $previousRevId );
+
+ # Finally, clean up the link tables
+ $options = new ParserOptions;
+ $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options,
+ true, true, $newRevId );
+ $u = new LinksUpdate( $this->title, $parserOutput );
+ $u->doUpdate();
+
+ #TODO: SearchUpdate, etc.
+ }
+
+ if( $newid ) {
+ Article::onArticleCreate( $this->title );
+ } else {
+ Article::onArticleEdit( $this->title );
+ }
+ } else {
+ # Something went terribly wrong!
+ }
+
+ # Now that it's safely stored, take it out of the archive
+ $dbw->delete( 'archive',
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ $fname );
+
+ return $restored;
+ }
+
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UndeleteForm {
+ var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
+ var $mTargetTimestamp, $mAllowed, $mComment;
+
+ function UndeleteForm( &$request, $par = "" ) {
+ global $wgUser;
+ $this->mAction = $request->getText( 'action' );
+ $this->mTarget = $request->getText( 'target' );
+ $this->mTimestamp = $request->getText( 'timestamp' );
+ $this->mFile = $request->getVal( 'file' );
+
+ $posted = $request->wasPosted() &&
+ $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ $this->mRestore = $request->getCheck( 'restore' ) && $posted;
+ $this->mPreview = $request->getCheck( 'preview' ) && $posted;
+ $this->mComment = $request->getText( 'wpComment' );
+
+ if( $par != "" ) {
+ $this->mTarget = $par;
+ }
+ if ( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
+ $this->mAllowed = true;
+ } else {
+ $this->mAllowed = false;
+ $this->mTimestamp = '';
+ $this->mRestore = false;
+ }
+ if ( $this->mTarget !== "" ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ } else {
+ $this->mTargetObj = NULL;
+ }
+ if( $this->mRestore ) {
+ $timestamps = array();
+ $this->mFileVersions = array();
+ foreach( $_REQUEST as $key => $val ) {
+ if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
+ array_push( $timestamps, $matches[1] );
+ }
+
+ if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
+ $this->mFileVersions[] = intval( $matches[1] );
+ }
+ }
+ rsort( $timestamps );
+ $this->mTargetTimestamp = $timestamps;
+ }
+ }
+
+ function execute() {
+
+ if( is_null( $this->mTargetObj ) ) {
+ return $this->showList();
+ }
+ if( $this->mTimestamp !== '' ) {
+ return $this->showRevision( $this->mTimestamp );
+ }
+ if( $this->mFile !== null ) {
+ return $this->showFile( $this->mFile );
+ }
+ if( $this->mRestore && $this->mAction == "submit" ) {
+ return $this->undelete();
+ }
+ return $this->showHistory();
+ }
+
+ /* private */ function showList() {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+ $fname = "UndeleteForm::showList";
+
+ # List undeletable articles
+ $result = PageArchive::listAllPages();
+
+ if ( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
+ }
+ $wgOut->addWikiText( wfMsg( "undeletepagetext" ) );
+
+ $sk = $wgUser->getSkin();
+ $undelete =& Title::makeTitle( NS_SPECIAL, 'Undelete' );
+ $wgOut->addHTML( "<ul>\n" );
+ while( $row = $result->fetchObject() ) {
+ $n = ($row->ar_namespace ?
+ ($wgContLang->getNsText( $row->ar_namespace ) . ":") : "").
+ $row->ar_title;
+ $link = $sk->makeKnownLinkObj( $undelete,
+ htmlspecialchars( $n ), "target=" . urlencode( $n ) );
+ $revisions = htmlspecialchars( wfMsg( "undeleterevisions",
+ $wgLang->formatNum( $row->count ) ) );
+ $wgOut->addHTML( "<li>$link ($revisions)</li>\n" );
+ }
+ $result->free();
+ $wgOut->addHTML( "</ul>\n" );
+
+ return true;
+ }
+
+ /* private */ function showRevision( $timestamp ) {
+ global $wgLang, $wgUser, $wgOut;
+ $fname = "UndeleteForm::showRevision";
+
+ if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
+
+ $archive =& new PageArchive( $this->mTargetObj );
+ $text = $archive->getRevisionText( $timestamp );
+
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ $wgOut->addWikiText( "(" . wfMsg( "undeleterevision",
+ $wgLang->date( $timestamp ) ) . ")\n" );
+
+ if( $this->mPreview ) {
+ $wgOut->addHtml( "<hr />\n" );
+ $wgOut->addWikiText( $text );
+ }
+
+ $self = Title::makeTitle( NS_SPECIAL, "Undelete" );
+
+ $wgOut->addHtml(
+ wfElement( 'textarea', array(
+ 'readonly' => true,
+ 'cols' => intval( $wgUser->getOption( 'cols' ) ),
+ 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
+ $text . "\n" ) .
+ wfOpenElement( 'div' ) .
+ wfOpenElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $self->getLocalURL( "action=submit" ) ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'target',
+ 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'timestamp',
+ 'value' => $timestamp ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $wgUser->editToken() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'preview',
+ 'value' => '1' ) ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'value' => wfMsg( 'showpreview' ) ) ) .
+ wfCloseElement( 'form' ) .
+ wfCloseElement( 'div' ) );
+ }
+
+ /**
+ * Show a deleted file version requested by the visitor.
+ */
+ function showFile( $key ) {
+ global $wgOut;
+ $wgOut->disable();
+
+ $store = FileStore::get( 'deleted' );
+ $store->stream( $key );
+ }
+
+ /* private */ function showHistory() {
+ global $wgLang, $wgUser, $wgOut;
+
+ $sk = $wgUser->getSkin();
+ if ( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
+ }
+
+ $archive = new PageArchive( $this->mTargetObj );
+ $text = $archive->getLastRevisionText();
+ /*
+ if( is_null( $text ) ) {
+ $wgOut->addWikiText( wfMsg( "nohistory" ) );
+ return;
+ }
+ */
+ if ( $this->mAllowed ) {
+ $wgOut->addWikiText( wfMsg( "undeletehistory" ) );
+ } else {
+ $wgOut->addWikiText( wfMsg( "undeletehistorynoadmin" ) );
+ }
+
+ # List all stored revisions
+ $revisions = $archive->listRevisions();
+ $files = $archive->listFiles();
+
+ $haveRevisions = $revisions && $revisions->numRows() > 0;
+ $haveFiles = $files && $files->numRows() > 0;
+
+ # Batch existence check on user and talk pages
+ if( $haveRevisions ) {
+ $batch = new LinkBatch();
+ while( $row = $revisions->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
+ }
+ $batch->execute();
+ $revisions->seek( 0 );
+ }
+ if( $haveFiles ) {
+ $batch = new LinkBatch();
+ while( $row = $files->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
+ }
+ $batch->execute();
+ $files->seek( 0 );
+ }
+
+ if ( $this->mAllowed ) {
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Undelete" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+ $wgOut->addHtml( $top );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ require_once( 'SpecialLog.php' );
+ $logViewer =& new LogViewer(
+ new LogReader(
+ new FauxRequest(
+ array( 'page' => $this->mTargetObj->getPrefixedText(),
+ 'type' => 'delete' ) ) ) );
+ $logViewer->showList( $wgOut );
+
+ if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ $table = '<fieldset><table><tr>';
+ $table .= '<td colspan="2">' . wfMsgWikiHtml( 'undeleteextrahelp' ) . '</td></tr><tr>';
+ $table .= '<td align="right"><strong>' . wfMsgHtml( 'undeletecomment' ) . '</strong></td>';
+ $table .= '<td>' . wfInput( 'wpComment', 50, $this->mComment ) . '</td>';
+ $table .= '</tr><tr><td>&nbsp;</td><td>';
+ $table .= wfSubmitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore' ) );
+ $table .= wfElement( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ) ) );
+ $table .= '</td></tr></table></fieldset>';
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( wfMsg( "history" ) ) . "</h2>\n" );
+
+ if( $haveRevisions ) {
+ # The page's stored (deleted) history:
+ $wgOut->addHTML("<ul>");
+ $target = urlencode( $this->mTarget );
+ while( $row = $revisions->fetchObject() ) {
+ $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+ if ( $this->mAllowed ) {
+ $checkBox = wfCheck( "ts$ts" );
+ $pageLink = $sk->makeKnownLinkObj( $titleObj,
+ $wgLang->timeanddate( $ts, true ),
+ "target=$target&timestamp=$ts" );
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ }
+ $userLink = $sk->userLink( $row->ar_user, $row->ar_user_text ) . $sk->userToolLinks( $row->ar_user, $row->ar_user_text );
+ $comment = $sk->commentBlock( $row->ar_comment );
+ $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $comment</li>\n" );
+
+ }
+ $revisions->free();
+ $wgOut->addHTML("</ul>");
+ } else {
+ $wgOut->addWikiText( wfMsg( "nohistory" ) );
+ }
+
+
+ if( $haveFiles ) {
+ $wgOut->addHtml( "<h2>" . wfMsgHtml( 'imghistory' ) . "</h2>\n" );
+ $wgOut->addHtml( "<ul>" );
+ while( $row = $files->fetchObject() ) {
+ $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
+ if ( $this->mAllowed && $row->fa_storage_key ) {
+ $checkBox = wfCheck( "fileid" . $row->fa_id );
+ $key = urlencode( $row->fa_storage_key );
+ $target = urlencode( $this->mTarget );
+ $pageLink = $sk->makeKnownLinkObj( $titleObj,
+ $wgLang->timeanddate( $ts, true ),
+ "target=$target&file=$key" );
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ }
+ $userLink = $sk->userLink( $row->fa_user, $row->fa_user_text ) . $sk->userToolLinks( $row->fa_user, $row->fa_user_text );
+ $data =
+ wfMsgHtml( 'widthheight',
+ $wgLang->formatNum( $row->fa_width ),
+ $wgLang->formatNum( $row->fa_height ) ) .
+ ' (' .
+ wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
+ ')';
+ $comment = $sk->commentBlock( $row->fa_description );
+ $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $data $comment</li>\n" );
+ }
+ $files->free();
+ $wgOut->addHTML( "</ul>" );
+ }
+
+ if ( $this->mAllowed ) {
+ # Slip in the hidden controls here
+ $misc = wfHidden( 'target', $this->mTarget );
+ $misc .= wfHidden( 'wpEditToken', $wgUser->editToken() );
+ $wgOut->addHtml( $misc . '</form>' );
+ }
+
+ return true;
+ }
+
+ function undelete() {
+ global $wgOut, $wgUser;
+ if( !is_null( $this->mTargetObj ) ) {
+ $archive = new PageArchive( $this->mTargetObj );
+ $ok = true;
+
+ $ok = $archive->undelete(
+ $this->mTargetTimestamp,
+ $this->mComment,
+ $this->mFileVersions );
+
+ if( $ok ) {
+ $skin =& $wgUser->getSkin();
+ $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+ $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
+ return true;
+ }
+ }
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ return false;
+ }
+}
+
+?>
diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php
new file mode 100644
index 00000000..a10d1ee0
--- /dev/null
+++ b/includes/SpecialUnlockdb.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialUnlockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if ( ! $wgUser->isAllowed('siteadmin') ) {
+ $wgOut->developerRequired();
+ return;
+ }
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBUnlockForm();
+
+ if ( "success" == $action ) {
+ $f->showSuccess();
+ } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( "" );
+ }
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class DBUnlockForm {
+ function showForm( $err )
+ {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->addWikiText( wfMsg( "unlockdbtext" ) );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) );
+ $lb = htmlspecialchars( wfMsg( "unlockbtn" ) );
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Unlockdb" );
+ $action = $titleObj->escapeLocalURL( "action=submit" );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+
+<form id="unlockdb" method="post" action="{$action}">
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgRequest, $wgReadOnlyFile;
+
+ $wpLockConfirm = $wgRequest->getCheck( 'wpLockConfirm' );
+ if ( ! $wpLockConfirm ) {
+ $this->showForm( wfMsg( "locknoconfirm" ) );
+ return;
+ }
+ if ( @! unlink( $wgReadOnlyFile ) ) {
+ $wgOut->showFileDeleteError( $wgReadOnlyFile );
+ return;
+ }
+ $titleObj = Title::makeTitle( NS_SPECIAL, "Unlockdb" );
+ $success = $titleObj->getFullURL( "action=success" );
+ $wgOut->redirect( $success );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+ global $ip;
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
+ $wgOut->addWikiText( wfMsg( "unlockdbsuccesstext", $ip ) );
+ }
+}
+
+?>
diff --git a/includes/SpecialUnusedcategories.php b/includes/SpecialUnusedcategories.php
new file mode 100644
index 00000000..270180ef
--- /dev/null
+++ b/includes/SpecialUnusedcategories.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UnusedCategoriesPage extends QueryPage {
+
+ function getName() {
+ return 'Unusedcategories';
+ }
+
+ function getPageHeader() {
+ return '<p>' . wfMsg('unusedcategoriestext') . '</p>';
+ }
+
+ function getSQL() {
+ $NScat = NS_CATEGORY;
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'categorylinks','page' ));
+ return "SELECT 'Unusedcategories' as type,
+ {$NScat} as namespace, page_title as title, 1 as value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_title=cl_to
+ WHERE cl_from IS NULL
+ AND page_namespace = {$NScat}
+ AND page_is_redirect = 0";
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_CATEGORY, $result->title );
+ return $skin->makeLinkObj( $title, $title->getText() );
+ }
+}
+
+/** constructor */
+function wfSpecialUnusedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uc = new UnusedCategoriesPage();
+ return $uc->doQuery( $offset, $limit );
+}
+?>
diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php
new file mode 100644
index 00000000..32a6f95a
--- /dev/null
+++ b/includes/SpecialUnusedimages.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UnusedimagesPage extends QueryPage {
+
+ function getName() {
+ return 'Unusedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgCountCategorizedImagesAsUsed;
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ if ( $wgCountCategorizedImagesAsUsed ) {
+ extract( $dbr->tableNames( 'page', 'image', 'imagelinks', 'categorylinks' ) );
+
+ return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description
+ FROM ((('.$page.' AS I LEFT JOIN '.$categorylinks.' AS L ON I.page_id = L.cl_from)
+ LEFT JOIN '.$imagelinks.' AS P ON I.page_title = P.il_to)
+ INNER JOIN '.$image.' AS G ON I.page_title = G.img_name)
+ WHERE I.page_namespace = '.NS_IMAGE.' AND L.cl_from IS NULL AND P.il_to IS NULL';
+ } else {
+ extract( $dbr->tableNames( 'image','imagelinks' ) );
+
+ return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description' .
+ ' FROM '.$image.' LEFT JOIN '.$imagelinks.' ON img_name=il_to WHERE il_to IS NULL ';
+ }
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $title = Title::makeTitle( NS_IMAGE, $result->title );
+
+ $imageUrl = htmlspecialchars( Image::imageUrl( $result->title ) );
+ $dirmark = $wgContLang->getDirMark(); // To keep text in correct order
+
+ $return =
+ # The 'desc' linking to the image page
+ '('.$skin->makeKnownLinkObj( $title, wfMsg('imgdesc') ).') ' . $dirmark .
+
+ # Link to the image itself
+ '<a href="' . $imageUrl . '">' . htmlspecialchars( $title->getText() ) .
+ '</a> . . ' . $dirmark .
+
+ # Last modified date
+ $wgLang->timeanddate($result->value) . ' . . ' . $dirmark .
+
+ # Link to username
+ $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ),
+ $result->img_user_text) . $dirmark .
+
+ # If there is a description, show it
+ $skin->commentBlock( $wgContLang->convert( $result->img_description ) );
+
+ return $return;
+ }
+
+ function getPageHeader() {
+ return wfMsg( "unusedimagestext" );
+ }
+
+}
+
+/**
+ * Entry point
+ */
+function wfSpecialUnusedimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uip = new UnusedimagesPage();
+
+ return $uip->doQuery( $offset, $limit );
+}
+?>
diff --git a/includes/SpecialUnusedtemplates.php b/includes/SpecialUnusedtemplates.php
new file mode 100644
index 00000000..b33a24da
--- /dev/null
+++ b/includes/SpecialUnusedtemplates.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @package MediaWiki
+ * @subpackage Special pages
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+class UnusedtemplatesPage extends QueryPage {
+
+ function getName() { return( 'Unusedtemplates' ); }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+ function sortDescending() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'templatelinks' ) );
+ $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title,
+ page_namespace AS namespace, 0 AS value
+ FROM $page
+ LEFT JOIN $templatelinks
+ ON page_namespace = tl_namespace AND page_title = tl_title
+ WHERE page_namespace = 10 AND tl_from IS NULL";
+ return $sql;
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_TEMPLATE, $result->title );
+ $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
+ $wlhLink = $skin->makeKnownLinkObj(
+ Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ),
+ wfMsgHtml( 'unusedtemplateswlh' ),
+ 'target=' . $title->getPrefixedUrl() );
+ return wfSpecialList( $pageLink, $wlhLink );
+ }
+
+ function getPageHeader() {
+ global $wgOut;
+ return $wgOut->parse( wfMsg( 'unusedtemplatestext' ) );
+ }
+
+}
+
+function wfSpecialUnusedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UnusedtemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialUnwatchedpages.php b/includes/SpecialUnwatchedpages.php
new file mode 100644
index 00000000..66e5c091
--- /dev/null
+++ b/includes/SpecialUnwatchedpages.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * A special page that displays a list of pages that are not on anyones watchlist
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UnwatchedpagesPage extends QueryPage {
+
+ function getName() { return 'Unwatchedpages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'watchlist' ) );
+ $mwns = NS_MEDIAWIKI;
+ return
+ "
+ SELECT
+ 'Unwatchedpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_namespace as value
+ FROM $page
+ LEFT JOIN $watchlist ON wl_namespace = page_namespace AND page_title = wl_title
+ WHERE wl_title IS NULL AND page_is_redirect = 0 AND page_namespace<>$mwns
+ ";
+ }
+
+ function sortDescending() { return false; }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, htmlspecialchars( $text ) );
+ $wlink = $skin->makeKnownLinkObj( $nt, wfMsgHtml( 'watch' ), 'action=watch' );
+
+ return wfSpecialList( $plink, $wlink );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUnwatchedpages() {
+ global $wgUser, $wgOut;
+
+ if ( ! $wgUser->isAllowed( 'unwatchedpages' ) )
+ return $wgOut->permissionRequired( 'unwatchedpages' );
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new UnwatchedpagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php
new file mode 100644
index 00000000..06336df9
--- /dev/null
+++ b/includes/SpecialUpload.php
@@ -0,0 +1,1109 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once 'Image.php';
+/**
+ * Entry point
+ */
+function wfSpecialUpload() {
+ global $wgRequest;
+ $form = new UploadForm( $wgRequest );
+ $form->execute();
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UploadForm {
+ /**#@+
+ * @access private
+ */
+ var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError;
+ var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion;
+ var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload;
+ var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile;
+ /**#@-*/
+
+ /**
+ * Constructor : initialise object
+ * Get data POSTed through the form and assign them to the object
+ * @param $request Data posted.
+ */
+ function UploadForm( &$request ) {
+ $this->mDestFile = $request->getText( 'wpDestFile' );
+
+ if( !$request->wasPosted() ) {
+ # GET requests just give the main form; no data except wpDestfile.
+ return;
+ }
+
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
+ $this->mReUpload = $request->getCheck( 'wpReUpload' );
+ $this->mUpload = $request->getCheck( 'wpUpload' );
+
+ $this->mUploadDescription = $request->getText( 'wpUploadDescription' );
+ $this->mLicense = $request->getText( 'wpLicense' );
+ $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' );
+ $this->mUploadSource = $request->getText( 'wpUploadSource' );
+ $this->mWatchthis = $request->getBool( 'wpWatchthis' );
+ wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" );
+
+ $this->mAction = $request->getVal( 'action' );
+
+ $this->mSessionKey = $request->getInt( 'wpSessionKey' );
+ if( !empty( $this->mSessionKey ) &&
+ isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) {
+ /**
+ * Confirming a temporarily stashed upload.
+ * We don't want path names to be forged, so we keep
+ * them in the session on the server and just give
+ * an opaque key to the user agent.
+ */
+ $data = $_SESSION['wsUploadData'][$this->mSessionKey];
+ $this->mUploadTempName = $data['mUploadTempName'];
+ $this->mUploadSize = $data['mUploadSize'];
+ $this->mOname = $data['mOname'];
+ $this->mUploadError = 0/*UPLOAD_ERR_OK*/;
+ $this->mStashed = true;
+ $this->mRemoveTempFile = false;
+ } else {
+ /**
+ *Check for a newly uploaded file.
+ */
+ $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' );
+ $this->mUploadSize = $request->getFileSize( 'wpUploadFile' );
+ $this->mOname = $request->getFileName( 'wpUploadFile' );
+ $this->mUploadError = $request->getUploadError( 'wpUploadFile' );
+ $this->mSessionKey = false;
+ $this->mStashed = false;
+ $this->mRemoveTempFile = false; // PHP will handle this
+ }
+ }
+
+ /**
+ * Start doing stuff
+ * @access public
+ */
+ function execute() {
+ global $wgUser, $wgOut;
+ global $wgEnableUploads, $wgUploadDirectory;
+
+ # Check uploading enabled
+ if( !$wgEnableUploads ) {
+ $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' );
+ return;
+ }
+
+ # Check permissions
+ if( $wgUser->isLoggedIn() ) {
+ if( !$wgUser->isAllowed( 'upload' ) ) {
+ $wgOut->permissionRequired( 'upload' );
+ return;
+ }
+ } else {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ return;
+ }
+
+ # Check blocks
+ if( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ /** Check if the image directory is writeable, this is a common mistake */
+ if ( !is_writeable( $wgUploadDirectory ) ) {
+ $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) );
+ return;
+ }
+
+ if( $this->mReUpload ) {
+ if ( !$this->unsaveUploadedFile() ) {
+ return;
+ }
+ $this->mainUploadForm();
+ } else if ( 'submit' == $this->mAction || $this->mUpload ) {
+ $this->processUpload();
+ } else {
+ $this->mainUploadForm();
+ }
+
+ $this->cleanupTempFile();
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * Really do the upload
+ * Checks are made in SpecialUpload::execute()
+ * @access private
+ */
+ function processUpload() {
+ global $wgUser, $wgOut;
+
+ /* Check for PHP error if any, requires php 4.2 or newer */
+ if ( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
+ $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
+ return;
+ }
+
+ /**
+ * If there was no filename or a zero size given, give up quick.
+ */
+ if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) {
+ $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
+ return;
+ }
+
+ # Chop off any directories in the given filename
+ if ( $this->mDestFile ) {
+ $basename = wfBaseName( $this->mDestFile );
+ } else {
+ $basename = wfBaseName( $this->mOname );
+ }
+
+ /**
+ * We'll want to blacklist against *any* 'extension', and use
+ * only the final one for the whitelist.
+ */
+ list( $partname, $ext ) = $this->splitExtensions( $basename );
+
+ if( count( $ext ) ) {
+ $finalExt = $ext[count( $ext ) - 1];
+ } else {
+ $finalExt = '';
+ }
+ $fullExt = implode( '.', $ext );
+
+ # If there was more than one "extension", reassemble the base
+ # filename to prevent bogus complaints about length
+ if( count( $ext ) > 1 ) {
+ for( $i = 0; $i < count( $ext ) - 1; $i++ )
+ $partname .= '.' . $ext[$i];
+ }
+
+ if ( strlen( $partname ) < 3 ) {
+ $this->mainUploadForm( wfMsgHtml( 'minlength' ) );
+ return;
+ }
+
+ /**
+ * Filter out illegal characters, and try to make a legible name
+ * out of it. We'll strip some silently that Title would die on.
+ */
+ $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename );
+ $nt = Title::newFromText( $filtered );
+ if( is_null( $nt ) ) {
+ $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
+ return;
+ }
+ $nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() );
+ $this->mUploadSaveName = $nt->getDBkey();
+
+ /**
+ * If the image is protected, non-sysop users won't be able
+ * to modify it by uploading a new revision.
+ */
+ if( !$nt->userCanEdit() ) {
+ return $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) );
+ }
+
+ /**
+ * In some cases we may forbid overwriting of existing files.
+ */
+ $overwrite = $this->checkOverwrite( $this->mUploadSaveName );
+ if( WikiError::isError( $overwrite ) ) {
+ return $this->uploadError( $overwrite->toString() );
+ }
+
+ /* Don't allow users to override the blacklist (check file extension) */
+ global $wgStrictFileExtensions;
+ global $wgFileExtensions, $wgFileBlacklist;
+ if( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
+ ($wgStrictFileExtensions &&
+ !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
+ return $this->uploadError( wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ) );
+ }
+
+ /**
+ * Look at the contents of the file; if we can recognize the
+ * type but it's corrupt or data of the wrong type, we should
+ * probably not accept it.
+ */
+ if( !$this->mStashed ) {
+ $this->checkMacBinary();
+ $veri = $this->verify( $this->mUploadTempName, $finalExt );
+
+ if( $veri !== true ) { //it's a wiki error...
+ return $this->uploadError( $veri->toString() );
+ }
+ }
+
+ /**
+ * Provide an opportunity for extensions to add futher checks
+ */
+ $error = '';
+ if( !wfRunHooks( 'UploadVerification',
+ array( $this->mUploadSaveName, $this->mUploadTempName, &$error ) ) ) {
+ return $this->uploadError( $error );
+ }
+
+ /**
+ * Check for non-fatal conditions
+ */
+ if ( ! $this->mIgnoreWarning ) {
+ $warning = '';
+
+ global $wgCapitalLinks;
+ if( $wgCapitalLinks ) {
+ $filtered = ucfirst( $filtered );
+ }
+ if( $this->mUploadSaveName != $filtered ) {
+ $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mUploadSaveName ) ).'</li>';
+ }
+
+ global $wgCheckFileExtensions;
+ if ( $wgCheckFileExtensions ) {
+ if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
+ $warning .= '<li>'.wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ).'</li>';
+ }
+ }
+
+ global $wgUploadSizeWarning;
+ if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) {
+ # TODO: Format $wgUploadSizeWarning to something that looks better than the raw byte
+ # value, perhaps add GB,MB and KB suffixes?
+ $warning .= '<li>'.wfMsgHtml( 'largefile', $wgUploadSizeWarning, $this->mUploadSize ).'</li>';
+ }
+ if ( $this->mUploadSize == 0 ) {
+ $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
+ }
+
+ if( $nt->getArticleID() ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ $dlink = $sk->makeKnownLinkObj( $nt );
+ $warning .= '<li>'.wfMsgHtml( 'fileexists', $dlink ).'</li>';
+ } else {
+ # If the file existed before and was deleted, warn the user of this
+ # Don't bother doing so if the image exists now, however
+ $image = new Image( $nt );
+ if( $image->wasDeleted() ) {
+ $skin = $wgUser->getSkin();
+ $ltitle = Title::makeTitle( NS_SPECIAL, 'Log' );
+ $llink = $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() );
+ $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' );
+ }
+ }
+
+ if( $warning != '' ) {
+ /**
+ * Stash the file in a temporary location; the user can choose
+ * to let it through and we'll complete the upload then.
+ */
+ return $this->uploadWarning( $warning );
+ }
+ }
+
+ /**
+ * Try actually saving the thing...
+ * It will show an error form on failure.
+ */
+ $hasBeenMunged = !empty( $this->mSessionKey ) || $this->mRemoveTempFile;
+ if( $this->saveUploadedFile( $this->mUploadSaveName,
+ $this->mUploadTempName,
+ $hasBeenMunged ) ) {
+ /**
+ * Update the upload log and create the description page
+ * if it's a new file.
+ */
+ $img = Image::newFromName( $this->mUploadSaveName );
+ $success = $img->recordUpload( $this->mUploadOldVersion,
+ $this->mUploadDescription,
+ $this->mLicense,
+ $this->mUploadCopyStatus,
+ $this->mUploadSource,
+ $this->mWatchthis );
+
+ if ( $success ) {
+ $this->showSuccess();
+ wfRunHooks( 'UploadComplete', array( &$img ) );
+ } else {
+ // Image::recordUpload() fails if the image went missing, which is
+ // unlikely, hence the lack of a specialised message
+ $wgOut->showFileNotFoundError( $this->mUploadSaveName );
+ }
+ }
+ }
+
+ /**
+ * Move the uploaded file from its temporary location to the final
+ * destination. If a previous version of the file exists, move
+ * it into the archive subdirectory.
+ *
+ * @todo If the later save fails, we may have disappeared the original file.
+ *
+ * @param string $saveName
+ * @param string $tempName full path to the temporary file
+ * @param bool $useRename if true, doesn't check that the source file
+ * is a PHP-managed upload temporary
+ */
+ function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
+ global $wgOut;
+
+ $fname= "SpecialUpload::saveUploadedFile";
+
+ $dest = wfImageDir( $saveName );
+ $archive = wfImageArchiveDir( $saveName );
+ if ( !is_dir( $dest ) ) wfMkdirParents( $dest );
+ if ( !is_dir( $archive ) ) wfMkdirParents( $archive );
+
+ $this->mSavedFile = "{$dest}/{$saveName}";
+
+ if( is_file( $this->mSavedFile ) ) {
+ $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
+ wfSuppressWarnings();
+ $success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" );
+ wfRestoreWarnings();
+
+ if( ! $success ) {
+ $wgOut->showFileRenameError( $this->mSavedFile,
+ "${archive}/{$this->mUploadOldVersion}" );
+ return false;
+ }
+ else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n");
+ }
+ else {
+ $this->mUploadOldVersion = '';
+ }
+
+ wfSuppressWarnings();
+ $success = $useRename
+ ? rename( $tempName, $this->mSavedFile )
+ : move_uploaded_file( $tempName, $this->mSavedFile );
+ wfRestoreWarnings();
+
+ if( ! $success ) {
+ $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
+ return false;
+ } else {
+ wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n");
+ }
+
+ chmod( $this->mSavedFile, 0644 );
+ return true;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing
+ * after the user has confirmed it.
+ *
+ * If the user doesn't explicitly cancel or accept, these files
+ * can accumulate in the temp directory.
+ *
+ * @param string $saveName - the destination filename
+ * @param string $tempName - the source temporary file to save
+ * @return string - full path the stashed file, or false on failure
+ * @access private
+ */
+ function saveTempUploadedFile( $saveName, $tempName ) {
+ global $wgOut;
+ $archive = wfImageArchiveDir( $saveName, 'temp' );
+ if ( !is_dir ( $archive ) ) wfMkdirParents( $archive );
+ $stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName;
+
+ $success = $this->mRemoveTempFile
+ ? rename( $tempName, $stash )
+ : move_uploaded_file( $tempName, $stash );
+ if ( !$success ) {
+ $wgOut->showFileCopyError( $tempName, $stash );
+ return false;
+ }
+
+ return $stash;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing,
+ * and save the necessary descriptive info into the session.
+ * Returns a key value which will be passed through a form
+ * to pick up the path info on a later invocation.
+ *
+ * @return int
+ * @access private
+ */
+ function stashSession() {
+ $stash = $this->saveTempUploadedFile(
+ $this->mUploadSaveName, $this->mUploadTempName );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mUploadTempName' => $stash,
+ 'mUploadSize' => $this->mUploadSize,
+ 'mOname' => $this->mOname );
+ return $key;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ wfSuppressWarnings();
+ $success = unlink( $this->mUploadTempName );
+ wfRestoreWarnings();
+ if ( ! $success ) {
+ $wgOut->showFileDeleteError( $this->mUploadTempName );
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * Show some text and linkage on successful upload.
+ * @access private
+ */
+ function showSuccess() {
+ global $wgUser, $wgOut, $wgContLang;
+
+ $sk = $wgUser->getSkin();
+ $ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) );
+ $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName;
+ $dlink = $sk->makeKnownLink( $dname, $dname );
+
+ $wgOut->addHTML( '<h2>' . wfMsgHtml( 'successfulupload' ) . "</h2>\n" );
+ $text = wfMsgWikiHtml( 'fileuploaded', $ilink, $dlink );
+ $wgOut->addHTML( $text );
+ $wgOut->returnToMain( false );
+ }
+
+ /**
+ * @param string $error as HTML
+ * @access private
+ */
+ function uploadError( $error ) {
+ global $wgOut;
+ $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+ $wgOut->addHTML( "<span class='error'>{$error}</span>\n" );
+ }
+
+ /**
+ * There's something wrong with this file, not enough to reject it
+ * totally but we require manual intervention to save it for real.
+ * Stash it away, then present a form asking to confirm or cancel.
+ *
+ * @param string $warning as HTML
+ * @access private
+ */
+ function uploadWarning( $warning ) {
+ global $wgOut;
+ global $wgUseCopyrightUpload;
+
+ $this->mSessionKey = $this->stashSession();
+ if( !$this->mSessionKey ) {
+ # Couldn't save file; an error has been displayed so let's go.
+ return;
+ }
+
+ $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+ $wgOut->addHTML( "<ul class='warning'>{$warning}</ul><br />\n" );
+
+ $save = wfMsgHtml( 'savefile' );
+ $reupload = wfMsgHtml( 'reupload' );
+ $iw = wfMsgWikiHtml( 'ignorewarning' );
+ $reup = wfMsgWikiHtml( 'reuploaddesc' );
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $action = $titleObj->escapeLocalURL( 'action=submit' );
+
+ if ( $wgUseCopyrightUpload )
+ {
+ $copyright = "
+ <input type='hidden' name='wpUploadCopyStatus' value=\"" . htmlspecialchars( $this->mUploadCopyStatus ) . "\" />
+ <input type='hidden' name='wpUploadSource' value=\"" . htmlspecialchars( $this->mUploadSource ) . "\" />
+ ";
+ } else {
+ $copyright = "";
+ }
+
+ $wgOut->addHTML( "
+ <form id='uploadwarning' method='post' enctype='multipart/form-data' action='$action'>
+ <input type='hidden' name='wpIgnoreWarning' value='1' />
+ <input type='hidden' name='wpSessionKey' value=\"" . htmlspecialchars( $this->mSessionKey ) . "\" />
+ <input type='hidden' name='wpUploadDescription' value=\"" . htmlspecialchars( $this->mUploadDescription ) . "\" />
+ <input type='hidden' name='wpLicense' value=\"" . htmlspecialchars( $this->mLicense ) . "\" />
+ <input type='hidden' name='wpDestFile' value=\"" . htmlspecialchars( $this->mDestFile ) . "\" />
+ <input type='hidden' name='wpWatchthis' value=\"" . htmlspecialchars( intval( $this->mWatchthis ) ) . "\" />
+ {$copyright}
+ <table border='0'>
+ <tr>
+ <tr>
+ <td align='right'>
+ <input tabindex='2' type='submit' name='wpUpload' value=\"$save\" />
+ </td>
+ <td align='left'>$iw</td>
+ </tr>
+ <tr>
+ <td align='right'>
+ <input tabindex='2' type='submit' name='wpReUpload' value=\"{$reupload}\" />
+ </td>
+ <td align='left'>$reup</td>
+ </tr>
+ </tr>
+ </table></form>\n" );
+ }
+
+ /**
+ * Displays the main upload form, optionally with a highlighted
+ * error message up at the top.
+ *
+ * @param string $msg as HTML
+ * @access private
+ */
+ function mainUploadForm( $msg='' ) {
+ global $wgOut, $wgUser;
+ global $wgUseCopyrightUpload;
+
+ $cols = intval($wgUser->getOption( 'cols' ));
+ $ew = $wgUser->getOption( 'editwidth' );
+ if ( $ew ) $ew = " style=\"width:100%\"";
+ else $ew = '';
+
+ if ( '' != $msg ) {
+ $sub = wfMsgHtml( 'uploaderror' );
+ $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
+ "<span class='error'>{$msg}</span>\n" );
+ }
+ $wgOut->addHTML( '<div id="uploadtext">' );
+ $wgOut->addWikiText( wfMsg( 'uploadtext' ) );
+ $wgOut->addHTML( '</div>' );
+ $sk = $wgUser->getSkin();
+
+
+ $sourcefilename = wfMsgHtml( 'sourcefilename' );
+ $destfilename = wfMsgHtml( 'destfilename' );
+ $summary = wfMsgWikiHtml( 'fileuploadsummary' );
+
+ $licenses = new Licenses();
+ $license = wfMsgHtml( 'license' );
+ $nolicense = wfMsgHtml( 'nolicense' );
+ $licenseshtml = $licenses->getHtml();
+
+ $ulb = wfMsgHtml( 'uploadbtn' );
+
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' );
+ $action = $titleObj->escapeLocalURL();
+
+ $encDestFile = htmlspecialchars( $this->mDestFile );
+
+ $watchChecked = $wgUser->getOption( 'watchdefault' )
+ ? 'checked="checked"'
+ : '';
+
+ $wgOut->addHTML( "
+ <form id='upload' method='post' enctype='multipart/form-data' action=\"$action\">
+ <table border='0'>
+ <tr>
+ <td align='right'><label for='wpUploadFile'>{$sourcefilename}:</label></td>
+ <td align='left'>
+ <input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . ($this->mDestFile?"":"onchange='fillDestFilename()' ") . "size='40' />
+ </td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpDestFile'>{$destfilename}:</label></td>
+ <td align='left'>
+ <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='40' value=\"$encDestFile\" />
+ </td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpUploadDescription'>{$summary}</label></td>
+ <td align='left'>
+ <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' cols='{$cols}'{$ew}>" . htmlspecialchars( $this->mUploadDescription ) . "</textarea>
+ </td>
+ </tr>
+ <tr>" );
+
+ if ( $licenseshtml != '' ) {
+ global $wgStylePath;
+ $wgOut->addHTML( "
+ <td align='right'><label for='wpLicense'>$license:</label></td>
+ <td align='left'>
+ <script type='text/javascript' src=\"$wgStylePath/common/upload.js\"></script>
+ <select name='wpLicense' id='wpLicense' tabindex='4'
+ onchange='licenseSelectorCheck()'>
+ <option value=''>$nolicense</option>
+ $licenseshtml
+ </select>
+ </td>
+ </tr>
+ <tr>
+ ");
+ }
+
+ if ( $wgUseCopyrightUpload ) {
+ $filestatus = wfMsgHtml ( 'filestatus' );
+ $copystatus = htmlspecialchars( $this->mUploadCopyStatus );
+ $filesource = wfMsgHtml ( 'filesource' );
+ $uploadsource = htmlspecialchars( $this->mUploadSource );
+
+ $wgOut->addHTML( "
+ <td align='right' nowrap='nowrap'><label for='wpUploadCopyStatus'>$filestatus:</label></td>
+ <td><input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus' value=\"$copystatus\" size='40' /></td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpUploadCopyStatus'>$filesource:</label></td>
+ <td><input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus' value=\"$uploadsource\" size='40' /></td>
+ </tr>
+ <tr>
+ ");
+ }
+
+
+ $wgOut->addHtml( "
+ <td></td>
+ <td>
+ <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
+ <label for='wpWatchthis'>" . wfMsgHtml( 'watchthis' ) . "</label>
+ <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' />
+ <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
+ </td>
+ </tr>
+ <tr>
+
+ </tr>
+ <tr>
+ <td></td>
+ <td align='left'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\" /></td>
+ </tr>
+
+ <tr>
+ <td></td>
+ <td align='left'>
+ " );
+ $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
+ $wgOut->addHTML( "
+ </td>
+ </tr>
+
+ </table>
+ </form>" );
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * Split a file into a base name and all dot-delimited 'extensions'
+ * on the end. Some web server configurations will fall back to
+ * earlier pseudo-'extensions' to determine type and execute
+ * scripts, so the blacklist needs to check them all.
+ *
+ * @return array
+ */
+ function splitExtensions( $filename ) {
+ $bits = explode( '.', $filename );
+ $basename = array_shift( $bits );
+ return array( $basename, $bits );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if the extension is in the list.
+ *
+ * @param string $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtension( $ext, $list ) {
+ return in_array( strtolower( $ext ), $list );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if any of the extensions are in the list.
+ *
+ * @param array $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtensionList( $ext, $list ) {
+ foreach( $ext as $e ) {
+ if( in_array( strtolower( $e ), $list ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Verifies that it's ok to include the uploaded file
+ *
+ * @param string $tmpfile the full path of the temporary file to verify
+ * @param string $extension The filename extension that the file is to be served with
+ * @return mixed true of the file is verified, a WikiError object otherwise.
+ */
+ function verify( $tmpfile, $extension ) {
+ #magically determine mime type
+ $magic=& wfGetMimeMagic();
+ $mime= $magic->guessMimeType($tmpfile,false);
+
+ $fname= "SpecialUpload::verify";
+
+ #check mime type, if desired
+ global $wgVerifyMimeType;
+ if ($wgVerifyMimeType) {
+
+ #check mime type against file extension
+ if( !$this->verifyExtension( $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadcorrupt' );
+ }
+
+ #check mime type blacklist
+ global $wgMimeTypeBlacklist;
+ if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
+ && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return new WikiErrorMsg( 'badfiletype', htmlspecialchars( $mime ) );
+ }
+ }
+
+ #check for htmlish code and javascript
+ if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadscripted' );
+ }
+
+ /**
+ * Scan the uploaded file for viruses
+ */
+ $virus= $this->detectVirus($tmpfile);
+ if ( $virus ) {
+ return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
+ }
+
+ wfDebug( "$fname: all clear; passing.\n" );
+ return true;
+ }
+
+ /**
+ * Checks if the mime type of the uploaded file matches the file extension.
+ *
+ * @param string $mime the mime type of the uploaded file
+ * @param string $extension The filename extension that the file is to be served with
+ * @return bool
+ */
+ function verifyExtension( $mime, $extension ) {
+ $fname = 'SpecialUpload::verifyExtension';
+
+ $magic =& wfGetMimeMagic();
+
+ if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+ if ( ! $magic->isRecognizableExtension( $extension ) ) {
+ wfDebug( "$fname: passing file with unknown detected mime type; unrecognized extension '$extension', can't verify\n" );
+ return true;
+ } else {
+ wfDebug( "$fname: rejecting file with unknown detected mime type; recognized extension '$extension', so probably invalid file\n" );
+ return false;
+ }
+
+ $match= $magic->isMatchingExtension($extension,$mime);
+
+ if ($match===NULL) {
+ wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" );
+ return true;
+ } elseif ($match===true) {
+ wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" );
+
+ #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
+ return true;
+
+ } else {
+ wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" );
+ return false;
+ }
+ }
+
+ /** Heuristig for detecting files that *could* contain JavaScript instructions or
+ * things that may look like HTML to a browser and are thus
+ * potentially harmful. The present implementation will produce false positives in some situations.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @param string $mime The mime type of the file
+ * @param string $extension The extension of the file
+ * @return bool true if the file contains something looking like embedded scripts
+ */
+ function detectScript($file, $mime, $extension) {
+ global $wgAllowTitlesInSVG;
+
+ #ugly hack: for text files, always look at the entire file.
+ #For binarie field, just check the first K.
+
+ if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
+ else {
+ $fp = fopen( $file, 'rb' );
+ $chunk = fread( $fp, 1024 );
+ fclose( $fp );
+ }
+
+ $chunk= strtolower( $chunk );
+
+ if (!$chunk) return false;
+
+ #decode from UTF-16 if needed (could be used for obfuscation).
+ if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
+ elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
+ else $enc= NULL;
+
+ if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
+
+ $chunk= trim($chunk);
+
+ #FIXME: convert from UTF-16 if necessarry!
+
+ wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
+
+ #check for HTML doctype
+ if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
+
+ /**
+ * Internet Explorer for Windows performs some really stupid file type
+ * autodetection which can cause it to interpret valid image files as HTML
+ * and potentially execute JavaScript, creating a cross-site scripting
+ * attack vectors.
+ *
+ * Apple's Safari browser also performs some unsafe file type autodetection
+ * which can cause legitimate files to be interpreted as HTML if the
+ * web server is not correctly configured to send the right content-type
+ * (or if you're really uploading plain text and octet streams!)
+ *
+ * Returns true if IE is likely to mistake the given file for HTML.
+ * Also returns true if Safari would mistake the given file for HTML
+ * when served with a generic content-type.
+ */
+
+ $tags = array(
+ '<body',
+ '<head',
+ '<html', #also in safari
+ '<img',
+ '<pre',
+ '<script', #also in safari
+ '<table'
+ );
+ if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
+ $tags[] = '<title';
+ }
+
+ foreach( $tags as $tag ) {
+ if( false !== strpos( $chunk, $tag ) ) {
+ return true;
+ }
+ }
+
+ /*
+ * look for javascript
+ */
+
+ #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
+ $chunk = Sanitizer::decodeCharReferences( $chunk );
+
+ #look for script-types
+ if (preg_match("!type\s*=\s*['\"]?\s*(\w*/)?(ecma|java)!sim",$chunk)) return true;
+
+ #look for html-style script-urls
+ if (preg_match("!(href|src|data)\s*=\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true;
+
+ #look for css-style script-urls
+ if (preg_match("!url\s*\(\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true;
+
+ wfDebug("SpecialUpload::detectScript: no scripts found\n");
+ return false;
+ }
+
+ /** Generic wrapper function for a virus scanner program.
+ * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
+ * $wgAntivirusRequired may be used to deny upload if the scan fails.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
+ * or a string containing feedback from the virus scanner if a virus was found.
+ * If textual feedback is missing but a virus was found, this function returns true.
+ */
+ function detectVirus($file) {
+ global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
+
+ $fname= "SpecialUpload::detectVirus";
+
+ if (!$wgAntivirus) { #disabled?
+ wfDebug("$fname: virus scanner disabled\n");
+
+ return NULL;
+ }
+
+ if (!$wgAntivirusSetup[$wgAntivirus]) {
+ wfDebug("$fname: unknown virus scanner: $wgAntivirus\n");
+
+ $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" ); #LOCALIZE
+
+ return "unknown antivirus: $wgAntivirus";
+ }
+
+ #look up scanner configuration
+ $virus_scanner= $wgAntivirusSetup[$wgAntivirus]["command"]; #command pattern
+ $virus_scanner_codes= $wgAntivirusSetup[$wgAntivirus]["codemap"]; #exit-code map
+ $msg_pattern= $wgAntivirusSetup[$wgAntivirus]["messagepattern"]; #message pattern
+
+ $scanner= $virus_scanner; #copy, so we can resolve the pattern
+
+ if (strpos($scanner,"%f")===false) $scanner.= " ".wfEscapeShellArg($file); #simple pattern: append file to scan
+ else $scanner= str_replace("%f",wfEscapeShellArg($file),$scanner); #complex pattern: replace "%f" with file to scan
+
+ wfDebug("$fname: running virus scan: $scanner \n");
+
+ #execute virus scanner
+ $code= false;
+
+ #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
+ # that does not seem to be worth the pain.
+ # Ask me (Duesentrieb) about it if it's ever needed.
+ if (wfIsWindows()) exec("$scanner",$output,$code);
+ else exec("$scanner 2>&1",$output,$code);
+
+ $exit_code= $code; #remeber for user feedback
+
+ if ($virus_scanner_codes) { #map exit code to AV_xxx constants.
+ if (isset($virus_scanner_codes[$code])) $code= $virus_scanner_codes[$code]; #explicite mapping
+ else if (isset($virus_scanner_codes["*"])) $code= $virus_scanner_codes["*"]; #fallback mapping
+ }
+
+ if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes)
+ wfDebug("$fname: failed to scan $file (code $exit_code).\n");
+
+ if ($wgAntivirusRequired) return "scan failed (code $exit_code)";
+ else return NULL;
+ }
+ else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune)
+ wfDebug("$fname: unsupported file type $file (code $exit_code).\n");
+ return NULL;
+ }
+ else if ($code===AV_NO_VIRUS) {
+ wfDebug("$fname: file passed virus scan.\n");
+ return false; #no virus found
+ }
+ else {
+ $output= join("\n",$output);
+ $output= trim($output);
+
+ if (!$output) $output= true; #if ther's no output, return true
+ else if ($msg_pattern) {
+ $groups= array();
+ if (preg_match($msg_pattern,$output,$groups)) {
+ if ($groups[1]) $output= $groups[1];
+ }
+ }
+
+ wfDebug("$fname: FOUND VIRUS! scanner feedback: $output");
+ return $output;
+ }
+ }
+
+ /**
+ * Check if the temporary file is MacBinary-encoded, as some uploads
+ * from Internet Explorer on Mac OS Classic and Mac OS X will be.
+ * If so, the data fork will be extracted to a second temporary file,
+ * which will then be checked for validity and either kept or discarded.
+ *
+ * @access private
+ */
+ function checkMacBinary() {
+ $macbin = new MacBinary( $this->mUploadTempName );
+ if( $macbin->isValid() ) {
+ $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
+ $dataHandle = fopen( $dataFile, 'wb' );
+
+ wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
+ $macbin->extractData( $dataHandle );
+
+ $this->mUploadTempName = $dataFile;
+ $this->mUploadSize = $macbin->dataForkLength();
+
+ // We'll have to manually remove the new file if it's not kept.
+ $this->mRemoveTempFile = true;
+ }
+ $macbin->close();
+ }
+
+ /**
+ * If we've modified the upload file we need to manually remove it
+ * on exit to clean up.
+ * @access private
+ */
+ function cleanupTempFile() {
+ if( $this->mRemoveTempFile && file_exists( $this->mUploadTempName ) ) {
+ wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file $this->mUploadTempName\n" );
+ unlink( $this->mUploadTempName );
+ }
+ }
+
+ /**
+ * Check if there's an overwrite conflict and, if so, if restrictions
+ * forbid this user from performing the upload.
+ *
+ * @return mixed true on success, WikiError on failure
+ * @access private
+ */
+ function checkOverwrite( $name ) {
+ $img = Image::newFromName( $name );
+ if( is_null( $img ) ) {
+ // Uh... this shouldn't happen ;)
+ // But if it does, fall through to previous behavior
+ return false;
+ }
+
+ $error = '';
+ if( $img->exists() ) {
+ global $wgUser, $wgOut;
+ if( $img->isLocal() ) {
+ if( !$wgUser->isAllowed( 'reupload' ) ) {
+ $error = 'fileexists-forbidden';
+ }
+ } else {
+ if( !$wgUser->isAllowed( 'reupload' ) ||
+ !$wgUser->isAllowed( 'reupload-shared' ) ) {
+ $error = "fileexists-shared-forbidden";
+ }
+ }
+ }
+
+ if( $error ) {
+ $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
+ return new WikiError( $wgOut->parse( $errorText ) );
+ }
+
+ // Rockin', go ahead and upload
+ return true;
+ }
+
+}
+?>
diff --git a/includes/SpecialUploadMogile.php b/includes/SpecialUploadMogile.php
new file mode 100644
index 00000000..51a6dd28
--- /dev/null
+++ b/includes/SpecialUploadMogile.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'SpecialUpload.php' );
+require_once( 'MogileFS.php' );
+
+/**
+ * Entry point
+ */
+function wfSpecialUploadMogile() {
+ global $wgRequest;
+ $form = new UploadFormMogile( $wgRequest );
+ $form->execute();
+}
+
+/** @package MediaWiki */
+class UploadFormMogile extends UploadForm {
+ /**
+ * Move the uploaded file from its temporary location to the final
+ * destination. If a previous version of the file exists, move
+ * it into the archive subdirectory.
+ *
+ * @todo If the later save fails, we may have disappeared the original file.
+ *
+ * @param string $saveName
+ * @param string $tempName full path to the temporary file
+ * @param bool $useRename Not used in this implementation
+ */
+ function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
+ global $wgOut;
+ $mfs = MogileFS::NewMogileFS();
+
+ $this->mSavedFile = "image!{$saveName}";
+
+ if( $mfs->getPaths( $this->mSavedFile )) {
+ $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
+ if( !$mfs->rename( $this->mSavedFile, "archive!{$this->mUploadOldVersion}" ) ) {
+ $wgOut->showFileRenameError( $this->mSavedFile,
+ "archive!{$this->mUploadOldVersion}" );
+ return false;
+ }
+ } else {
+ $this->mUploadOldVersion = '';
+ }
+
+ if ( $this->mStashed ) {
+ if (!$mfs->rename($tempName,$this->mSavedFile)) {
+ $wgOut->showFileRenameError($tempName, $this->mSavedFile );
+ return false;
+ }
+ } else {
+ if ( !$mfs->saveFile($this->mSavedFile,'normal',$tempName )) {
+ $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
+ return false;
+ }
+ unlink($tempName);
+ }
+ return true;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing
+ * after the user has confirmed it.
+ *
+ * If the user doesn't explicitly cancel or accept, these files
+ * can accumulate in the temp directory.
+ *
+ * @param string $saveName - the destination filename
+ * @param string $tempName - the source temporary file to save
+ * @return string - full path the stashed file, or false on failure
+ * @access private
+ */
+ function saveTempUploadedFile( $saveName, $tempName ) {
+ global $wgOut;
+
+ $stash = 'stash!' . gmdate( "YmdHis" ) . '!' . $saveName;
+ $mfs = MogileFS::NewMogileFS();
+ if ( !$mfs->saveFile( $stash, 'normal', $tempName ) ) {
+ $wgOut->showFileCopyError( $tempName, $stash );
+ return false;
+ }
+ unlink($tempName);
+ return $stash;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing,
+ * and save the necessary descriptive info into the session.
+ * Returns a key value which will be passed through a form
+ * to pick up the path info on a later invocation.
+ *
+ * @return int
+ * @access private
+ */
+ function stashSession() {
+ $stash = $this->saveTempUploadedFile(
+ $this->mUploadSaveName, $this->mUploadTempName );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mUploadTempName' => $stash,
+ 'mUploadSize' => $this->mUploadSize,
+ 'mOname' => $this->mOname );
+ return $key;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ $mfs = MogileFS::NewMogileFS();
+ if ( ! $mfs->delete( $this->mUploadTempName ) ) {
+ $wgOut->showFileDeleteError( $this->mUploadTempName );
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+?>
diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php
new file mode 100644
index 00000000..4ee35b1b
--- /dev/null
+++ b/includes/SpecialUserlogin.php
@@ -0,0 +1,671 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogin() {
+ global $wgCommandLineMode;
+ global $wgRequest;
+ if( !$wgCommandLineMode && !isset( $_COOKIE[session_name()] ) ) {
+ User::SetupSession();
+ }
+
+ $form = new LoginForm( $wgRequest );
+ $form->execute();
+}
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class LoginForm {
+ var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
+ var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
+ var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
+
+ /**
+ * Constructor
+ * @param webrequest $request A webrequest object passed by reference
+ */
+ function LoginForm( &$request ) {
+ global $wgLang, $wgAllowRealName, $wgEnableEmail;
+ global $wgAuth;
+
+ $this->mType = $request->getText( 'type' );
+ $this->mName = $request->getText( 'wpName' );
+ $this->mPassword = $request->getText( 'wpPassword' );
+ $this->mRetype = $request->getText( 'wpRetype' );
+ $this->mDomain = $request->getText( 'wpDomain' );
+ $this->mReturnTo = $request->getVal( 'returnto' );
+ $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
+ $this->mPosted = $request->wasPosted();
+ $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
+ $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
+ && $wgEnableEmail;
+ $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
+ && $wgEnableEmail;
+ $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
+ $this->mAction = $request->getVal( 'action' );
+ $this->mRemember = $request->getCheck( 'wpRemember' );
+ $this->mLanguage = $request->getText( 'uselang' );
+
+ if( $wgEnableEmail ) {
+ $this->mEmail = $request->getText( 'wpEmail' );
+ } else {
+ $this->mEmail = '';
+ }
+ if( $wgAllowRealName ) {
+ $this->mRealName = $request->getText( 'wpRealName' );
+ } else {
+ $this->mRealName = '';
+ }
+
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mDomain = 'invaliddomain';
+ }
+ $wgAuth->setDomain( $this->mDomain );
+
+ # When switching accounts, it sucks to get automatically logged out
+ if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
+ $this->mReturnTo = '';
+ }
+ }
+
+ function execute() {
+ if ( !is_null( $this->mCookieCheck ) ) {
+ $this->onCookieRedirectCheck( $this->mCookieCheck );
+ return;
+ } else if( $this->mPosted ) {
+ if( $this->mCreateaccount ) {
+ return $this->addNewAccount();
+ } else if ( $this->mCreateaccountMail ) {
+ return $this->addNewAccountMailPassword();
+ } else if ( $this->mMailmypassword ) {
+ return $this->mailPassword();
+ } else if ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
+ return $this->processLogin();
+ }
+ }
+ $this->mainLoginForm( '' );
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountMailPassword() {
+ global $wgOut;
+
+ if ('' == $this->mEmail) {
+ $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
+ return;
+ }
+
+ $u = $this->addNewaccountInternal();
+
+ if ($u == NULL) {
+ return;
+ }
+
+ $u->saveSettings();
+ $result = $this->mailPasswordInternal($u);
+
+ wfRunHooks( 'AddNewAccount', array( $u ) );
+
+ $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $wgOut->addWikiText( wfMsg( 'accmailtext', $u->getName(), $u->getEmail() ) );
+ $wgOut->returnToMain( false );
+ }
+ $u = 0;
+ }
+
+
+ /**
+ * @private
+ */
+ function addNewAccount() {
+ global $wgUser, $wgEmailAuthentication;
+
+ # Create the account and abort if there's a problem doing so
+ $u = $this->addNewAccountInternal();
+ if( $u == NULL )
+ return;
+
+ # If we showed up language selection links, and one was in use, be
+ # smart (and sensible) and save that language as the user's preference
+ global $wgLoginLanguageSelector;
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $u->setOption( 'language', $this->mLanguage );
+
+ # Save user settings and send out an email authentication message if needed
+ $u->saveSettings();
+ if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) )
+ $u->sendConfirmationMail();
+
+ # If not logged in, assume the new account as the current one and set session cookies
+ # then show a "welcome" message or a "need cookies" message as needed
+ if( $wgUser->isAnon() ) {
+ $wgUser = $u;
+ $wgUser->setCookies();
+ wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+ if( $this->hasSessionCookie() ) {
+ return $this->successfulLogin( wfMsg( 'welcomecreation', $wgUser->getName() ), false );
+ } else {
+ return $this->cookieRedirectCheck( 'new' );
+ }
+ } else {
+ # Confirm that the account was created
+ global $wgOut;
+ $skin = $wgUser->getSkin();
+ $self = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
+ $wgOut->returnToMain( $self->getPrefixedText() );
+ wfRunHooks( 'AddNewAccount', array( $u ) );
+ return true;
+ }
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountInternal() {
+ global $wgUser, $wgOut;
+ global $wgEnableSorbs, $wgProxyWhitelist;
+ global $wgMemc, $wgAccountCreationThrottle, $wgDBname;
+ global $wgAuth, $wgMinimalPasswordLength, $wgReservedUsernames;
+
+ // If the user passes an invalid domain, something is fishy
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return false;
+ }
+
+ // If we are not allowing users to login locally, we should
+ // be checking to see if the user is actually able to
+ // authenticate to the authentication server before they
+ // create an account (otherwise, they can create a local account
+ // and login as any domain user). We only need to check this for
+ // domains that aren't local.
+ if( 'local' != $this->mDomain && '' != $this->mDomain ) {
+ if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return false;
+ }
+ }
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return false;
+ }
+
+ if (!$wgUser->isAllowedToCreateAccount()) {
+ $this->userNotPrivilegedMessage();
+ return false;
+ }
+
+ $ip = wfGetIP();
+ if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
+ $wgUser->inSorbsBlacklist( $ip ) )
+ {
+ $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
+ return;
+ }
+
+ $name = trim( $this->mName );
+ $u = User::newFromName( $name );
+ if ( is_null( $u ) || in_array( $u->getName(), $wgReservedUsernames ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return false;
+ }
+
+ if ( 0 != $u->idForName() ) {
+ $this->mainLoginForm( wfMsg( 'userexists' ) );
+ return false;
+ }
+
+ if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
+ $this->mainLoginForm( wfMsg( 'badretype' ) );
+ return false;
+ }
+
+ if ( !$wgUser->isValidPassword( $this->mPassword ) ) {
+ $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
+ return false;
+ }
+
+ if ( $wgAccountCreationThrottle ) {
+ $key = $wgDBname.':acctcreate:ip:'.$ip;
+ $value = $wgMemc->incr( $key );
+ if ( !$value ) {
+ $wgMemc->set( $key, 1, 86400 );
+ }
+ if ( $value > $wgAccountCreationThrottle ) {
+ $this->throttleHit( $wgAccountCreationThrottle );
+ return false;
+ }
+ }
+
+ $abortError = '';
+ if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
+ // Hook point to add extra creation throttles and blocks
+ wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+ $this->mainLoginForm( $abortError );
+ return false;
+ }
+
+ if( !$wgAuth->addUser( $u, $this->mPassword ) ) {
+ $this->mainLoginForm( wfMsg( 'externaldberror' ) );
+ return false;
+ }
+
+ # Update user count
+ $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate->doUpdate();
+
+ return $this->initUser( $u );
+ }
+
+ /**
+ * Actually add a user to the database.
+ * Give it a User object that has been initialised with a name.
+ *
+ * @param $u User object.
+ * @return User object.
+ * @private
+ */
+ function &initUser( &$u ) {
+ $u->addToDatabase();
+ $u->setPassword( $this->mPassword );
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
+ $u->setToken();
+
+ global $wgAuth;
+ $wgAuth->initUser( $u );
+
+ $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+
+ return $u;
+ }
+
+ /**
+ * @private
+ */
+ function processLogin() {
+ global $wgUser, $wgAuth, $wgReservedUsernames;
+
+ if ( '' == $this->mName ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) || in_array( $u->getName(), $wgReservedUsernames ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ if ( 0 == $u->getID() ) {
+ global $wgAuth;
+ /**
+ * If the external authentication plugin allows it,
+ * automatically create a new account for users that
+ * are externally defined but have not yet logged in.
+ */
+ if ( $wgAuth->autoCreate() && $wgAuth->userExists( $u->getName() ) ) {
+ if ( $wgAuth->authenticate( $u->getName(), $this->mPassword ) ) {
+ $u =& $this->initUser( $u );
+ } else {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return;
+ }
+ } else {
+ $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
+ return;
+ }
+ } else {
+ $u->loadFromDatabase();
+ }
+
+ if (!$u->checkPassword( $this->mPassword )) {
+ $this->mainLoginForm( wfMsg( $this->mPassword == '' ? 'wrongpasswordempty' : 'wrongpassword' ) );
+ return;
+ }
+
+ # We've verified now, update the real record
+ #
+ if ( $this->mRemember ) {
+ $r = 1;
+ } else {
+ $r = 0;
+ }
+ $u->setOption( 'rememberpassword', $r );
+
+ $wgAuth->updateUser( $u );
+
+ $wgUser = $u;
+ $wgUser->setCookies();
+
+ $wgUser->saveSettings();
+
+ if( $this->hasSessionCookie() ) {
+ return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) );
+ } else {
+ return $this->cookieRedirectCheck( 'login' );
+ }
+ }
+
+ /**
+ * @private
+ */
+ function mailPassword() {
+ global $wgUser, $wgOut;
+
+ # Check against the rate limiter
+ if( $wgUser->pingLimiter( 'mailpassword' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ if ( '' == $this->mName ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ if ( 0 == $u->getID() ) {
+ $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
+ return;
+ }
+
+ $u->loadFromDatabase();
+
+ $result = $this->mailPasswordInternal( $u );
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
+ }
+ }
+
+
+ /**
+ * @return mixed true on success, WikiError on failure
+ * @private
+ */
+ function mailPasswordInternal( $u ) {
+ global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
+ global $wgServer, $wgScript;
+
+ if ( '' == $u->getEmail() ) {
+ return wfMsg( 'noemail', $u->getName() );
+ }
+
+ $np = $u->randomPassword();
+ $u->setNewpassword( $np );
+
+ setcookie( "{$wgCookiePrefix}Token", '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+
+ $u->saveSettings();
+
+ $ip = wfGetIP();
+ if ( '' == $ip ) { $ip = '(Unknown)'; }
+
+ $m = wfMsg( 'passwordremindertext', $ip, $u->getName(), $np, $wgServer . $wgScript );
+
+ $result = $u->sendMail( wfMsg( 'passwordremindertitle' ), $m );
+ return $result;
+ }
+
+
+ /**
+ * @param string $msg Message that will be shown on success
+ * @param bool $auto Toggle auto-redirect to main page; default true
+ * @private
+ */
+ function successfulLogin( $msg, $auto = true ) {
+ global $wgUser;
+ global $wgOut;
+
+ # Run any hooks; ignore results
+
+ wfRunHooks('UserLoginComplete', array(&$wgUser));
+
+ $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->addWikiText( $msg );
+ if ( !empty( $this->mReturnTo ) ) {
+ $wgOut->returnToMain( $auto, $this->mReturnTo );
+ } else {
+ $wgOut->returnToMain( $auto );
+ }
+ }
+
+ /** */
+ function userNotPrivilegedMessage() {
+ global $wgOut;
+
+ $wgOut->setPageTitle( wfMsg( 'whitelistacctitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ $wgOut->addWikiText( wfMsg( 'whitelistacctext' ) );
+
+ $wgOut->returnToMain( false );
+ }
+
+ /**
+ * @private
+ */
+ function mainLoginForm( $msg, $msgtype = 'error' ) {
+ global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
+ global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
+
+ if ( $this->mType == 'signup' && !$wgUser->isAllowedToCreateAccount() ) {
+ $this->userNotPrivilegedMessage();
+ return;
+ }
+
+ if ( '' == $this->mName ) {
+ if ( $wgUser->isLoggedIn() ) {
+ $this->mName = $wgUser->getName();
+ } else {
+ $this->mName = @$_COOKIE[$wgCookiePrefix.'UserName'];
+ }
+ }
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+
+ require_once( 'SkinTemplate.php' );
+ require_once( 'templates/Userlogin.php' );
+
+ if ( $this->mType == 'signup' ) {
+ $template =& new UsercreateTemplate();
+ $q = 'action=submitlogin&type=signup';
+ $linkq = 'type=login';
+ $linkmsg = 'gotaccount';
+ } else {
+ $template =& new UserloginTemplate();
+ $q = 'action=submitlogin&type=login';
+ $linkq = 'type=signup';
+ $linkmsg = 'nologin';
+ }
+
+ if ( !empty( $this->mReturnTo ) ) {
+ $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+ $q .= $returnto;
+ $linkq .= $returnto;
+ }
+
+ # Pass any language selection on to the mode switch link
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $linkq .= '&uselang=' . $this->mLanguage;
+
+ $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
+ $link .= wfMsgHtml( $linkmsg . 'link' );
+ $link .= '</a>';
+
+ # Don't show a "create account" link if the user can't
+ if( $this->showCreateOrLoginLink( $wgUser ) )
+ $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
+ else
+ $template->set( 'link', '' );
+
+ $template->set( 'header', '' );
+ $template->set( 'name', $this->mName );
+ $template->set( 'password', $this->mPassword );
+ $template->set( 'retype', $this->mRetype );
+ $template->set( 'email', $this->mEmail );
+ $template->set( 'realname', $this->mRealName );
+ $template->set( 'domain', $this->mDomain );
+
+ $template->set( 'action', $titleObj->getLocalUrl( $q ) );
+ $template->set( 'message', $msg );
+ $template->set( 'messagetype', $msgtype );
+ $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
+ $template->set( 'userealname', $wgAllowRealName );
+ $template->set( 'useemail', $wgEnableEmail );
+ $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
+
+ # Prepare language selection links as needed
+ if( $wgLoginLanguageSelector ) {
+ $template->set( 'languages', $this->makeLanguageSelector() );
+ if( $this->mLanguage )
+ $template->set( 'uselang', $this->mLanguage );
+ }
+
+ // Give authentication and captcha plugins a chance to modify the form
+ $wgAuth->modifyUITemplate( $template );
+ if ( $this->mType == 'signup' ) {
+ wfRunHooks( 'UserCreateForm', array( &$template ) );
+ } else {
+ wfRunHooks( 'UserLoginForm', array( &$template ) );
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->addTemplate( $template );
+ }
+
+ /**
+ * @private
+ */
+ function showCreateOrLoginLink( &$user ) {
+ if( $this->mType == 'signup' ) {
+ return( true );
+ } elseif( $user->isAllowedToCreateAccount() ) {
+ return( true );
+ } else {
+ return( false );
+ }
+ }
+
+ /**
+ * @private
+ */
+ function hasSessionCookie() {
+ global $wgDisableCookieCheck;
+ return ( $wgDisableCookieCheck ) ? true : ( isset( $_COOKIE[session_name()] ) );
+ }
+
+ /**
+ * @private
+ */
+ function cookieRedirectCheck( $type ) {
+ global $wgOut;
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
+
+ return $wgOut->redirect( $check );
+ }
+
+ /**
+ * @private
+ */
+ function onCookieRedirectCheck( $type ) {
+ global $wgUser;
+
+ if ( !$this->hasSessionCookie() ) {
+ if ( $type == 'new' ) {
+ return $this->mainLoginForm( wfMsg( 'nocookiesnew' ) );
+ } else if ( $type == 'login' ) {
+ return $this->mainLoginForm( wfMsg( 'nocookieslogin' ) );
+ } else {
+ # shouldn't happen
+ return $this->mainLoginForm( wfMsg( 'error' ) );
+ }
+ } else {
+ return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) );
+ }
+ }
+
+ /**
+ * @private
+ */
+ function throttleHit( $limit ) {
+ global $wgOut;
+
+ $wgOut->addWikiText( wfMsg( 'acct_creation_throttle_hit', $limit ) );
+ }
+
+ /**
+ * Produce a bar of links which allow the user to select another language
+ * during login/registration but retain "returnto"
+ *
+ * @return string
+ */
+ function makeLanguageSelector() {
+ $msg = wfMsgForContent( 'loginlanguagelinks' );
+ if( $msg != '' && $msg != '&lt;loginlanguagelinks&gt;' ) {
+ $langs = explode( "\n", $msg );
+ $links = array();
+ foreach( $langs as $lang ) {
+ $lang = trim( $lang, '* ' );
+ $parts = explode( '|', $lang );
+ $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+ }
+ return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Create a language selector link for a particular language
+ * Links back to this page preserving type and returnto
+ *
+ * @param $text Link text
+ * @param $lang Language code
+ */
+ function makeLanguageSelectorLink( $text, $lang ) {
+ global $wgUser;
+ $self = Title::makeTitle( NS_SPECIAL, 'Userlogin' );
+ $attr[] = 'uselang=' . $lang;
+ if( $this->mType == 'signup' )
+ $attr[] = 'type=signup';
+ if( $this->mReturnTo )
+ $attr[] = 'returnto=' . $this->mReturnTo;
+ $skin =& $wgUser->getSkin();
+ return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
+ }
+
+}
+?>
diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php
new file mode 100644
index 00000000..f3fcbc4f
--- /dev/null
+++ b/includes/SpecialUserlogout.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogout() {
+ global $wgUser, $wgOut;
+
+ if (wfRunHooks('UserLogout', array(&$wgUser))) {
+
+ $wgUser->logout();
+
+ wfRunHooks('UserLogoutComplete', array(&$wgUser));
+
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) );
+ $wgOut->returnToMain();
+
+ }
+}
+
+?>
diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php
new file mode 100644
index 00000000..8f43092c
--- /dev/null
+++ b/includes/SpecialUserrights.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Provide an administration interface
+ * DO NOT USE: INSECURE.
+ *
+ * TODO : remove everything related to group editing (SpecialGrouplevels.php)
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/** */
+require_once('HTMLForm.php');
+
+/** Entry point */
+function wfSpecialUserrights() {
+ global $wgRequest;
+ $form = new UserrightsForm($wgRequest);
+ $form->execute();
+}
+
+/**
+ * A class to manage user levels rights.
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class UserrightsForm extends HTMLForm {
+ var $mPosted, $mRequest, $mSaveprefs;
+ /** Escaped local url name*/
+ var $action;
+
+ /** Constructor*/
+ function UserrightsForm ( &$request ) {
+ $this->mPosted = $request->wasPosted();
+ $this->mRequest =& $request;
+ $this->mName = 'userrights';
+
+ $titleObj = Title::makeTitle( NS_SPECIAL, 'Userrights' );
+ $this->action = $titleObj->escapeLocalURL();
+ }
+
+ /**
+ * Manage forms to be shown according to posted data.
+ * Depending on the submit button used, call a form or a save function.
+ */
+ function execute() {
+ // show the general form
+ $this->switchForm();
+ if( $this->mPosted ) {
+ // show some more forms
+ if( $this->mRequest->getCheck( 'ssearchuser' ) ) {
+ $this->editUserGroupsForm( $this->mRequest->getVal( 'user-editname' ) );
+ }
+
+ // save settings
+ if( $this->mRequest->getCheck( 'saveusergroups' ) ) {
+ global $wgUser;
+ $username = $this->mRequest->getVal( 'user-editname' );
+ if( $wgUser->matchEditToken( $this->mRequest->getVal( 'wpEditToken' ), $username ) ) {
+ $this->saveUserGroups( $username,
+ $this->mRequest->getArray( 'member' ),
+ $this->mRequest->getArray( 'available' ) );
+ }
+ }
+ }
+ }
+
+ /**
+ * Save user groups changes in the database.
+ * Data comes from the editUserGroupsForm() form function
+ *
+ * @param string $username Username to apply changes to.
+ * @param array $removegroup id of groups to be removed.
+ * @param array $addgroup id of groups to be added.
+ *
+ */
+ function saveUserGroups( $username, $removegroup, $addgroup) {
+ global $wgOut;
+ $u = User::newFromName($username);
+
+ if(is_null($u)) {
+ $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) );
+ return;
+ }
+
+ if($u->getID() == 0) {
+ $wgOut->addWikiText( wfMsg( 'nosuchusershort', htmlspecialchars( $username ) ) );
+ return;
+ }
+
+ $oldGroups = $u->getGroups();
+ $newGroups = $oldGroups;
+ $logcomment = ' ';
+ // remove then add groups
+ if(isset($removegroup)) {
+ $newGroups = array_diff($newGroups, $removegroup);
+ foreach( $removegroup as $group ) {
+ $u->removeGroup( $group );
+ }
+ }
+ if(isset($addgroup)) {
+ $newGroups = array_merge($newGroups, $addgroup);
+ foreach( $addgroup as $group ) {
+ $u->addGroup( $group );
+ }
+ }
+ $newGroups = array_unique( $newGroups );
+
+ wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
+ wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
+
+ wfRunHooks( 'UserRights', array( &$u, $addgroup, $removegroup ) );
+ $log = new LogPage( 'rights' );
+ $log->addEntry( 'rights', Title::makeTitle( NS_USER, $u->getName() ), '', array( $this->makeGroupNameList( $oldGroups ),
+ $this->makeGroupNameList( $newGroups ) ) );
+ }
+
+ function makeGroupNameList( $ids ) {
+ return implode( ', ', $ids );
+ }
+
+ /**
+ * The entry form
+ * It allows a user to look for a username and edit its groups membership
+ */
+ function switchForm() {
+ global $wgOut;
+
+ // user selection
+ $wgOut->addHTML( "<form name=\"uluser\" action=\"$this->action\" method=\"post\">\n" );
+ $wgOut->addHTML( $this->fieldset( 'lookup-user',
+ $this->textbox( 'user-editname' ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'name' => 'ssearchuser',
+ 'value' => wfMsg( 'editusergroup' ) ) )
+ ));
+ $wgOut->addHTML( "</form>\n" );
+ }
+
+ /**
+ * Edit user groups membership
+ * @param string $username Name of the user.
+ */
+ function editUserGroupsForm($username) {
+ global $wgOut, $wgUser;
+
+ $user = User::newFromName($username);
+ if( is_null( $user ) ) {
+ $wgOut->addWikiText( wfMsg( 'nouserspecified' ) );
+ return;
+ } elseif( $user->getID() == 0 ) {
+ $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) );
+ return;
+ }
+
+ $groups = $user->getGroups();
+
+ $wgOut->addHTML( "<form name=\"editGroup\" action=\"$this->action\" method=\"post\">\n".
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'user-editname',
+ 'value' => $username ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $wgUser->editToken( $username ) ) ) .
+ $this->fieldset( 'editusergroup',
+ $wgOut->parse( wfMsg('editing', $username ) ) .
+ '<table border="0" align="center"><tr><td>'.
+ HTMLSelectGroups('member', $this->mName.'-groupsmember', $groups,true,6).
+ '</td><td>'.
+ HTMLSelectGroups('available', $this->mName.'-groupsavailable', $groups,true,6,true).
+ '</td></tr></table>'."\n".
+ $wgOut->parse( wfMsg('userrights-groupshelp') ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'name' => 'saveusergroups',
+ 'value' => wfMsg( 'saveusergroups' ) ) )
+ ));
+ $wgOut->addHTML( "</form>\n" );
+ }
+} // end class UserrightsForm
+?>
diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php
new file mode 100644
index 00000000..5f7e857f
--- /dev/null
+++ b/includes/SpecialVersion.php
@@ -0,0 +1,270 @@
+<?php
+/**#@+
+ * Give information about the version of MediaWiki, PHP, the DB and extensions
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @bug 2019, 4531
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialVersion() {
+ $version = new SpecialVersion;
+ $version->execute();
+}
+
+class SpecialVersion {
+ /**
+ * main()
+ */
+ function execute() {
+ global $wgOut;
+
+ $wgOut->addHTML( '<div dir="ltr">' );
+ $wgOut->addWikiText(
+ $this->MediaWikiCredits() .
+ $this->extensionCredits() .
+ $this->wgHooks()
+ );
+ $wgOut->addHTML( $this->IPInfo() );
+ $wgOut->addHTML( '</div>' );
+ }
+
+ /**#@+
+ * @private
+ */
+
+ /**
+ * @static
+ */
+ function MediaWikiCredits() {
+ $version = $this->getVersion();
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $ret =
+ "__NOTOC__
+ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
+ copyright (C) 2001-2006 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+ Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason,
+ Niklas Laxström, Domas Mituzas, Rob Church and others.
+
+ MediaWiki 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.
+
+ MediaWiki 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 [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ or [http://www.gnu.org/copyleft/gpl.html read it online]
+
+ * [http://www.mediawiki.org/ MediaWiki]: $version
+ * [http://www.php.net/ PHP]: " . phpversion() . " (" . php_sapi_name() . ")
+ * " . $dbr->getSoftwareLink() . ": " . $dbr->getServerVersion();
+
+ return str_replace( "\t\t", '', $ret );
+ }
+
+ function getVersion() {
+ global $wgVersion, $IP;
+ $svn = $this->getSvnRevision( $IP );
+ return $svn ? "$wgVersion (r$svn)" : $wgVersion;
+ }
+
+ function extensionCredits() {
+ global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunction;
+
+ if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunction ) )
+ return '';
+
+ $extensionTypes = array(
+ 'specialpage' => 'Special pages',
+ 'parserhook' => 'Parser hooks',
+ 'variable' => 'Variables',
+ 'other' => 'Other',
+ );
+ wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
+
+ $out = "\n* Extensions:\n";
+ foreach ( $extensionTypes as $type => $text ) {
+ if ( count( @$wgExtensionCredits[$type] ) ) {
+ $out .= "** $text:\n";
+
+ usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
+
+ foreach ( $wgExtensionCredits[$type] as $extension ) {
+ wfSuppressWarnings();
+ $out .= $this->formatCredits(
+ $extension['name'],
+ $extension['version'],
+ $extension['author'],
+ $extension['url'],
+ $extension['description']
+ );
+ wfRestoreWarnings();
+ }
+ }
+ }
+
+ if ( count( $wgExtensionFunctions ) ) {
+ $out .= "** Extension functions:\n";
+ $out .= '***' . $this->listToText( $wgExtensionFunctions ) . "\n";
+ }
+
+ if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
+ for ( $i = 0; $i < $cnt; ++$i )
+ $tags[$i] = "&lt;{$tags[$i]}&gt;";
+ $out .= "** Parser extension tags:\n";
+ $out .= '***' . $this->listToText( $tags ). "\n";
+ }
+
+ if ( count( $wgSkinExtensionFunction ) ) {
+ $out .= "** Skin extension functions:\n";
+ $out .= '***' . $this->listToText( $wgSkinExtensionFunction ) . "\n";
+ }
+
+ return $out;
+ }
+
+ function compare( $a, $b ) {
+ if ( $a['name'] === $b['name'] )
+ return 0;
+ else
+ return LanguageUtf8::lc( $a['name'] ) > LanguageUtf8::lc( $b['name'] ) ? 1 : -1;
+ }
+
+ function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) {
+ $ret = '*** ';
+ if ( isset( $url ) )
+ $ret .= "[$url ";
+ $ret .= "''$name";
+ if ( isset( $version ) )
+ $ret .= " (version $version)";
+ $ret .= "''";
+ if ( isset( $url ) )
+ $ret .= ']';
+ if ( isset( $description ) )
+ $ret .= ', ' . $description;
+ if ( isset( $description ) && isset( $author ) )
+ $ret .= ', ';
+ if ( isset( $author ) )
+ $ret .= ' by ' . $this->listToText( (array)$author );
+
+ return "$ret\n";
+ }
+
+ /**
+ * @return string
+ */
+ function wgHooks() {
+ global $wgHooks;
+
+ if ( count( $wgHooks ) ) {
+ $myWgHooks = $wgHooks;
+ ksort( $myWgHooks );
+
+ $ret = "* Hooks:\n";
+ foreach ($myWgHooks as $hook => $hooks)
+ $ret .= "** $hook: " . $this->listToText( $hooks ) . "\n";
+
+ return $ret;
+ } else
+ return '';
+ }
+
+ /**
+ * @static
+ *
+ * @return string
+ */
+ function IPInfo() {
+ $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
+ return "<!-- visited from $ip -->\n" .
+ "<span style='display:none'>visited from $ip</span>";
+ }
+
+ /**
+ * @param array $list
+ * @return string
+ */
+ function listToText( $list ) {
+ $cnt = count( $list );
+
+ if ( $cnt == 1 )
+ // Enforce always returning a string
+ return (string)$this->arrayToString( $list[0] );
+ else {
+ $t = array_slice( $list, 0, $cnt - 1 );
+ $one = array_map( array( &$this, 'arrayToString' ), $t );
+ $two = $this->arrayToString( $list[$cnt - 1] );
+
+ return implode( ', ', $one ) . " and $two";
+ }
+ }
+
+ /**
+ * @static
+ *
+ * @param mixed $list Will convert an array to string if given and return
+ * the paramater unaltered otherwise
+ * @return mixed
+ */
+ function arrayToString( $list ) {
+ if ( ! is_array( $list ) )
+ return $list;
+ else {
+ $class = get_class( $list[0] );
+ return "($class, {$list[1]})";
+ }
+ }
+
+ /**
+ * Retrieve the revision number of a Subversion working directory.
+ *
+ * @param string $dir
+ * @return mixed revision number as int, or false if not a SVN checkout
+ */
+ function getSvnRevision( $dir ) {
+ if( !function_exists( 'simplexml_load_file' ) ) {
+ // We could fall back to expat... YUCK
+ return false;
+ }
+
+ // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
+ $entries = $dir . '/.svn/entries';
+
+ // SimpleXml whines about the xmlns...
+ wfSuppressWarnings();
+ $xml = simplexml_load_file( $entries );
+ wfRestoreWarnings();
+
+ if( $xml ) {
+ foreach( $xml->entry as $entry ) {
+ if( $xml->entry[0]['name'] == '' ) {
+ // The directory entry should always have a revision marker.
+ if( $entry['revision'] ) {
+ return intval( $entry['revision'] );
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**#@-*/
+}
+
+/**#@-*/
+?>
diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php
new file mode 100644
index 00000000..8e75953a
--- /dev/null
+++ b/includes/SpecialWantedcategories.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * A querypage to list the most wanted categories
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ *
+ * @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 2.0 or later
+ */
+
+/**
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class WantedCategoriesPage extends QueryPage {
+
+ function getName() { return 'Wantedcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'categorylinks', 'page' ) );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ."
+ WHERE page_title IS NULL
+ GROUP BY cl_to
+ ";
+ }
+
+ function sortDescending() { return true; }
+
+ /**
+ * Fetch user page links and cache their existence
+ */
+ function preprocessResults( &$db, &$res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) )
+ $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ $batch->execute();
+
+ // Back to start for display
+ if ( $db->numRows( $res ) > 0 )
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+
+ $plink = $this->isCached() ?
+ $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
+ $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new WantedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
+
+?>
diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php
new file mode 100644
index 00000000..8bbe49cb
--- /dev/null
+++ b/includes/SpecialWantedpages.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+class WantedPagesPage extends QueryPage {
+ var $nlinks;
+
+ function WantedPagesPage( $inc = false, $nlinks = true ) {
+ $this->setListoutput( $inc );
+ $this->nlinks = $nlinks;
+ }
+
+ function getName() {
+ return 'Wantedpages';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgWantedPagesThreshold;
+ $count = $wgWantedPagesThreshold - 1;
+ $dbr =& wfGetDB( DB_SLAVE );
+ $pagelinks = $dbr->tableName( 'pagelinks' );
+ $page = $dbr->tableName( 'page' );
+ return
+ "SELECT 'Wantedpages' AS type,
+ pl_namespace AS namespace,
+ pl_title AS title,
+ COUNT(*) AS value
+ FROM $pagelinks
+ LEFT JOIN $page AS pg1
+ ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title
+ LEFT JOIN $page AS pg2
+ ON pl_from = pg2.page_id
+ WHERE pg1.page_namespace IS NULL
+ AND pl_namespace NOT IN ( 2, 3 )
+ AND pg2.page_namespace != 8
+ GROUP BY pl_namespace, pl_title
+ HAVING COUNT(*) > $count";
+ }
+
+ /**
+ * Cache page existence for performance
+ */
+ function preprocessResults( &$db, &$res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) )
+ $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+ $batch->execute();
+
+ // Back to start for display
+ if ( $db->numRows( $res ) > 0 )
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+
+
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+
+ if( $this->isCached() ) {
+ # Check existence; which is stored in the link cache
+ if( !$title->exists() ) {
+ # Make a redlink
+ $pageLink = $skin->makeBrokenLinkObj( $title );
+ } else {
+ # Make a a struck-out normal link
+ $pageLink = "<s>" . $skin->makeLinkObj( $title ) . "</s>";
+ }
+ } else {
+ # Not cached? Don't bother checking existence; it can't
+ $pageLink = $skin->makeBrokenLinkObj( $title );
+ }
+
+ # Make a link to "what links here" if it's required
+ $wlhLink = $this->nlinks
+ ? $this->makeWlhLink( $title, $skin,
+ wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) ) )
+ : null;
+
+ return wfSpecialList($pageLink, $wlhLink);
+ }
+
+ /**
+ * Make a "what links here" link for a specified title
+ * @param $title Title to make the link for
+ * @param $skin Skin to use
+ * @param $text Link text
+ * @return string
+ */
+ function makeWlhLink( &$title, &$skin, $text ) {
+ $wlhTitle = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' );
+ return $skin->makeKnownLinkObj( $wlhTitle, $text, 'target=' . $title->getPrefixedUrl() );
+ }
+
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedpages( $par = null, $specialPage ) {
+ $inc = $specialPage->including();
+
+ if ( $inc ) {
+ @list( $limit, $nlinks ) = explode( '/', $par, 2 );
+ $limit = (int)$limit;
+ $nlinks = $nlinks === 'nlinks';
+ $offset = 0;
+ } else {
+ list( $limit, $offset ) = wfCheckLimits();
+ $nlinks = true;
+ }
+
+ $wpp = new WantedPagesPage( $inc, $nlinks );
+
+ $wpp->doQuery( $offset, $limit, !$inc );
+}
+
+?>
diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php
new file mode 100644
index 00000000..5b1e2890
--- /dev/null
+++ b/includes/SpecialWatchlist.php
@@ -0,0 +1,513 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'SpecialRecentchanges.php' );
+
+/**
+ * Constructor
+ * @todo Document $par parameter.
+ * @param $par String: FIXME
+ */
+function wfSpecialWatchlist( $par ) {
+ global $wgUser, $wgOut, $wgLang, $wgMemc, $wgRequest, $wgContLang;
+ global $wgUseWatchlistCache, $wgWLCacheTimeout, $wgDBname;
+ global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
+ global $wgEnotifWatchlist;
+ $fname = 'wfSpecialWatchlist';
+
+ $skin =& $wgUser->getSkin();
+ $specialTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ # Anons don't get a watchlist
+ if( $wgUser->isAnon() ) {
+ $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
+ $llink = $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+ $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+ return;
+ } else {
+ $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
+ $wgOut->setSubtitle( wfMsgWikiHtml( 'watchlistfor', htmlspecialchars( $wgUser->getName() ) ) );
+ }
+
+ if( wlHandleClear( $wgOut, $wgRequest, $par ) ) {
+ return;
+ }
+
+ $defaults = array(
+ /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
+ /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
+ /* ? */ 'namespace' => 'all',
+ );
+
+ extract($defaults);
+
+ # Extract variables from the request, falling back to user preferences or
+ # other default values if these don't exist
+ $prefs['days' ] = floatval( $wgUser->getOption( 'watchlistdays' ) );
+ $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
+ $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
+
+ # Get query variables
+ $days = $wgRequest->getVal( 'days', $prefs['days'] );
+ $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
+ $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
+
+ # Get namespace value, if supplied, and prepare a WHERE fragment
+ $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
+ if( !is_null( $nameSpace ) ) {
+ $nameSpace = intval( $nameSpace );
+ $nameSpaceClause = " AND rc_namespace = $nameSpace";
+ } else {
+ $nameSpace = '';
+ $nameSpaceClause = '';
+ }
+
+ # Watchlist editing
+ $action = $wgRequest->getVal( 'action' );
+ $remove = $wgRequest->getVal( 'remove' );
+ $id = $wgRequest->getArray( 'id' );
+
+ $uid = $wgUser->getID();
+ if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
+ $wgUser->clearAllNotifications( $uid );
+ }
+
+ # Deleting items from watchlist
+ if(($action == 'submit') && isset($remove) && is_array($id)) {
+ $wgOut->addWikiText( wfMsg( 'removingchecked' ) );
+ $wgOut->addHTML( '<p>' );
+ foreach($id as $one) {
+ $t = Title::newFromURL( $one );
+ if( !is_null( $t ) ) {
+ $wl = WatchedItem::fromUserTitle( $wgUser, $t );
+ if( $wl->removeWatch() === false ) {
+ $wgOut->addHTML( "<br />\n" . wfMsg( 'couldntremove', htmlspecialchars($one) ) );
+ } else {
+ wfRunHooks('UnwatchArticle', array(&$wgUser, new Article($t)));
+ $wgOut->addHTML( ' (' . htmlspecialchars($one) . ')' );
+ }
+ } else {
+ $wgOut->addHTML( "<br />\n" . wfMsg( 'iteminvalidname', htmlspecialchars($one) ) );
+ }
+ }
+ $wgOut->addHTML( "<br />\n" . wfMsg( 'wldone' ) . "</p>\n" );
+ }
+
+ if ( $wgUseWatchlistCache ) {
+ $memckey = "$wgDBname:watchlist:id:" . $wgUser->getId();
+ $cache_s = @$wgMemc->get( $memckey );
+ if( $cache_s ){
+ $wgOut->addWikiText( wfMsg('wlsaved') );
+ $wgOut->addHTML( $cache_s );
+ return;
+ }
+ }
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ extract( $dbr->tableNames( 'page', 'revision', 'watchlist', 'recentchanges' ) );
+
+ $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_user=$uid";
+ $res = $dbr->query( $sql, $fname );
+ $s = $dbr->fetchObject( $res );
+
+# Patch *** A1 *** (see A2 below)
+# adjust for page X, talk:page X, which are both stored separately, but treated together
+ $nitems = floor($s->n / 2);
+# $nitems = $s->n;
+
+ if($nitems == 0) {
+ $wgOut->addWikiText( wfMsg( 'nowatchlist' ) );
+ return;
+ }
+
+ if( is_null($days) || !is_numeric($days) ) {
+ $big = 1000; /* The magical big */
+ if($nitems > $big) {
+ # Set default cutoff shorter
+ $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
+ } else {
+ $days = $defaults['days']; # default cutoff for shortlisters
+ }
+ } else {
+ $days = floatval($days);
+ }
+
+ // Dump everything here
+ $nondefaults = array();
+
+ wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideOwn', (int)$hideOwn, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideBots', (int)$hideBots, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'namespace', $nameSpace, $defaults, $nondefaults );
+
+ if ( $days <= 0 ) {
+ $docutoff = '';
+ $cutoff = false;
+ $npages = wfMsg( 'watchlistall1' );
+ } else {
+ $docutoff = "AND rev_timestamp > '" .
+ ( $cutoff = $dbr->timestamp( time() - intval( $days * 86400 ) ) )
+ . "'";
+ /*
+ $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page";
+ $res = $dbr->query( $sql, $fname );
+ $s = $dbr->fetchObject( $res );
+ $npages = $s->n;
+ */
+ $npages = 40000 * $days;
+
+ }
+
+ /* Edit watchlist form */
+ if($wgRequest->getBool('edit') || $par == 'edit' ) {
+ $wgOut->addWikiText( wfMsg( 'watchlistcontains', $wgLang->formatNum( $nitems ) ) .
+ "\n\n" . wfMsg( 'watcheditlist' ) );
+
+ $wgOut->addHTML( '<form action=\'' .
+ $specialTitle->escapeLocalUrl( 'action=submit' ) .
+ "' method='post'>\n" );
+
+# Patch A2
+# The following was proposed by KTurner 07.11.2004 to T.Gries
+# $sql = "SELECT distinct (wl_namespace & ~1),wl_title FROM $watchlist WHERE wl_user=$uid";
+ $sql = "SELECT wl_namespace, wl_title, page_is_redirect FROM $watchlist LEFT JOIN $page ON wl_namespace = page_namespace AND wl_title = page_title WHERE wl_user=$uid";
+
+ $res = $dbr->query( $sql, $fname );
+
+ # Batch existence check
+ $linkBatch = new LinkBatch();
+ while( $row = $dbr->fetchObject( $res ) )
+ $linkBatch->addObj( Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ) );
+ $linkBatch->execute();
+ if( $dbr->numRows( $res ) > 0 )
+ $dbr->dataSeek( $res, 0 ); # Let's do the time warp again!
+
+ $sk = $wgUser->getSkin();
+
+ $list = array();
+ while( $s = $dbr->fetchObject( $res ) ) {
+ $list[$s->wl_namespace][$s->wl_title] = $s->page_is_redirect;
+ }
+
+ // TODO: Display a TOC
+ foreach($list as $ns => $titles) {
+ if (Namespace::isTalk($ns))
+ continue;
+ if ($ns != NS_MAIN)
+ $wgOut->addHTML( '<h2>' . $wgContLang->getFormattedNsText( $ns ) . '</h2>' );
+ $wgOut->addHTML( '<ul>' );
+ foreach( $titles as $title => $redir ) {
+ $titleObj = Title::makeTitle( $ns, $title );
+ if( is_null( $titleObj ) ) {
+ $wgOut->addHTML(
+ '<!-- bad title "' .
+ htmlspecialchars( $s->wl_title ) . '" in namespace ' . $s->wl_namespace . " -->\n"
+ );
+ } else {
+ global $wgContLang;
+ $toolLinks = array();
+ $titleText = $titleObj->getPrefixedText();
+ $pageLink = $sk->makeLinkObj( $titleObj );
+ $toolLinks[] = $sk->makeLinkObj( $titleObj->getTalkPage(), $wgLang->getNsText( NS_TALK ) );
+ if( $titleObj->exists() )
+ $toolLinks[] = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'history_short' ), 'action=history' );
+ $toolLinks = '(' . implode( ' | ', $toolLinks ) . ')';
+ $checkbox = '<input type="checkbox" name="id[]" value="' . htmlspecialchars( $titleObj->getPrefixedText() ) . '" /> ' . ( $wgContLang->isRTL() ? '&rlm;' : '&lrm;' );
+ if( $redir ) {
+ $spanopen = '<span class="watchlistredir">';
+ $spanclosed = '</span>';
+ } else {
+ $spanopen = $spanclosed = '';
+ }
+
+ $wgOut->addHTML( "<li>{$checkbox}{$spanopen}{$pageLink}{$spanclosed} {$toolLinks}</li>\n" );
+ }
+ }
+ $wgOut->addHTML( '</ul>' );
+ }
+ $wgOut->addHTML(
+ "<input type='submit' name='remove' value=\"" .
+ htmlspecialchars( wfMsg( "removechecked" ) ) . "\" />\n" .
+ "</form>\n"
+ );
+
+ return;
+ }
+
+ # If the watchlist is relatively short, it's simplest to zip
+ # down its entirety and then sort the results.
+
+ # If it's relatively long, it may be worth our while to zip
+ # through the time-sorted page list checking for watched items.
+
+ # Up estimate of watched items by 15% to compensate for talk pages...
+
+ # Toggles
+ $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : '';
+ $andHideBots = $hideBots ? "AND (rc_bot = 0)" : '';
+
+ # Show watchlist header
+ $header = '';
+ if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
+ $header .= wfMsg( 'wlheader-enotif' ) . "\n";
+ }
+ if ( $wgEnotifWatchlist && $wgShowUpdatedMarker ) {
+ $header .= wfMsg( 'wlheader-showupdated' ) . "\n";
+ }
+
+ # Toggle watchlist content (all recent edits or just the latest)
+ if( $wgUser->getOption( 'extendwatchlist' )) {
+ $andLatest='';
+ $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
+ } else {
+ $andLatest= 'AND rc_this_oldid=page_latest';
+ $limitWatchlist = '';
+ }
+
+ # TODO: Consider removing the third parameter
+ $header .= wfMsg( 'watchdetails', $wgLang->formatNum( $nitems ),
+ $wgLang->formatNum( $npages ), '',
+ $specialTitle->getFullUrl( 'edit=yes' ) );
+ $wgOut->addWikiText( $header );
+
+ if ( $wgEnotifWatchlist && $wgShowUpdatedMarker ) {
+ $wgOut->addHTML( '<form action="' .
+ $specialTitle->escapeLocalUrl() .
+ '" method="post"><input type="submit" name="dummy" value="' .
+ htmlspecialchars( wfMsg( 'enotif_reset' ) ) .
+ '" /><input type="hidden" name="reset" value="all" /></form>' .
+ "\n\n" );
+ }
+
+ $sql = "SELECT
+ rc_namespace AS page_namespace, rc_title AS page_title,
+ rc_comment AS rev_comment, rc_cur_id AS page_id,
+ rc_user AS rev_user, rc_user_text AS rev_user_text,
+ rc_timestamp AS rev_timestamp, rc_minor AS rev_minor_edit,
+ rc_this_oldid AS rev_id,
+ rc_last_oldid, rc_id, rc_patrolled,
+ rc_new AS page_is_new,wl_notificationtimestamp
+ FROM $watchlist,$recentchanges,$page
+ WHERE wl_user=$uid
+ AND wl_namespace=rc_namespace
+ AND wl_title=rc_title
+ AND rc_timestamp > '$cutoff'
+ AND rc_cur_id=page_id
+ $andLatest
+ $andHideOwn
+ $andHideBots
+ $nameSpaceClause
+ ORDER BY rc_timestamp DESC
+ $limitWatchlist";
+
+ $res = $dbr->query( $sql, $fname );
+ $numRows = $dbr->numRows( $res );
+
+ /* Start bottom header */
+ $wgOut->addHTML( "<hr />\n<p>" );
+
+ if($days >= 1)
+ $wgOut->addWikiText( wfMsg( 'rcnote', $wgLang->formatNum( $numRows ),
+ $wgLang->formatNum( $days ), $wgLang->timeAndDate( wfTimestampNow(), true ) ) . '<br />' , false );
+ elseif($days > 0)
+ $wgOut->addWikiText( wfMsg( 'wlnote', $wgLang->formatNum( $numRows ),
+ $wgLang->formatNum( round($days*24) ) ) . '<br />' , false );
+
+ $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
+
+ # Spit out some control panel links
+ $thisTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
+ $skin = $wgUser->getSkin();
+ $linkElements = array( 'hideOwn' => 'wlhideshowown', 'hideBots' => 'wlhideshowbots' );
+
+ # Problems encountered using the fancier method
+ $label = $hideBots ? wfMsgHtml( 'show' ) : wfMsgHtml( 'hide' );
+ $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
+ $link = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+ $links[] = wfMsgHtml( 'wlhideshowbots', $link );
+
+ $label = $hideOwn ? wfMsgHtml( 'show' ) : wfMsgHtml( 'hide' );
+ $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
+ $link = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+ $links[] = wfMsgHtml( 'wlhideshowown', $link );
+
+ $wgOut->addHTML( implode( ' | ', $links ) );
+
+ # Form for namespace filtering
+ $thisAction = $thisTitle->escapeLocalUrl();
+ $nsForm = "<form method=\"post\" action=\"{$thisAction}\">\n";
+ $nsForm .= "<label for=\"namespace\">" . wfMsgExt( 'namespace', array( 'parseinline') ) . "</label> ";
+ $nsForm .= HTMLnamespaceselector( $nameSpace, '' ) . "\n";
+ $nsForm .= ( $hideOwn ? "<input type=\"hidden\" name=\"hideown\" value=\"1\" />\n" : "" );
+ $nsForm .= ( $hideBots ? "<input type=\"hidden\" name=\"hidebots\" value=\"1\" />\n" : "" );
+ $nsForm .= "<input type=\"hidden\" name=\"days\" value=\"" . $days . "\" />\n";
+ $nsForm .= "<input type=\"submit\" name=\"submit\" value=\"" . wfMsgExt( 'allpagessubmit', array( 'escape') ) . "\" />\n";
+ $nsForm .= "</form>\n";
+ $wgOut->addHTML( $nsForm );
+
+ if ( $numRows == 0 ) {
+ $wgOut->addWikitext( "<br />" . wfMsg( 'watchnochange' ), false );
+ $wgOut->addHTML( "</p>\n" );
+ return;
+ }
+
+ $wgOut->addHTML( "</p>\n" );
+ /* End bottom header */
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ $s = $list->beginRecentChangesList();
+ $counter = 1;
+ while ( $obj = $dbr->fetchObject( $res ) ) {
+ # Make fake RC entry
+ $rc = RecentChange::newFromCurRow( $obj, $obj->rc_last_oldid );
+ $rc->counter = $counter++;
+
+ if ( $wgShowUpdatedMarker ) {
+ $updated = $obj->wl_notificationtimestamp;
+ } else {
+ // Same visual appearance as MW 1.4
+ $updated = true;
+ }
+
+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" .wfStrencode($obj->page_title). "' AND wl_namespace='{$obj->page_namespace}'" ;
+ $res3 = $dbr->query( $sql3, DB_READ, $fname );
+ $x = $dbr->fetchObject( $res3 );
+ $rc->numberofWatchingusers = $x->n;
+ } else {
+ $rc->numberofWatchingusers = 0;
+ }
+
+ $s .= $list->recentChangesLine( $rc, $updated );
+ }
+ $s .= $list->endRecentChangesList();
+
+ $dbr->freeResult( $res );
+ $wgOut->addHTML( $s );
+
+ if ( $wgUseWatchlistCache ) {
+ $wgMemc->set( $memckey, $s, $wgWLCacheTimeout);
+ }
+
+}
+
+function wlHoursLink( $h, $page, $options = array() ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink(
+ $wgContLang->specialPage( $page ),
+ $wgLang->formatNum( $h ),
+ wfArrayToCGI( array('days' => ($h / 24.0)), $options ) );
+ return $s;
+}
+
+function wlDaysLink( $d, $page, $options = array() ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink(
+ $wgContLang->specialPage( $page ),
+ ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ),
+ wfArrayToCGI( array('days' => $d), $options ) );
+ return $s;
+}
+
+/**
+ * Returns html
+ */
+function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
+ $hours = array( 1, 2, 6, 12 );
+ $days = array( 1, 3, 7 );
+ $cl = '';
+ $i = 0;
+ foreach( $hours as $h ) {
+ $hours[$i++] = wlHoursLink( $h, $page, $options );
+ }
+ $i = 0;
+ foreach( $days as $d ) {
+ $days[$i++] = wlDaysLink( $d, $page, $options );
+ }
+ return wfMsgExt('wlshowlast',
+ array('parseinline', 'replaceafter'),
+ implode(' | ', $hours),
+ implode(' | ', $days),
+ wlDaysLink( 0, $page, $options ) );
+}
+
+/**
+ * Count the number of items on a user's watchlist
+ *
+ * @param $talk Include talk pages
+ * @return integer
+ */
+function wlCountItems( &$user, $talk = true ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ # Fetch the raw count
+ $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
+ $row = $dbr->fetchObject( $res );
+ $count = $row->count;
+ $dbr->freeResult( $res );
+
+ # Halve to remove talk pages if needed
+ if( !$talk )
+ $count = floor( $count / 2 );
+
+ return( $count );
+}
+
+/**
+ * Allow the user to clear their watchlist
+ *
+ * @param $out Output object
+ * @param $request Request object
+ * @param $par Parameters passed to the watchlist page
+ * @return bool True if it's been taken care of; false indicates the watchlist
+ * code needs to do something further
+ */
+function wlHandleClear( &$out, &$request, $par ) {
+ # Check this function has something to do
+ if( $request->getText( 'action' ) == 'clear' || $par == 'clear' ) {
+ global $wgUser;
+ $out->setPageTitle( wfMsgHtml( 'clearwatchlist' ) );
+ $count = wlCountItems( $wgUser );
+ if( $count > 0 ) {
+ # See if we're clearing or confirming
+ if( $request->wasPosted() && $wgUser->matchEditToken( $request->getText( 'token' ), 'clearwatchlist' ) ) {
+ # Clearing, so do it and report the result
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->delete( 'watchlist', array( 'wl_user' => $wgUser->mId ), 'wlHandleClear' );
+ $out->addWikiText( wfMsg( 'watchlistcleardone', $count ) );
+ $out->returnToMain();
+ } else {
+ # Confirming, so show a form
+ $wlTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
+ $out->addHTML( wfElement( 'form', array( 'method' => 'post', 'action' => $wlTitle->getLocalUrl( 'action=clear' ) ), NULL ) );
+ $out->addWikiText( wfMsg( 'watchlistcount', $count ) );
+ $out->addWikiText( wfMsg( 'watchlistcleartext' ) );
+ $out->addHTML( wfElement( 'input', array( 'type' => 'hidden', 'name' => 'token', 'value' => $wgUser->editToken( 'clearwatchlist' ) ), '' ) );
+ $out->addHTML( wfElement( 'input', array( 'type' => 'submit', 'name' => 'submit', 'value' => wfMsgHtml( 'watchlistclearbutton' ) ), '' ) );
+ $out->addHTML( wfCloseElement( 'form' ) );
+ }
+ return( true );
+ } else {
+ # Nothing on the watchlist; nothing to do here
+ $out->addWikiText( wfMsg( 'nowatchlist' ) );
+ $out->returnToMain();
+ return( true );
+ }
+ } else {
+ return( false );
+ }
+}
+
+?>
diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php
new file mode 100644
index 00000000..cedf6049
--- /dev/null
+++ b/includes/SpecialWhatlinkshere.php
@@ -0,0 +1,277 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+/**
+ * Entry point
+ * @param string $par An article name ??
+ */
+function wfSpecialWhatlinkshere($par = NULL) {
+ global $wgRequest;
+ $page = new WhatLinksHerePage( $wgRequest, $par );
+ $page->execute();
+}
+
+class WhatLinksHerePage {
+ var $request, $par;
+ var $limit, $from, $dir, $target;
+ var $selfTitle, $skin;
+
+ function WhatLinksHerePage( &$request, $par = null ) {
+ global $wgUser;
+ $this->request =& $request;
+ $this->skin =& $wgUser->getSkin();
+ $this->par = $par;
+ }
+
+ function execute() {
+ global $wgOut;
+
+ $this->limit = min( $this->request->getInt( 'limit', 50 ), 5000 );
+ if ( $this->limit <= 0 ) {
+ $this->limit = 50;
+ }
+ $this->from = $this->request->getInt( 'from' );
+ $this->dir = $this->request->getText( 'dir', 'next' );
+ if ( $this->dir != 'prev' ) {
+ $this->dir = 'next';
+ }
+
+ $targetString = isset($this->par) ? $this->par : $this->request->getVal( 'target' );
+
+ if (is_null($targetString)) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+
+ $this->target = Title::newFromURL( $targetString );
+ if( !$this->target ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+ $this->selfTitle = Title::makeTitleSafe( NS_SPECIAL,
+ 'Whatlinkshere/' . $this->target->getPrefixedDBkey() );
+ $wgOut->setPagetitle( $this->target->getPrefixedText() );
+ $wgOut->setSubtitle( wfMsg( 'linklistsub' ) );
+
+ $isredir = ' (' . wfMsg( 'isredirect' ) . ")\n";
+
+ $wgOut->addHTML('&lt; '.$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
+
+ $this->showIndirectLinks( 0, $this->target, $this->limit, $this->from, $this->dir );
+ }
+
+ /**
+ * @param int $level Recursion level
+ * @param Title $target Target title
+ * @param int $limit Number of entries to display
+ * @param Title $from Display from this article ID
+ * @param string $dir 'next' or 'prev', whether $fromTitle is the start or end of the list
+ * @private
+ */
+ function showIndirectLinks( $level, $target, $limit, $from = 0, $dir = 'next' ) {
+ global $wgOut;
+ $fname = 'WhatLinksHerePage::showIndirectLinks';
+
+ $dbr =& wfGetDB( DB_READ );
+
+ extract( $dbr->tableNames( 'pagelinks', 'templatelinks', 'page' ) );
+
+ // Some extra validation
+ $from = intval( $from );
+ if ( !$from && $dir == 'prev' ) {
+ // Before start? No make sense
+ $dir = 'next';
+ }
+
+ // Make the query
+ $plConds = array(
+ 'page_id=pl_from',
+ 'pl_namespace' => $target->getNamespace(),
+ 'pl_title' => $target->getDBkey(),
+ );
+
+ $tlConds = array(
+ 'page_id=tl_from',
+ 'tl_namespace' => $target->getNamespace(),
+ 'tl_title' => $target->getDBkey(),
+ );
+
+ if ( $from ) {
+ if ( 'prev' == $dir ) {
+ $offsetCond = "page_id < $from";
+ $options = array( 'ORDER BY page_id DESC' );
+ } else {
+ $offsetCond = "page_id >= $from";
+ $options = array( 'ORDER BY page_id' );
+ }
+ } else {
+ $offsetCond = false;
+ $options = array( 'ORDER BY page_id,is_template DESC' );
+ }
+ // Read an extra row as an at-end check
+ $queryLimit = $limit + 1;
+ $options['LIMIT'] = $queryLimit;
+ if ( $offsetCond ) {
+ $tlConds[] = $offsetCond;
+ $plConds[] = $offsetCond;
+ }
+ $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
+
+ $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
+ $plConds, $fname, $options );
+ $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
+ $tlConds, $fname, $options );
+
+ if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) {
+ if ( 0 == $level ) {
+ $wgOut->addWikiText( wfMsg( 'nolinkshere' ) );
+ }
+ return;
+ }
+
+ // Read the rows into an array and remove duplicates
+ // templatelinks comes second so that the templatelinks row overwrites the
+ // pagelinks row, so we get (inclusion) rather than nothing
+ while ( $row = $dbr->fetchObject( $plRes ) ) {
+ $row->is_template = 0;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $plRes );
+ while ( $row = $dbr->fetchObject( $tlRes ) ) {
+ $row->is_template = 1;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $tlRes );
+
+ // Sort by key and then change the keys to 0-based indices
+ ksort( $rows );
+ $rows = array_values( $rows );
+
+ $numRows = count( $rows );
+
+ // Work out the start and end IDs, for prev/next links
+ if ( $dir == 'prev' ) {
+ // Descending order
+ if ( $numRows > $limit ) {
+ // More rows available before these ones
+ // Get the ID from the next row past the end of the displayed set
+ $prevId = $rows[$limit]->page_id;
+ // Remove undisplayed rows
+ $rows = array_slice( $rows, 0, $limit );
+ } else {
+ // No more rows available before
+ $prevId = 0;
+ }
+ // Assume that the ID specified in $from exists, so there must be another page
+ $nextId = $from;
+
+ // Reverse order ready for display
+ $rows = array_reverse( $rows );
+ } else {
+ // Ascending
+ if ( $numRows > $limit ) {
+ // More rows available after these ones
+ // Get the ID from the last row in the result set
+ $nextId = $rows[$limit]->page_id;
+ // Remove undisplayed rows
+ $rows = array_slice( $rows, 0, $limit );
+ } else {
+ // No more rows after
+ $nextId = false;
+ }
+ $prevId = $from;
+ }
+
+ if ( 0 == $level ) {
+ $wgOut->addWikiText( wfMsg( 'linkshere' ) );
+ }
+ $isredir = wfMsg( 'isredirect' );
+ $istemplate = wfMsg( 'istemplate' );
+
+ if( $level == 0 ) {
+ $prevnext = $this->getPrevNext( $limit, $prevId, $nextId );
+ $wgOut->addHTML( $prevnext );
+ }
+
+ $wgOut->addHTML( '<ul>' );
+ foreach ( $rows as $row ) {
+ $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+ if ( $row->page_is_redirect ) {
+ $extra = 'redirect=no';
+ } else {
+ $extra = '';
+ }
+
+ $link = $this->skin->makeKnownLinkObj( $nt, '', $extra );
+ $wgOut->addHTML( '<li>'.$link );
+
+ // Display properties (redirect or template)
+ $props = array();
+ if ( $row->page_is_redirect ) {
+ $props[] = $isredir;
+ }
+ if ( $row->is_template ) {
+ $props[] = $istemplate;
+ }
+ if ( count( $props ) ) {
+ // FIXME? Cultural assumption, hard-coded punctuation
+ $wgOut->addHTML( ' (' . implode( ', ', $props ) . ') ' );
+ }
+
+ if ( $row->page_is_redirect ) {
+ if ( $level < 2 ) {
+ $this->showIndirectLinks( $level + 1, $nt, 500 );
+ }
+ }
+ $wgOut->addHTML( "</li>\n" );
+ }
+ $wgOut->addHTML( "</ul>\n" );
+
+ if( $level == 0 ) {
+ $wgOut->addHTML( $prevnext );
+ }
+ }
+
+ function makeSelfLink( $text, $query ) {
+ return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
+ }
+
+ function getPrevNext( $limit, $prevId, $nextId ) {
+ global $wgLang;
+ $fmtLimit = $wgLang->formatNum( $limit );
+ $prev = wfMsg( 'prevn', $fmtLimit );
+ $next = wfMsg( 'nextn', $fmtLimit );
+
+ if ( 0 != $prevId ) {
+ $prevLink = $this->makeSelfLink( $prev, "limit={$limit}&from={$prevId}&dir=prev" );
+ } else {
+ $prevLink = $prev;
+ }
+ if ( 0 != $nextId ) {
+ $nextLink = $this->makeSelfLink( $next, "limit={$limit}&from={$nextId}" );
+ } else {
+ $nextLink = $next;
+ }
+ $nums = $this->numLink( 20, $prevId ) . ' | ' .
+ $this->numLink( 50, $prevId ) . ' | ' .
+ $this->numLink( 100, $prevId ) . ' | ' .
+ $this->numLink( 250, $prevId ) . ' | ' .
+ $this->numLink( 500, $prevId );
+
+ return wfMsg( 'viewprevnext', $prevLink, $nextLink, $nums );
+ }
+
+ function numLink( $limit, $from ) {
+ global $wgLang;
+ $query = "limit={$limit}&from={$from}";
+ $fmtLimit = $wgLang->formatNum( $limit );
+ return $this->makeSelfLink( $fmtLimit, $query );
+ }
+}
+
+?>
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
new file mode 100644
index 00000000..37d97e01
--- /dev/null
+++ b/includes/SquidUpdate.php
@@ -0,0 +1,279 @@
+<?php
+/**
+ * See deferred.txt
+ * @package MediaWiki
+ */
+
+/**
+ *
+ * @package MediaWiki
+ */
+class SquidUpdate {
+ var $urlArr, $mMaxTitles;
+
+ function SquidUpdate( $urlArr = Array(), $maxTitles = false ) {
+ global $wgMaxSquidPurgeTitles;
+ if ( $maxTitles === false ) {
+ $this->mMaxTitles = $wgMaxSquidPurgeTitles;
+ } else {
+ $this->mMaxTitles = $maxTitles;
+ }
+ if ( count( $urlArr ) > $this->mMaxTitles ) {
+ $urlArr = array_slice( $urlArr, 0, $this->mMaxTitles );
+ }
+ $this->urlArr = $urlArr;
+ }
+
+ /* static */ function newFromLinksTo( &$title ) {
+ $fname = 'SquidUpdate::newFromLinksTo';
+ wfProfileIn( $fname );
+
+ # Get a list of URLs linking to this page
+ $id = $title->getArticleID();
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( array( 'links', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ array(
+ 'pl_namespace' => $title->getNamespace(),
+ 'pl_title' => $title->getDbKey(),
+ 'pl_from=page_id' ),
+ $fname );
+ $blurlArr = $title->getSquidURLs();
+ if ( $dbr->numRows( $res ) <= $this->mMaxTitles ) {
+ while ( $BL = $dbr->fetchObject ( $res ) )
+ {
+ $tobj = Title::makeTitle( $BL->page_namespace, $BL->page_title ) ;
+ $blurlArr[] = $tobj->getInternalURL();
+ }
+ }
+ $dbr->freeResult ( $res ) ;
+
+ wfProfileOut( $fname );
+ return new SquidUpdate( $blurlArr );
+ }
+
+ /* static */ function newFromTitles( &$titles, $urlArr = array() ) {
+ global $wgMaxSquidPurgeTitles;
+ if ( count( $titles ) > $wgMaxSquidPurgeTitles ) {
+ $titles = array_slice( $titles, 0, $wgMaxSquidPurgeTitles );
+ }
+ foreach ( $titles as $title ) {
+ $urlArr[] = $title->getInternalURL();
+ }
+ return new SquidUpdate( $urlArr );
+ }
+
+ /* static */ function newSimplePurge( &$title ) {
+ $urlArr = $title->getSquidURLs();
+ return new SquidUpdate( $urlArr );
+ }
+
+ function doUpdate() {
+ SquidUpdate::purge( $this->urlArr );
+ }
+
+ /* Purges a list of Squids defined in $wgSquidServers.
+ $urlArr should contain the full URLs to purge as values
+ (example: $urlArr[] = 'http://my.host/something')
+ XXX report broken Squids per mail or log */
+
+ /* static */ function purge( $urlArr ) {
+ global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort;
+
+ /*if ( (@$wgSquidServers[0]) == 'echo' ) {
+ echo implode("<br />\n", $urlArr) . "<br />\n";
+ return;
+ }*/
+
+ if ( $wgHTCPMulticastAddress && $wgHTCPPort )
+ SquidUpdate::HTCPPurge( $urlArr );
+
+ $fname = 'SquidUpdate::purge';
+ wfProfileIn( $fname );
+
+ $maxsocketspersquid = 8; // socket cap per Squid
+ $urlspersocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while
+ $firsturl = SquidUpdate::expand( $urlArr[0] );
+ unset($urlArr[0]);
+ $urlArr = array_values($urlArr);
+ $sockspersq = max(ceil(count($urlArr) / $urlspersocket ),1);
+ if ($sockspersq == 1) {
+ /* the most common case */
+ $urlspersocket = count($urlArr);
+ } else if ($sockspersq > $maxsocketspersquid ) {
+ $urlspersocket = ceil(count($urlArr) / $maxsocketspersquid);
+ $sockspersq = $maxsocketspersquid;
+ }
+ $totalsockets = count($wgSquidServers) * $sockspersq;
+ $sockets = Array();
+
+ /* this sets up the sockets and tests the first socket for each server. */
+ for ($ss=0;$ss < count($wgSquidServers);$ss++) {
+ $failed = false;
+ $so = 0;
+ while ($so < $sockspersq && !$failed) {
+ if ($so == 0) {
+ /* first socket for this server, do the tests */
+ @list($server, $port) = explode(':', $wgSquidServers[$ss]);
+ if(!isset($port)) $port = 80;
+ #$this->debug("Opening socket to $server:$port");
+ $error = $errstr = false;
+ $socket = @fsockopen($server, $port, $error, $errstr, 3);
+ #$this->debug("\n");
+ if (!$socket) {
+ $failed = true;
+ $totalsockets -= $sockspersq;
+ } else {
+ $msg = 'PURGE ' . $firsturl . " HTTP/1.0\r\n".
+ "Connection: Keep-Alive\r\n\r\n";
+ #$this->debug($msg);
+ @fputs($socket,$msg);
+ #$this->debug("...");
+ $res = @fread($socket,512);
+ #$this->debug("\n");
+ /* Squid only returns http headers with 200 or 404 status,
+ if there's more returned something's wrong */
+ if (strlen($res) > 250) {
+ fclose($socket);
+ $failed = true;
+ $totalsockets -= $sockspersq;
+ } else {
+ @stream_set_blocking($socket,false);
+ $sockets[] = $socket;
+ }
+ }
+ } else {
+ /* open the remaining sockets for this server */
+ list($server, $port) = explode(':', $wgSquidServers[$ss]);
+ if(!isset($port)) $port = 80;
+ $sockets[$so+1] = @fsockopen($server, $port, $error, $errstr, 2);
+ @stream_set_blocking($sockets[$so+1],false);
+ }
+ $so++;
+ }
+ }
+
+ if ($urlspersocket > 0) {
+ /* now do the heavy lifting. The fread() relies on Squid returning only the headers */
+ for ($r=0;$r < $urlspersocket;$r++) {
+ for ($s=0;$s < $totalsockets;$s++) {
+ if($r != 0) {
+ $res = '';
+ $esc = 0;
+ while (strlen($res) < 100 && $esc < 200 ) {
+ $res .= @fread($sockets[$s],512);
+ $esc++;
+ usleep(20);
+ }
+ }
+ $urindex = $r + $urlspersocket * ($s - $sockspersq * floor($s / $sockspersq));
+ $url = SquidUpdate::expand( $urlArr[$urindex] );
+ $msg = 'PURGE ' . $url . " HTTP/1.0\r\n".
+ "Connection: Keep-Alive\r\n\r\n";
+ #$this->debug($msg);
+ @fputs($sockets[$s],$msg);
+ #$this->debug("\n");
+ }
+ }
+ }
+ #$this->debug("Reading response...");
+ foreach ($sockets as $socket) {
+ $res = '';
+ $esc = 0;
+ while (strlen($res) < 100 && $esc < 200 ) {
+ $res .= @fread($socket,1024);
+ $esc++;
+ usleep(20);
+ }
+
+ @fclose($socket);
+ }
+ #$this->debug("\n");
+ wfProfileOut( $fname );
+ }
+
+ /* static */ function HTCPPurge( $urlArr ) {
+ global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort;
+ $fname = 'SquidUpdate::HTCPPurge';
+ wfProfileIn( $fname );
+
+ $htcpOpCLR = 4; // HTCP CLR
+
+ // FIXME PHP doesn't support these socket constants (include/linux/in.h)
+ define( "IPPROTO_IP", 0 );
+ define( "IP_MULTICAST_LOOP", 34 );
+ define( "IP_MULTICAST_TTL", 33 );
+
+ // pfsockopen doesn't work because we need set_sock_opt
+ $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+ if ( $conn ) {
+ // Set socket options
+ socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 );
+ if ( $wgHTCPMulticastTTL != 1 )
+ socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL,
+ $wgHTCPMulticastTTL );
+
+ foreach ( $urlArr as $url ) {
+ $url = SquidUpdate::expand( $url );
+
+ // Construct a minimal HTCP request diagram
+ // as per RFC 2756
+ // Opcode 'CLR', no response desired, no auth
+ $htcpTransID = rand();
+
+ $htcpSpecifier = pack( 'na4na*na8n',
+ 4, 'NONE', strlen( $url ), $url,
+ 8, 'HTTP/1.0', 0 );
+
+ $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier );
+ $htcpLen = 4 + $htcpDataLen + 2;
+
+ // Note! Squid gets the bit order of the first
+ // word wrong, wrt the RFC. Apparently no other
+ // implementation exists, so adapt to Squid
+ $htcpPacket = pack( 'nxxnCxNxxa*n',
+ $htcpLen, $htcpDataLen, $htcpOpCLR,
+ $htcpTransID, $htcpSpecifier, 2);
+
+ // Send out
+ wfDebug( "Purging URL $url via HTCP\n" );
+ socket_sendto( $conn, $htcpPacket, $htcpLen, 0,
+ $wgHTCPMulticastAddress, $wgHTCPPort );
+ }
+ } else {
+ $errstr = socket_strerror( socket_last_error() );
+ wfDebug( "SquidUpdate::HTCPPurge(): Error opening UDP socket: $errstr\n" );
+ }
+ wfProfileOut( $fname );
+ }
+
+ function debug( $text ) {
+ global $wgDebugSquid;
+ if ( $wgDebugSquid ) {
+ wfDebug( $text );
+ }
+ }
+
+ /**
+ * Expand local URLs to fully-qualified URLs using the internal protocol
+ * and host defined in $wgInternalServer. Input that's already fully-
+ * qualified will be passed through unchanged.
+ *
+ * This is used to generate purge URLs that may be either local to the
+ * main wiki or include a non-native host, such as images hosted on a
+ * second internal server.
+ *
+ * Client functions should not need to call this.
+ *
+ * @return string
+ */
+ static function expand( $url ) {
+ global $wgInternalServer;
+ if( $url != '' && $url{0} == '/' ) {
+ return $wgInternalServer . $url;
+ }
+ return $url;
+ }
+}
+?>
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
new file mode 100644
index 00000000..83417185
--- /dev/null
+++ b/includes/StreamFile.php
@@ -0,0 +1,72 @@
+<?php
+/** */
+
+/** */
+function wfStreamFile( $fname ) {
+ $stat = @stat( $fname );
+ if ( !$stat ) {
+ header( 'HTTP/1.0 404 Not Found' );
+ echo "<html><body>
+<h1>File not found</h1>
+<p>Although this PHP script ({$_SERVER['SCRIPT_NAME']}) exists, the file requested for output
+does not.</p>
+</body></html>";
+ return;
+ }
+
+ header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $stat['mtime'] ) . ' GMT' );
+
+ // Cancel output buffering and gzipping if set
+ while( $status = ob_get_status() ) {
+ ob_end_clean();
+ if( $status['name'] == 'ob_gzhandler' ) {
+ header( 'Content-Encoding:' );
+ }
+ }
+
+ $type = wfGetType( $fname );
+ if ( $type and $type!="unknown/unknown") {
+ header("Content-type: $type");
+ } else {
+ header('Content-type: application/x-wiki');
+ }
+
+ if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+ $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
+ $sinceTime = strtotime( $modsince );
+ if ( $stat['mtime'] <= $sinceTime ) {
+ header( "HTTP/1.0 304 Not Modified" );
+ return;
+ }
+ }
+
+ header( 'Content-Length: ' . $stat['size'] );
+
+ readfile( $fname );
+}
+
+/** */
+function wfGetType( $filename ) {
+ global $wgTrivialMimeDetection;
+
+ # trivial detection by file extension,
+ # used for thumbnails (thumb.php)
+ if ($wgTrivialMimeDetection) {
+ $ext= strtolower(strrchr($filename, '.'));
+
+ switch ($ext) {
+ case '.gif': return 'image/gif';
+ case '.png': return 'image/png';
+ case '.jpg': return 'image/jpeg';
+ case '.jpeg': return 'image/jpeg';
+ }
+
+ return 'unknown/unknown';
+ }
+ else {
+ $magic=& wfGetMimeMagic();
+ return $magic->guessMimeType($filename); //full fancy mime detection
+ }
+}
+
+?>
diff --git a/includes/Title.php b/includes/Title.php
new file mode 100644
index 00000000..bc8f69a2
--- /dev/null
+++ b/includes/Title.php
@@ -0,0 +1,2307 @@
+<?php
+/**
+ * See title.txt
+ *
+ * @package MediaWiki
+ */
+
+/** */
+require_once( 'normal/UtfNormal.php' );
+
+define ( 'GAID_FOR_UPDATE', 1 );
+
+# Title::newFromTitle maintains a cache to avoid
+# expensive re-normalization of commonly used titles.
+# On a batch operation this can become a memory leak
+# if not bounded. After hitting this many titles,
+# reset the cache.
+define( 'MW_TITLECACHE_MAX', 1000 );
+
+/**
+ * Title class
+ * - Represents a title, which may contain an interwiki designation or namespace
+ * - Can fetch various kinds of data from the database, albeit inefficiently.
+ *
+ * @package MediaWiki
+ */
+class Title {
+ /**
+ * Static cache variables
+ */
+ static private $titleCache=array();
+ static private $interwikiCache=array();
+
+
+ /**
+ * All member variables should be considered private
+ * Please use the accessor functions
+ */
+
+ /**#@+
+ * @private
+ */
+
+ var $mTextform; # Text form (spaces not underscores) of the main part
+ var $mUrlform; # URL-encoded form of the main part
+ var $mDbkeyform; # Main part with underscores
+ var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants
+ var $mInterwiki; # Interwiki prefix (or null string)
+ var $mFragment; # Title fragment (i.e. the bit after the #)
+ var $mArticleID; # Article ID, fetched from the link cache on demand
+ var $mLatestID; # ID of most recent revision
+ var $mRestrictions; # Array of groups allowed to edit this article
+ # Only null or "sysop" are supported
+ var $mRestrictionsLoaded; # Boolean for initialisation on demand
+ var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand
+ var $mDefaultNamespace; # Namespace index when there is no namespace
+ # Zero except in {{transclusion}} tags
+ var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching()
+ /**#@-*/
+
+
+ /**
+ * Constructor
+ * @private
+ */
+ /* private */ function Title() {
+ $this->mInterwiki = $this->mUrlform =
+ $this->mTextform = $this->mDbkeyform = '';
+ $this->mArticleID = -1;
+ $this->mNamespace = NS_MAIN;
+ $this->mRestrictionsLoaded = false;
+ $this->mRestrictions = array();
+ # Dont change the following, NS_MAIN is hardcoded in several place
+ # See bug #696
+ $this->mDefaultNamespace = NS_MAIN;
+ $this->mWatched = NULL;
+ $this->mLatestID = false;
+ }
+
+ /**
+ * Create a new Title from a prefixed DB key
+ * @param string $key The database key, which has underscores
+ * instead of spaces, possibly including namespace and
+ * interwiki prefixes
+ * @return Title the new object, or NULL on an error
+ * @static
+ * @access public
+ */
+ /* static */ function newFromDBkey( $key ) {
+ $t = new Title();
+ $t->mDbkeyform = $key;
+ if( $t->secureAndSplit() )
+ return $t;
+ else
+ return NULL;
+ }
+
+ /**
+ * Create a new Title from text, such as what one would
+ * find in a link. Decodes any HTML entities in the text.
+ *
+ * @param string $text the link text; spaces, prefixes,
+ * and an initial ':' indicating the main namespace
+ * are accepted
+ * @param int $defaultNamespace the namespace to use if
+ * none is specified by a prefix
+ * @return Title the new object, or NULL on an error
+ * @static
+ * @access public
+ */
+ function newFromText( $text, $defaultNamespace = NS_MAIN ) {
+ $fname = 'Title::newFromText';
+
+ if( is_object( $text ) ) {
+ throw new MWException( 'Title::newFromText given an object' );
+ }
+
+ /**
+ * Wiki pages often contain multiple links to the same page.
+ * Title normalization and parsing can become expensive on
+ * pages with many links, so we can save a little time by
+ * caching them.
+ *
+ * In theory these are value objects and won't get changed...
+ */
+ if( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) {
+ return Title::$titleCache[$text];
+ }
+
+ /**
+ * Convert things like &eacute; &#257; or &#x3017; into real text...
+ */
+ $filteredText = Sanitizer::decodeCharReferences( $text );
+
+ $t =& new Title();
+ $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
+ $t->mDefaultNamespace = $defaultNamespace;
+
+ static $cachedcount = 0 ;
+ if( $t->secureAndSplit() ) {
+ if( $defaultNamespace == NS_MAIN ) {
+ if( $cachedcount >= MW_TITLECACHE_MAX ) {
+ # Avoid memory leaks on mass operations...
+ Title::$titleCache = array();
+ $cachedcount=0;
+ }
+ $cachedcount++;
+ Title::$titleCache[$text] =& $t;
+ }
+ return $t;
+ } else {
+ $ret = NULL;
+ return $ret;
+ }
+ }
+
+ /**
+ * Create a new Title from URL-encoded text. Ensures that
+ * the given title's length does not exceed the maximum.
+ * @param string $url the title, as might be taken from a URL
+ * @return Title the new object, or NULL on an error
+ * @static
+ * @access public
+ */
+ function newFromURL( $url ) {
+ global $wgLegalTitleChars;
+ $t = new Title();
+
+ # For compatibility with old buggy URLs. "+" is usually not valid in titles,
+ # but some URLs used it as a space replacement and they still come
+ # from some external search tools.
+ if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
+ $url = str_replace( '+', ' ', $url );
+ }
+
+ $t->mDbkeyform = str_replace( ' ', '_', $url );
+ if( $t->secureAndSplit() ) {
+ return $t;
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Create a new Title from an article ID
+ *
+ * @todo This is inefficiently implemented, the page row is requested
+ * but not used for anything else
+ *
+ * @param int $id the page_id corresponding to the Title to create
+ * @return Title the new object, or NULL on an error
+ * @access public
+ * @static
+ */
+ function newFromID( $id ) {
+ $fname = 'Title::newFromID';
+ $dbr =& wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'page', array( 'page_namespace', 'page_title' ),
+ array( 'page_id' => $id ), $fname );
+ if ( $row !== false ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ } else {
+ $title = NULL;
+ }
+ return $title;
+ }
+
+ /**
+ * Make an array of titles from an array of IDs
+ */
+ function newFromIDs( $ids ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ),
+ 'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ );
+
+ $titles = array();
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
+ }
+ return $titles;
+ }
+
+ /**
+ * Create a new Title from a namespace index and a DB key.
+ * It's assumed that $ns and $title are *valid*, for instance when
+ * they came directly from the database or a special page name.
+ * For convenience, spaces are converted to underscores so that
+ * eg user_text fields can be used directly.
+ *
+ * @param int $ns the namespace of the article
+ * @param string $title the unprefixed database key form
+ * @return Title the new object
+ * @static
+ * @access public
+ */
+ function &makeTitle( $ns, $title ) {
+ $t =& new Title();
+ $t->mInterwiki = '';
+ $t->mFragment = '';
+ $t->mNamespace = intval( $ns );
+ $t->mDbkeyform = str_replace( ' ', '_', $title );
+ $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
+ $t->mUrlform = wfUrlencode( $t->mDbkeyform );
+ $t->mTextform = str_replace( '_', ' ', $title );
+ return $t;
+ }
+
+ /**
+ * Create a new Title frrom a namespace index and a DB key.
+ * The parameters will be checked for validity, which is a bit slower
+ * than makeTitle() but safer for user-provided data.
+ *
+ * @param int $ns the namespace of the article
+ * @param string $title the database key form
+ * @return Title the new object, or NULL on an error
+ * @static
+ * @access public
+ */
+ function makeTitleSafe( $ns, $title ) {
+ $t = new Title();
+ $t->mDbkeyform = Title::makeName( $ns, $title );
+ if( $t->secureAndSplit() ) {
+ return $t;
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Create a new Title for the Main Page
+ *
+ * @static
+ * @return Title the new object
+ * @access public
+ */
+ function newMainPage() {
+ return Title::newFromText( wfMsgForContent( 'mainpage' ) );
+ }
+
+ /**
+ * Create a new Title for a redirect
+ * @param string $text the redirect title text
+ * @return Title the new object, or NULL if the text is not a
+ * valid redirect
+ * @static
+ * @access public
+ */
+ function newFromRedirect( $text ) {
+ $mwRedir = MagicWord::get( MAG_REDIRECT );
+ $rt = NULL;
+ if ( $mwRedir->matchStart( $text ) ) {
+ if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) {
+ # categories are escaped using : for example one can enter:
+ # #REDIRECT [[:Category:Music]]. Need to remove it.
+ if ( substr($m[1],0,1) == ':') {
+ # We don't want to keep the ':'
+ $m[1] = substr( $m[1], 1 );
+ }
+
+ $rt = Title::newFromText( $m[1] );
+ # Disallow redirects to Special:Userlogout
+ if ( !is_null($rt) && $rt->getNamespace() == NS_SPECIAL && preg_match( '/^Userlogout/i', $rt->getText() ) ) {
+ $rt = NULL;
+ }
+ }
+ }
+ return $rt;
+ }
+
+#----------------------------------------------------------------------------
+# Static functions
+#----------------------------------------------------------------------------
+
+ /**
+ * Get the prefixed DB key associated with an ID
+ * @param int $id the page_id of the article
+ * @return Title an object representing the article, or NULL
+ * if no such article was found
+ * @static
+ * @access public
+ */
+ function nameOf( $id ) {
+ $fname = 'Title::nameOf';
+ $dbr =& wfGetDB( DB_SLAVE );
+
+ $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ), array( 'page_id' => $id ), $fname );
+ if ( $s === false ) { return NULL; }
+
+ $n = Title::makeName( $s->page_namespace, $s->page_title );
+ return $n;
+ }
+
+ /**
+ * Get a regex character class describing the legal characters in a link
+ * @return string the list of characters, not delimited
+ * @static
+ * @access public
+ */
+ function legalChars() {
+ global $wgLegalTitleChars;
+ return $wgLegalTitleChars;
+ }
+
+ /**
+ * Get a string representation of a title suitable for
+ * including in a search index
+ *
+ * @param int $ns a namespace index
+ * @param string $title text-form main part
+ * @return string a stripped-down title string ready for the
+ * search index
+ */
+ /* static */ function indexTitle( $ns, $title ) {
+ global $wgContLang;
+
+ $lc = SearchEngine::legalSearchChars() . '&#;';
+ $t = $wgContLang->stripForSearch( $title );
+ $t = preg_replace( "/[^{$lc}]+/", ' ', $t );
+ $t = strtolower( $t );
+
+ # Handle 's, s'
+ $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t );
+ $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t );
+
+ $t = preg_replace( "/\\s+/", ' ', $t );
+
+ if ( $ns == NS_IMAGE ) {
+ $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
+ }
+ return trim( $t );
+ }
+
+ /*
+ * Make a prefixed DB key from a DB key and a namespace index
+ * @param int $ns numerical representation of the namespace
+ * @param string $title the DB key form the title
+ * @return string the prefixed form of the title
+ */
+ /* static */ function makeName( $ns, $title ) {
+ global $wgContLang;
+
+ $n = $wgContLang->getNsText( $ns );
+ return $n == '' ? $title : "$n:$title";
+ }
+
+ /**
+ * Returns the URL associated with an interwiki prefix
+ * @param string $key the interwiki prefix (e.g. "MeatBall")
+ * @return the associated URL, containing "$1", which should be
+ * replaced by an article title
+ * @static (arguably)
+ * @access public
+ */
+ function getInterwikiLink( $key ) {
+ global $wgMemc, $wgDBname, $wgInterwikiExpiry;
+ global $wgInterwikiCache;
+ $fname = 'Title::getInterwikiLink';
+
+ $key = strtolower( $key );
+
+ $k = $wgDBname.':interwiki:'.$key;
+ if( array_key_exists( $k, Title::$interwikiCache ) ) {
+ return Title::$interwikiCache[$k]->iw_url;
+ }
+
+ if ($wgInterwikiCache) {
+ return Title::getInterwikiCached( $key );
+ }
+
+ $s = $wgMemc->get( $k );
+ # Ignore old keys with no iw_local
+ if( $s && isset( $s->iw_local ) && isset($s->iw_trans)) {
+ Title::$interwikiCache[$k] = $s;
+ return $s->iw_url;
+ }
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'interwiki',
+ array( 'iw_url', 'iw_local', 'iw_trans' ),
+ array( 'iw_prefix' => $key ), $fname );
+ if( !$res ) {
+ return '';
+ }
+
+ $s = $dbr->fetchObject( $res );
+ if( !$s ) {
+ # Cache non-existence: create a blank object and save it to memcached
+ $s = (object)false;
+ $s->iw_url = '';
+ $s->iw_local = 0;
+ $s->iw_trans = 0;
+ }
+ $wgMemc->set( $k, $s, $wgInterwikiExpiry );
+ Title::$interwikiCache[$k] = $s;
+
+ return $s->iw_url;
+ }
+
+ /**
+ * Fetch interwiki prefix data from local cache in constant database
+ *
+ * More logic is explained in DefaultSettings
+ *
+ * @return string URL of interwiki site
+ * @access public
+ */
+ function getInterwikiCached( $key ) {
+ global $wgDBname, $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
+ static $db, $site;
+
+ if (!$db)
+ $db=dba_open($wgInterwikiCache,'r','cdb');
+ /* Resolve site name */
+ if ($wgInterwikiScopes>=3 and !$site) {
+ $site = dba_fetch("__sites:{$wgDBname}", $db);
+ if ($site=="")
+ $site = $wgInterwikiFallbackSite;
+ }
+ $value = dba_fetch("{$wgDBname}:{$key}", $db);
+ if ($value=='' and $wgInterwikiScopes>=3) {
+ /* try site-level */
+ $value = dba_fetch("_{$site}:{$key}", $db);
+ }
+ if ($value=='' and $wgInterwikiScopes>=2) {
+ /* try globals */
+ $value = dba_fetch("__global:{$key}", $db);
+ }
+ if ($value=='undef')
+ $value='';
+ $s = (object)false;
+ $s->iw_url = '';
+ $s->iw_local = 0;
+ $s->iw_trans = 0;
+ if ($value!='') {
+ list($local,$url)=explode(' ',$value,2);
+ $s->iw_url=$url;
+ $s->iw_local=(int)$local;
+ }
+ Title::$interwikiCache[$wgDBname.':interwiki:'.$key] = $s;
+ return $s->iw_url;
+ }
+ /**
+ * Determine whether the object refers to a page within
+ * this project.
+ *
+ * @return bool TRUE if this is an in-project interwiki link
+ * or a wikilink, FALSE otherwise
+ * @access public
+ */
+ function isLocal() {
+ global $wgDBname;
+
+ if ( $this->mInterwiki != '' ) {
+ # Make sure key is loaded into cache
+ $this->getInterwikiLink( $this->mInterwiki );
+ $k = $wgDBname.':interwiki:' . $this->mInterwiki;
+ return (bool)(Title::$interwikiCache[$k]->iw_local);
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Determine whether the object refers to a page within
+ * this project and is transcludable.
+ *
+ * @return bool TRUE if this is transcludable
+ * @access public
+ */
+ function isTrans() {
+ global $wgDBname;
+
+ if ($this->mInterwiki == '')
+ return false;
+ # Make sure key is loaded into cache
+ $this->getInterwikiLink( $this->mInterwiki );
+ $k = $wgDBname.':interwiki:' . $this->mInterwiki;
+ return (bool)(Title::$interwikiCache[$k]->iw_trans);
+ }
+
+ /**
+ * Update the page_touched field for an array of title objects
+ * @todo Inefficient unless the IDs are already loaded into the
+ * link cache
+ * @param array $titles an array of Title objects to be touched
+ * @param string $timestamp the timestamp to use instead of the
+ * default current time
+ * @static
+ * @access public
+ */
+ function touchArray( $titles, $timestamp = '' ) {
+
+ if ( count( $titles ) == 0 ) {
+ return;
+ }
+ $dbw =& wfGetDB( DB_MASTER );
+ if ( $timestamp == '' ) {
+ $timestamp = $dbw->timestamp();
+ }
+ /*
+ $page = $dbw->tableName( 'page' );
+ $sql = "UPDATE $page SET page_touched='{$timestamp}' WHERE page_id IN (";
+ $first = true;
+
+ foreach ( $titles as $title ) {
+ if ( $wgUseFileCache ) {
+ $cm = new CacheManager($title);
+ @unlink($cm->fileCacheName());
+ }
+
+ if ( ! $first ) {
+ $sql .= ',';
+ }
+ $first = false;
+ $sql .= $title->getArticleID();
+ }
+ $sql .= ')';
+ if ( ! $first ) {
+ $dbw->query( $sql, 'Title::touchArray' );
+ }
+ */
+ // hack hack hack -- brion 2005-07-11. this was unfriendly to db.
+ // do them in small chunks:
+ $fname = 'Title::touchArray';
+ foreach( $titles as $title ) {
+ $dbw->update( 'page',
+ array( 'page_touched' => $timestamp ),
+ array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey() ),
+ $fname );
+ }
+ }
+
+#----------------------------------------------------------------------------
+# Other stuff
+#----------------------------------------------------------------------------
+
+ /** Simple accessors */
+ /**
+ * Get the text form (spaces not underscores) of the main part
+ * @return string
+ * @access public
+ */
+ function getText() { return $this->mTextform; }
+ /**
+ * Get the URL-encoded form of the main part
+ * @return string
+ * @access public
+ */
+ function getPartialURL() { return $this->mUrlform; }
+ /**
+ * Get the main part with underscores
+ * @return string
+ * @access public
+ */
+ function getDBkey() { return $this->mDbkeyform; }
+ /**
+ * Get the namespace index, i.e. one of the NS_xxxx constants
+ * @return int
+ * @access public
+ */
+ function getNamespace() { return $this->mNamespace; }
+ /**
+ * Get the namespace text
+ * @return string
+ * @access public
+ */
+ function getNsText() {
+ global $wgContLang;
+ return $wgContLang->getNsText( $this->mNamespace );
+ }
+ /**
+ * Get the namespace text of the subject (rather than talk) page
+ * @return string
+ * @access public
+ */
+ function getSubjectNsText() {
+ global $wgContLang;
+ return $wgContLang->getNsText( Namespace::getSubject( $this->mNamespace ) );
+ }
+
+ /**
+ * Get the namespace text of the talk page
+ * @return string
+ */
+ function getTalkNsText() {
+ global $wgContLang;
+ return( $wgContLang->getNsText( Namespace::getTalk( $this->mNamespace ) ) );
+ }
+
+ /**
+ * Could this title have a corresponding talk page?
+ * @return bool
+ */
+ function canTalk() {
+ return( Namespace::canTalk( $this->mNamespace ) );
+ }
+
+ /**
+ * Get the interwiki prefix (or null string)
+ * @return string
+ * @access public
+ */
+ function getInterwiki() { return $this->mInterwiki; }
+ /**
+ * Get the Title fragment (i.e. the bit after the #)
+ * @return string
+ * @access public
+ */
+ function getFragment() { return $this->mFragment; }
+ /**
+ * Get the default namespace index, for when there is no namespace
+ * @return int
+ * @access public
+ */
+ function getDefaultNamespace() { return $this->mDefaultNamespace; }
+
+ /**
+ * Get title for search index
+ * @return string a stripped-down title string ready for the
+ * search index
+ */
+ function getIndexTitle() {
+ return Title::indexTitle( $this->mNamespace, $this->mTextform );
+ }
+
+ /**
+ * Get the prefixed database key form
+ * @return string the prefixed title, with underscores and
+ * any interwiki and namespace prefixes
+ * @access public
+ */
+ function getPrefixedDBkey() {
+ $s = $this->prefix( $this->mDbkeyform );
+ $s = str_replace( ' ', '_', $s );
+ return $s;
+ }
+
+ /**
+ * Get the prefixed title with spaces.
+ * This is the form usually used for display
+ * @return string the prefixed title, with spaces
+ * @access public
+ */
+ function getPrefixedText() {
+ if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
+ $s = $this->prefix( $this->mTextform );
+ $s = str_replace( '_', ' ', $s );
+ $this->mPrefixedText = $s;
+ }
+ return $this->mPrefixedText;
+ }
+
+ /**
+ * Get the prefixed title with spaces, plus any fragment
+ * (part beginning with '#')
+ * @return string the prefixed title, with spaces and
+ * the fragment, including '#'
+ * @access public
+ */
+ function getFullText() {
+ $text = $this->getPrefixedText();
+ if( '' != $this->mFragment ) {
+ $text .= '#' . $this->mFragment;
+ }
+ return $text;
+ }
+
+ /**
+ * Get the base name, i.e. the leftmost parts before the /
+ * @return string Base name
+ */
+ function getBaseText() {
+ global $wgNamespacesWithSubpages;
+ if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
+ $parts = explode( '/', $this->getText() );
+ # Don't discard the real title if there's no subpage involved
+ if( count( $parts ) > 1 )
+ unset( $parts[ count( $parts ) - 1 ] );
+ return implode( '/', $parts );
+ } else {
+ return $this->getText();
+ }
+ }
+
+ /**
+ * Get the lowest-level subpage name, i.e. the rightmost part after /
+ * @return string Subpage name
+ */
+ function getSubpageText() {
+ global $wgNamespacesWithSubpages;
+ if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
+ $parts = explode( '/', $this->mTextform );
+ return( $parts[ count( $parts ) - 1 ] );
+ } else {
+ return( $this->mTextform );
+ }
+ }
+
+ /**
+ * Get a URL-encoded form of the subpage text
+ * @return string URL-encoded subpage name
+ */
+ function getSubpageUrlForm() {
+ $text = $this->getSubpageText();
+ $text = wfUrlencode( str_replace( ' ', '_', $text ) );
+ $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe
+ return( $text );
+ }
+
+ /**
+ * Get a URL-encoded title (not an actual URL) including interwiki
+ * @return string the URL-encoded form
+ * @access public
+ */
+ function getPrefixedURL() {
+ $s = $this->prefix( $this->mDbkeyform );
+ $s = str_replace( ' ', '_', $s );
+
+ $s = wfUrlencode ( $s ) ;
+
+ # Cleaning up URL to make it look nice -- is this safe?
+ $s = str_replace( '%28', '(', $s );
+ $s = str_replace( '%29', ')', $s );
+
+ return $s;
+ }
+
+ /**
+ * Get a real URL referring to this title, with interwiki link and
+ * fragment
+ *
+ * @param string $query an optional query string, not used
+ * for interwiki links
+ * @return string the URL
+ * @access public
+ */
+ function getFullURL( $query = '' ) {
+ global $wgContLang, $wgServer, $wgRequest;
+
+ if ( '' == $this->mInterwiki ) {
+ $url = $this->getLocalUrl( $query );
+
+ // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
+ // Correct fix would be to move the prepending elsewhere.
+ if ($wgRequest->getVal('action') != 'render') {
+ $url = $wgServer . $url;
+ }
+ } else {
+ $baseUrl = $this->getInterwikiLink( $this->mInterwiki );
+
+ $namespace = $wgContLang->getNsText( $this->mNamespace );
+ if ( '' != $namespace ) {
+ # Can this actually happen? Interwikis shouldn't be parsed.
+ $namespace .= ':';
+ }
+ $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl );
+ if( $query != '' ) {
+ if( false === strpos( $url, '?' ) ) {
+ $url .= '?';
+ } else {
+ $url .= '&';
+ }
+ $url .= $query;
+ }
+ }
+
+ # Finally, add the fragment.
+ if ( '' != $this->mFragment ) {
+ $url .= '#' . $this->mFragment;
+ }
+
+ wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
+ return $url;
+ }
+
+ /**
+ * Get a URL with no fragment or server name. If this page is generated
+ * with action=render, $wgServer is prepended.
+ * @param string $query an optional query string; if not specified,
+ * $wgArticlePath will be used.
+ * @return string the URL
+ * @access public
+ */
+ function getLocalURL( $query = '' ) {
+ global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
+
+ if ( $this->isExternal() ) {
+ $url = $this->getFullURL();
+ if ( $query ) {
+ // This is currently only used for edit section links in the
+ // context of interwiki transclusion. In theory we should
+ // append the query to the end of any existing query string,
+ // but interwiki transclusion is already broken in that case.
+ $url .= "?$query";
+ }
+ } else {
+ $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
+ if ( $query == '' ) {
+ $url = str_replace( '$1', $dbkey, $wgArticlePath );
+ } else {
+ global $wgActionPaths;
+ $url = false;
+ if( !empty( $wgActionPaths ) &&
+ preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
+ {
+ $action = urldecode( $matches[2] );
+ if( isset( $wgActionPaths[$action] ) ) {
+ $query = $matches[1];
+ if( isset( $matches[4] ) ) $query .= $matches[4];
+ $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
+ if( $query != '' ) $url .= '?' . $query;
+ }
+ }
+ if ( $url === false ) {
+ if ( $query == '-' ) {
+ $query = '';
+ }
+ $url = "{$wgScript}?title={$dbkey}&{$query}";
+ }
+ }
+
+ // FIXME: this causes breakage in various places when we
+ // actually expected a local URL and end up with dupe prefixes.
+ if ($wgRequest->getVal('action') == 'render') {
+ $url = $wgServer . $url;
+ }
+ }
+ wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
+ return $url;
+ }
+
+ /**
+ * Get an HTML-escaped version of the URL form, suitable for
+ * using in a link, without a server name or fragment
+ * @param string $query an optional query string
+ * @return string the URL
+ * @access public
+ */
+ function escapeLocalURL( $query = '' ) {
+ return htmlspecialchars( $this->getLocalURL( $query ) );
+ }
+
+ /**
+ * Get an HTML-escaped version of the URL form, suitable for
+ * using in a link, including the server name and fragment
+ *
+ * @return string the URL
+ * @param string $query an optional query string
+ * @access public
+ */
+ function escapeFullURL( $query = '' ) {
+ return htmlspecialchars( $this->getFullURL( $query ) );
+ }
+
+ /**
+ * Get the URL form for an internal link.
+ * - Used in various Squid-related code, in case we have a different
+ * internal hostname for the server from the exposed one.
+ *
+ * @param string $query an optional query string
+ * @return string the URL
+ * @access public
+ */
+ function getInternalURL( $query = '' ) {
+ global $wgInternalServer;
+ $url = $wgInternalServer . $this->getLocalURL( $query );
+ wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
+ return $url;
+ }
+
+ /**
+ * Get the edit URL for this Title
+ * @return string the URL, or a null string if this is an
+ * interwiki link
+ * @access public
+ */
+ function getEditURL() {
+ if ( '' != $this->mInterwiki ) { return ''; }
+ $s = $this->getLocalURL( 'action=edit' );
+
+ return $s;
+ }
+
+ /**
+ * Get the HTML-escaped displayable text form.
+ * Used for the title field in <a> tags.
+ * @return string the text, including any prefixes
+ * @access public
+ */
+ function getEscapedText() {
+ return htmlspecialchars( $this->getPrefixedText() );
+ }
+
+ /**
+ * Is this Title interwiki?
+ * @return boolean
+ * @access public
+ */
+ function isExternal() { return ( '' != $this->mInterwiki ); }
+
+ /**
+ * Is this page "semi-protected" - the *only* protection is autoconfirm?
+ *
+ * @param string Action to check (default: edit)
+ * @return bool
+ */
+ function isSemiProtected( $action = 'edit' ) {
+ $restrictions = $this->getRestrictions( $action );
+ # We do a full compare because this could be an array
+ foreach( $restrictions as $restriction ) {
+ if( strtolower( $restriction ) != 'autoconfirmed' ) {
+ return( false );
+ }
+ }
+ return( true );
+ }
+
+ /**
+ * Does the title correspond to a protected article?
+ * @param string $what the action the page is protected from,
+ * by default checks move and edit
+ * @return boolean
+ * @access public
+ */
+ function isProtected( $action = '' ) {
+ global $wgRestrictionLevels;
+ if ( -1 == $this->mNamespace ) { return true; }
+
+ if( $action == 'edit' || $action == '' ) {
+ $r = $this->getRestrictions( 'edit' );
+ foreach( $wgRestrictionLevels as $level ) {
+ if( in_array( $level, $r ) && $level != '' ) {
+ return( true );
+ }
+ }
+ }
+
+ if( $action == 'move' || $action == '' ) {
+ $r = $this->getRestrictions( 'move' );
+ foreach( $wgRestrictionLevels as $level ) {
+ if( in_array( $level, $r ) && $level != '' ) {
+ return( true );
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Is $wgUser is watching this page?
+ * @return boolean
+ * @access public
+ */
+ function userIsWatching() {
+ global $wgUser;
+
+ if ( is_null( $this->mWatched ) ) {
+ if ( -1 == $this->mNamespace || 0 == $wgUser->getID()) {
+ $this->mWatched = false;
+ } else {
+ $this->mWatched = $wgUser->isWatched( $this );
+ }
+ }
+ return $this->mWatched;
+ }
+
+ /**
+ * Can $wgUser perform $action this page?
+ * @param string $action action that permission needs to be checked for
+ * @return boolean
+ * @private
+ */
+ function userCan($action) {
+ $fname = 'Title::userCan';
+ wfProfileIn( $fname );
+
+ global $wgUser;
+
+ $result = null;
+ wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) );
+ if ( $result !== null ) {
+ wfProfileOut( $fname );
+ return $result;
+ }
+
+ if( NS_SPECIAL == $this->mNamespace ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+ // XXX: This is the code that prevents unprotecting a page in NS_MEDIAWIKI
+ // from taking effect -ævar
+ if( NS_MEDIAWIKI == $this->mNamespace &&
+ !$wgUser->isAllowed('editinterface') ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ if( $this->mDbkeyform == '_' ) {
+ # FIXME: Is this necessary? Shouldn't be allowed anyway...
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ # protect css/js subpages of user pages
+ # XXX: this might be better using restrictions
+ # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
+ if( NS_USER == $this->mNamespace
+ && preg_match("/\\.(css|js)$/", $this->mTextform )
+ && !$wgUser->isAllowed('editinterface')
+ && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ foreach( $this->getRestrictions($action) as $right ) {
+ // Backwards compatibility, rewrite sysop -> protect
+ if ( $right == 'sysop' ) {
+ $right = 'protect';
+ }
+ if( '' != $right && !$wgUser->isAllowed( $right ) ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+ }
+
+ if( $action == 'move' &&
+ !( $this->isMovable() && $wgUser->isAllowed( 'move' ) ) ) {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ if( $action == 'create' ) {
+ if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) ||
+ ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) {
+ return false;
+ }
+ }
+
+ wfProfileOut( $fname );
+ return true;
+ }
+
+ /**
+ * Can $wgUser edit this page?
+ * @return boolean
+ * @access public
+ */
+ function userCanEdit() {
+ return $this->userCan('edit');
+ }
+
+ /**
+ * Can $wgUser create this page?
+ * @return boolean
+ * @access public
+ */
+ function userCanCreate() {
+ return $this->userCan('create');
+ }
+
+ /**
+ * Can $wgUser move this page?
+ * @return boolean
+ * @access public
+ */
+ function userCanMove() {
+ return $this->userCan('move');
+ }
+
+ /**
+ * Would anybody with sufficient privileges be able to move this page?
+ * Some pages just aren't movable.
+ *
+ * @return boolean
+ * @access public
+ */
+ function isMovable() {
+ return Namespace::isMovable( $this->getNamespace() )
+ && $this->getInterwiki() == '';
+ }
+
+ /**
+ * Can $wgUser read this page?
+ * @return boolean
+ * @access public
+ */
+ function userCanRead() {
+ global $wgUser;
+
+ $result = null;
+ wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
+ if ( $result !== null ) {
+ return $result;
+ }
+
+ if( $wgUser->isAllowed('read') ) {
+ return true;
+ } else {
+ global $wgWhitelistRead;
+
+ /** If anon users can create an account,
+ they need to reach the login page first! */
+ if( $wgUser->isAllowed( 'createaccount' )
+ && $this->getNamespace() == NS_SPECIAL
+ && $this->getText() == 'Userlogin' ) {
+ return true;
+ }
+
+ /** some pages are explicitly allowed */
+ $name = $this->getPrefixedText();
+ if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) {
+ return true;
+ }
+
+ # Compatibility with old settings
+ if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) {
+ if( in_array( ':' . $name, $wgWhitelistRead ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Is this a talk page of some sort?
+ * @return bool
+ * @access public
+ */
+ function isTalkPage() {
+ return Namespace::isTalk( $this->getNamespace() );
+ }
+
+ /**
+ * Is this a .css or .js subpage of a user page?
+ * @return bool
+ * @access public
+ */
+ function isCssJsSubpage() {
+ return ( NS_USER == $this->mNamespace and preg_match("/\\.(css|js)$/", $this->mTextform ) );
+ }
+ /**
+ * Is this a *valid* .css or .js subpage of a user page?
+ * Check that the corresponding skin exists
+ */
+ function isValidCssJsSubpage() {
+ if ( $this->isCssJsSubpage() ) {
+ $skinNames = Skin::getSkinNames();
+ return array_key_exists( $this->getSkinFromCssJsSubpage(), $skinNames );
+ } else {
+ return false;
+ }
+ }
+ /**
+ * Trim down a .css or .js subpage title to get the corresponding skin name
+ */
+ function getSkinFromCssJsSubpage() {
+ $subpage = explode( '/', $this->mTextform );
+ $subpage = $subpage[ count( $subpage ) - 1 ];
+ return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
+ }
+ /**
+ * Is this a .css subpage of a user page?
+ * @return bool
+ * @access public
+ */
+ function isCssSubpage() {
+ return ( NS_USER == $this->mNamespace and preg_match("/\\.css$/", $this->mTextform ) );
+ }
+ /**
+ * Is this a .js subpage of a user page?
+ * @return bool
+ * @access public
+ */
+ function isJsSubpage() {
+ return ( NS_USER == $this->mNamespace and preg_match("/\\.js$/", $this->mTextform ) );
+ }
+ /**
+ * Protect css/js subpages of user pages: can $wgUser edit
+ * this page?
+ *
+ * @return boolean
+ * @todo XXX: this might be better using restrictions
+ * @access public
+ */
+ function userCanEditCssJsSubpage() {
+ global $wgUser;
+ return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
+ }
+
+ /**
+ * Loads a string into mRestrictions array
+ * @param string $res restrictions in string format
+ * @access public
+ */
+ function loadRestrictions( $res ) {
+ foreach( explode( ':', trim( $res ) ) as $restrict ) {
+ $temp = explode( '=', trim( $restrict ) );
+ if(count($temp) == 1) {
+ // old format should be treated as edit/move restriction
+ $this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) );
+ $this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) );
+ } else {
+ $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
+ }
+ }
+ $this->mRestrictionsLoaded = true;
+ }
+
+ /**
+ * Accessor/initialisation for mRestrictions
+ * @param string $action action that permission needs to be checked for
+ * @return array the array of groups allowed to edit this article
+ * @access public
+ */
+ function getRestrictions($action) {
+ $id = $this->getArticleID();
+ if ( 0 == $id ) { return array(); }
+
+ if ( ! $this->mRestrictionsLoaded ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->selectField( 'page', 'page_restrictions', 'page_id='.$id );
+ $this->loadRestrictions( $res );
+ }
+ if( isset( $this->mRestrictions[$action] ) ) {
+ return $this->mRestrictions[$action];
+ }
+ return array();
+ }
+
+ /**
+ * Is there a version of this page in the deletion archive?
+ * @return int the number of archived revisions
+ * @access public
+ */
+ function isDeleted() {
+ $fname = 'Title::isDeleted';
+ if ( $this->getNamespace() < 0 ) {
+ $n = 0;
+ } else {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(),
+ 'ar_title' => $this->getDBkey() ), $fname );
+ if( $this->getNamespace() == NS_IMAGE ) {
+ $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
+ array( 'fa_name' => $this->getDBkey() ), $fname );
+ }
+ }
+ return (int)$n;
+ }
+
+ /**
+ * Get the article ID for this Title from the link cache,
+ * adding it if necessary
+ * @param int $flags a bit field; may be GAID_FOR_UPDATE to select
+ * for update
+ * @return int the ID
+ * @access public
+ */
+ function getArticleID( $flags = 0 ) {
+ $linkCache =& LinkCache::singleton();
+ if ( $flags & GAID_FOR_UPDATE ) {
+ $oldUpdate = $linkCache->forUpdate( true );
+ $this->mArticleID = $linkCache->addLinkObj( $this );
+ $linkCache->forUpdate( $oldUpdate );
+ } else {
+ if ( -1 == $this->mArticleID ) {
+ $this->mArticleID = $linkCache->addLinkObj( $this );
+ }
+ }
+ return $this->mArticleID;
+ }
+
+ function getLatestRevID() {
+ if ($this->mLatestID !== false)
+ return $this->mLatestID;
+
+ $db =& wfGetDB(DB_SLAVE);
+ return $this->mLatestID = $db->selectField( 'revision',
+ "max(rev_id)",
+ array('rev_page' => $this->getArticleID()),
+ 'Title::getLatestRevID' );
+ }
+
+ /**
+ * This clears some fields in this object, and clears any associated
+ * keys in the "bad links" section of the link cache.
+ *
+ * - This is called from Article::insertNewArticle() to allow
+ * loading of the new page_id. It's also called from
+ * Article::doDeleteArticle()
+ *
+ * @param int $newid the new Article ID
+ * @access public
+ */
+ function resetArticleID( $newid ) {
+ $linkCache =& LinkCache::singleton();
+ $linkCache->clearBadLink( $this->getPrefixedDBkey() );
+
+ if ( 0 == $newid ) { $this->mArticleID = -1; }
+ else { $this->mArticleID = $newid; }
+ $this->mRestrictionsLoaded = false;
+ $this->mRestrictions = array();
+ }
+
+ /**
+ * Updates page_touched for this page; called from LinksUpdate.php
+ * @return bool true if the update succeded
+ * @access public
+ */
+ function invalidateCache() {
+ global $wgUseFileCache;
+
+ if ( wfReadOnly() ) {
+ return;
+ }
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $success = $dbw->update( 'page',
+ array( /* SET */
+ 'page_touched' => $dbw->timestamp()
+ ), array( /* WHERE */
+ 'page_namespace' => $this->getNamespace() ,
+ 'page_title' => $this->getDBkey()
+ ), 'Title::invalidateCache'
+ );
+
+ if ($wgUseFileCache) {
+ $cache = new CacheManager($this);
+ @unlink($cache->fileCacheName());
+ }
+
+ return $success;
+ }
+
+ /**
+ * Prefix some arbitrary text with the namespace or interwiki prefix
+ * of this object
+ *
+ * @param string $name the text
+ * @return string the prefixed text
+ * @private
+ */
+ /* private */ function prefix( $name ) {
+ global $wgContLang;
+
+ $p = '';
+ if ( '' != $this->mInterwiki ) {
+ $p = $this->mInterwiki . ':';
+ }
+ if ( 0 != $this->mNamespace ) {
+ $p .= $wgContLang->getNsText( $this->mNamespace ) . ':';
+ }
+ return $p . $name;
+ }
+
+ /**
+ * Secure and split - main initialisation function for this object
+ *
+ * Assumes that mDbkeyform has been set, and is urldecoded
+ * and uses underscores, but not otherwise munged. This function
+ * removes illegal characters, splits off the interwiki and
+ * namespace prefixes, sets the other forms, and canonicalizes
+ * everything.
+ * @return bool true on success
+ * @private
+ */
+ /* private */ function secureAndSplit() {
+ global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks;
+ $fname = 'Title::secureAndSplit';
+
+ # Initialisation
+ static $rxTc = false;
+ if( !$rxTc ) {
+ # % is needed as well
+ $rxTc = '/[^' . Title::legalChars() . ']|%[0-9A-Fa-f]{2}/S';
+ }
+
+ $this->mInterwiki = $this->mFragment = '';
+ $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
+
+ # Clean up whitespace
+ #
+ $t = preg_replace( '/[ _]+/', '_', $this->mDbkeyform );
+ $t = trim( $t, '_' );
+
+ if ( '' == $t ) {
+ return false;
+ }
+
+ if( false !== strpos( $t, UTF8_REPLACEMENT ) ) {
+ # Contained illegal UTF-8 sequences or forbidden Unicode chars.
+ return false;
+ }
+
+ $this->mDbkeyform = $t;
+
+ # Initial colon indicates main namespace rather than specified default
+ # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
+ if ( ':' == $t{0} ) {
+ $this->mNamespace = NS_MAIN;
+ $t = substr( $t, 1 ); # remove the colon but continue processing
+ }
+
+ # Namespace or interwiki prefix
+ $firstPass = true;
+ do {
+ if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $t, $m ) ) {
+ $p = $m[1];
+ $lowerNs = strtolower( $p );
+ if ( $ns = Namespace::getCanonicalIndex( $lowerNs ) ) {
+ # Canonical namespace
+ $t = $m[2];
+ $this->mNamespace = $ns;
+ } elseif ( $ns = $wgContLang->getNsIndex( $lowerNs )) {
+ # Ordinary namespace
+ $t = $m[2];
+ $this->mNamespace = $ns;
+ } elseif( $this->getInterwikiLink( $p ) ) {
+ if( !$firstPass ) {
+ # Can't make a local interwiki link to an interwiki link.
+ # That's just crazy!
+ return false;
+ }
+
+ # Interwiki link
+ $t = $m[2];
+ $this->mInterwiki = strtolower( $p );
+
+ # Redundant interwiki prefix to the local wiki
+ if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) {
+ if( $t == '' ) {
+ # Can't have an empty self-link
+ return false;
+ }
+ $this->mInterwiki = '';
+ $firstPass = false;
+ # Do another namespace split...
+ continue;
+ }
+
+ # If there's an initial colon after the interwiki, that also
+ # resets the default namespace
+ if ( $t !== '' && $t[0] == ':' ) {
+ $this->mNamespace = NS_MAIN;
+ $t = substr( $t, 1 );
+ }
+ }
+ # If there's no recognized interwiki or namespace,
+ # then let the colon expression be part of the title.
+ }
+ break;
+ } while( true );
+ $r = $t;
+
+ # We already know that some pages won't be in the database!
+ #
+ if ( '' != $this->mInterwiki || -1 == $this->mNamespace ) {
+ $this->mArticleID = 0;
+ }
+ $f = strstr( $r, '#' );
+ if ( false !== $f ) {
+ $this->mFragment = substr( $f, 1 );
+ $r = substr( $r, 0, strlen( $r ) - strlen( $f ) );
+ # remove whitespace again: prevents "Foo_bar_#"
+ # becoming "Foo_bar_"
+ $r = preg_replace( '/_*$/', '', $r );
+ }
+
+ # Reject illegal characters.
+ #
+ if( preg_match( $rxTc, $r ) ) {
+ return false;
+ }
+
+ /**
+ * Pages with "/./" or "/../" appearing in the URLs will
+ * often be unreachable due to the way web browsers deal
+ * with 'relative' URLs. Forbid them explicitly.
+ */
+ if ( strpos( $r, '.' ) !== false &&
+ ( $r === '.' || $r === '..' ||
+ strpos( $r, './' ) === 0 ||
+ strpos( $r, '../' ) === 0 ||
+ strpos( $r, '/./' ) !== false ||
+ strpos( $r, '/../' ) !== false ) )
+ {
+ return false;
+ }
+
+ # We shouldn't need to query the DB for the size.
+ #$maxSize = $dbr->textFieldSize( 'page', 'page_title' );
+ if ( strlen( $r ) > 255 ) {
+ return false;
+ }
+
+ /**
+ * Normally, all wiki links are forced to have
+ * an initial capital letter so [[foo]] and [[Foo]]
+ * point to the same place.
+ *
+ * Don't force it for interwikis, since the other
+ * site might be case-sensitive.
+ */
+ if( $wgCapitalLinks && $this->mInterwiki == '') {
+ $t = $wgContLang->ucfirst( $r );
+ } else {
+ $t = $r;
+ }
+
+ /**
+ * Can't make a link to a namespace alone...
+ * "empty" local links can only be self-links
+ * with a fragment identifier.
+ */
+ if( $t == '' &&
+ $this->mInterwiki == '' &&
+ $this->mNamespace != NS_MAIN ) {
+ return false;
+ }
+
+ // Any remaining initial :s are illegal.
+ if ( $t !== '' && ':' == $t{0} ) {
+ return false;
+ }
+
+ # Fill fields
+ $this->mDbkeyform = $t;
+ $this->mUrlform = wfUrlencode( $t );
+
+ $this->mTextform = str_replace( '_', ' ', $t );
+
+ return true;
+ }
+
+ /**
+ * Get a Title object associated with the talk page of this article
+ * @return Title the object for the talk page
+ * @access public
+ */
+ function getTalkPage() {
+ return Title::makeTitle( Namespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+ }
+
+ /**
+ * Get a title object associated with the subject page of this
+ * talk page
+ *
+ * @return Title the object for the subject page
+ * @access public
+ */
+ function getSubjectPage() {
+ return Title::makeTitle( Namespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
+ }
+
+ /**
+ * Get an array of Title objects linking to this Title
+ * Also stores the IDs in the link cache.
+ *
+ * WARNING: do not use this function on arbitrary user-supplied titles!
+ * On heavily-used templates it will max out the memory.
+ *
+ * @param string $options may be FOR UPDATE
+ * @return array the Title objects linking here
+ * @access public
+ */
+ function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) {
+ $linkCache =& LinkCache::singleton();
+ $id = $this->getArticleID();
+
+ if ( $options ) {
+ $db =& wfGetDB( DB_MASTER );
+ } else {
+ $db =& wfGetDB( DB_SLAVE );
+ }
+
+ $res = $db->select( array( 'page', $table ),
+ array( 'page_namespace', 'page_title', 'page_id' ),
+ array(
+ "{$prefix}_from=page_id",
+ "{$prefix}_namespace" => $this->getNamespace(),
+ "{$prefix}_title" => $this->getDbKey() ),
+ 'Title::getLinksTo',
+ $options );
+
+ $retVal = array();
+ if ( $db->numRows( $res ) ) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
+ $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
+ $retVal[] = $titleObj;
+ }
+ }
+ }
+ $db->freeResult( $res );
+ return $retVal;
+ }
+
+ /**
+ * Get an array of Title objects using this Title as a template
+ * Also stores the IDs in the link cache.
+ *
+ * WARNING: do not use this function on arbitrary user-supplied titles!
+ * On heavily-used templates it will max out the memory.
+ *
+ * @param string $options may be FOR UPDATE
+ * @return array the Title objects linking here
+ * @access public
+ */
+ function getTemplateLinksTo( $options = '' ) {
+ return $this->getLinksTo( $options, 'templatelinks', 'tl' );
+ }
+
+ /**
+ * Get an array of Title objects referring to non-existent articles linked from this page
+ *
+ * @param string $options may be FOR UPDATE
+ * @return array the Title objects
+ * @access public
+ */
+ function getBrokenLinksFrom( $options = '' ) {
+ if ( $options ) {
+ $db =& wfGetDB( DB_MASTER );
+ } else {
+ $db =& wfGetDB( DB_SLAVE );
+ }
+
+ $res = $db->safeQuery(
+ "SELECT pl_namespace, pl_title
+ FROM !
+ LEFT JOIN !
+ ON pl_namespace=page_namespace
+ AND pl_title=page_title
+ WHERE pl_from=?
+ AND page_namespace IS NULL
+ !",
+ $db->tableName( 'pagelinks' ),
+ $db->tableName( 'page' ),
+ $this->getArticleId(),
+ $options );
+
+ $retVal = array();
+ if ( $db->numRows( $res ) ) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
+ }
+ }
+ $db->freeResult( $res );
+ return $retVal;
+ }
+
+
+ /**
+ * Get a list of URLs to purge from the Squid cache when this
+ * page changes
+ *
+ * @return array the URLs
+ * @access public
+ */
+ function getSquidURLs() {
+ return array(
+ $this->getInternalURL(),
+ $this->getInternalURL( 'action=history' )
+ );
+ }
+
+ function purgeSquid() {
+ global $wgUseSquid;
+ if ( $wgUseSquid ) {
+ $urls = $this->getSquidURLs();
+ $u = new SquidUpdate( $urls );
+ $u->doUpdate();
+ }
+ }
+
+ /**
+ * Move this page without authentication
+ * @param Title &$nt the new page Title
+ * @access public
+ */
+ function moveNoAuth( &$nt ) {
+ return $this->moveTo( $nt, false );
+ }
+
+ /**
+ * Check whether a given move operation would be valid.
+ * Returns true if ok, or a message key string for an error message
+ * if invalid. (Scarrrrry ugly interface this.)
+ * @param Title &$nt the new title
+ * @param bool $auth indicates whether $wgUser's permissions
+ * should be checked
+ * @return mixed true on success, message name on failure
+ * @access public
+ */
+ function isValidMoveOperation( &$nt, $auth = true ) {
+ if( !$this or !$nt ) {
+ return 'badtitletext';
+ }
+ if( $this->equals( $nt ) ) {
+ return 'selfmove';
+ }
+ if( !$this->isMovable() || !$nt->isMovable() ) {
+ return 'immobile_namespace';
+ }
+
+ $oldid = $this->getArticleID();
+ $newid = $nt->getArticleID();
+
+ if ( strlen( $nt->getDBkey() ) < 1 ) {
+ return 'articleexists';
+ }
+ if ( ( '' == $this->getDBkey() ) ||
+ ( !$oldid ) ||
+ ( '' == $nt->getDBkey() ) ) {
+ return 'badarticleerror';
+ }
+
+ if ( $auth && (
+ !$this->userCanEdit() || !$nt->userCanEdit() ||
+ !$this->userCanMove() || !$nt->userCanMove() ) ) {
+ return 'protectedpage';
+ }
+
+ # The move is allowed only if (1) the target doesn't exist, or
+ # (2) the target is a redirect to the source, and has no history
+ # (so we can undo bad moves right after they're done).
+
+ if ( 0 != $newid ) { # Target exists; check for validity
+ if ( ! $this->isValidMoveTarget( $nt ) ) {
+ return 'articleexists';
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Move a title to a new location
+ * @param Title &$nt the new title
+ * @param bool $auth indicates whether $wgUser's permissions
+ * should be checked
+ * @return mixed true on success, message name on failure
+ * @access public
+ */
+ function moveTo( &$nt, $auth = true, $reason = '' ) {
+ $err = $this->isValidMoveOperation( $nt, $auth );
+ if( is_string( $err ) ) {
+ return $err;
+ }
+
+ $pageid = $this->getArticleID();
+ if( $nt->exists() ) {
+ $this->moveOverExistingRedirect( $nt, $reason );
+ $pageCountChange = 0;
+ } else { # Target didn't exist, do normal move.
+ $this->moveToNewTitle( $nt, $reason );
+ $pageCountChange = 1;
+ }
+ $redirid = $this->getArticleID();
+
+ # Fixing category links (those without piped 'alternate' names) to be sorted under the new title
+ $dbw =& wfGetDB( DB_MASTER );
+ $categorylinks = $dbw->tableName( 'categorylinks' );
+ $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) .
+ " WHERE cl_from=" . $dbw->addQuotes( $pageid ) .
+ " AND cl_sortkey=" . $dbw->addQuotes( $this->getPrefixedText() );
+ $dbw->query( $sql, 'SpecialMovepage::doSubmit' );
+
+ # Update watchlists
+
+ $oldnamespace = $this->getNamespace() & ~1;
+ $newnamespace = $nt->getNamespace() & ~1;
+ $oldtitle = $this->getDBkey();
+ $newtitle = $nt->getDBkey();
+
+ if( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) {
+ WatchedItem::duplicateEntries( $this, $nt );
+ }
+
+ # Update search engine
+ $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
+ $u->doUpdate();
+ $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
+ $u->doUpdate();
+
+ # Update site_stats
+ if ( $this->getNamespace() == NS_MAIN and $nt->getNamespace() != NS_MAIN ) {
+ # Moved out of main namespace
+ # not viewed, edited, removing
+ $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange);
+ } elseif ( $this->getNamespace() != NS_MAIN and $nt->getNamespace() == NS_MAIN ) {
+ # Moved into main namespace
+ # not viewed, edited, adding
+ $u = new SiteStatsUpdate( 0, 1, +1, $pageCountChange );
+ } elseif ( $pageCountChange ) {
+ # Added redirect
+ $u = new SiteStatsUpdate( 0, 0, 0, 1 );
+ } else{
+ $u = false;
+ }
+ if ( $u ) {
+ $u->doUpdate();
+ }
+
+ global $wgUser;
+ wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
+ return true;
+ }
+
+ /**
+ * Move page to a title which is at present a redirect to the
+ * source page
+ *
+ * @param Title &$nt the page to move to, which should currently
+ * be a redirect
+ * @private
+ */
+ function moveOverExistingRedirect( &$nt, $reason = '' ) {
+ global $wgUseSquid;
+ $fname = 'Title::moveOverExistingRedirect';
+ $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
+
+ if ( $reason ) {
+ $comment .= ": $reason";
+ }
+
+ $now = wfTimestampNow();
+ $rand = wfRandom();
+ $newid = $nt->getArticleID();
+ $oldid = $this->getArticleID();
+ $dbw =& wfGetDB( DB_MASTER );
+ $linkCache =& LinkCache::singleton();
+
+ # Delete the old redirect. We don't save it to history since
+ # by definition if we've got here it's rather uninteresting.
+ # We have to remove it so that the next step doesn't trigger
+ # a conflict on the unique namespace+title index...
+ $dbw->delete( 'page', array( 'page_id' => $newid ), $fname );
+
+ # Save a null revision in the page's history notifying of the move
+ $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
+ $nullRevId = $nullRevision->insertOn( $dbw );
+
+ # Change the name of the target page:
+ $dbw->update( 'page',
+ /* SET */ array(
+ 'page_touched' => $dbw->timestamp($now),
+ 'page_namespace' => $nt->getNamespace(),
+ 'page_title' => $nt->getDBkey(),
+ 'page_latest' => $nullRevId,
+ ),
+ /* WHERE */ array( 'page_id' => $oldid ),
+ $fname
+ );
+ $linkCache->clearLink( $nt->getPrefixedDBkey() );
+
+ # Recreate the redirect, this time in the other direction.
+ $mwRedir = MagicWord::get( MAG_REDIRECT );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $this );
+ $newid = $redirectArticle->insertOn( $dbw );
+ $redirectRevision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $revid = $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+ $linkCache->clearLink( $this->getPrefixedDBkey() );
+
+ # Log the move
+ $log = new LogPage( 'move' );
+ $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
+
+ # Now, we record the link from the redirect to the new title.
+ # It should have no other outgoing links...
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname );
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $newid,
+ 'pl_namespace' => $nt->getNamespace(),
+ 'pl_title' => $nt->getDbKey() ),
+ $fname );
+
+ # Purge squid
+ if ( $wgUseSquid ) {
+ $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() );
+ $u = new SquidUpdate( $urls );
+ $u->doUpdate();
+ }
+ }
+
+ /**
+ * Move page to non-existing title.
+ * @param Title &$nt the new Title
+ * @private
+ */
+ function moveToNewTitle( &$nt, $reason = '' ) {
+ global $wgUseSquid;
+ $fname = 'MovePageForm::moveToNewTitle';
+ $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
+ if ( $reason ) {
+ $comment .= ": $reason";
+ }
+
+ $newid = $nt->getArticleID();
+ $oldid = $this->getArticleID();
+ $dbw =& wfGetDB( DB_MASTER );
+ $now = $dbw->timestamp();
+ $rand = wfRandom();
+ $linkCache =& LinkCache::singleton();
+
+ # Save a null revision in the page's history notifying of the move
+ $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
+ $nullRevId = $nullRevision->insertOn( $dbw );
+
+ # Rename cur entry
+ $dbw->update( 'page',
+ /* SET */ array(
+ 'page_touched' => $now,
+ 'page_namespace' => $nt->getNamespace(),
+ 'page_title' => $nt->getDBkey(),
+ 'page_latest' => $nullRevId,
+ ),
+ /* WHERE */ array( 'page_id' => $oldid ),
+ $fname
+ );
+
+ $linkCache->clearLink( $nt->getPrefixedDBkey() );
+
+ # Insert redirect
+ $mwRedir = MagicWord::get( MAG_REDIRECT );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $this );
+ $newid = $redirectArticle->insertOn( $dbw );
+ $redirectRevision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $revid = $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+ $linkCache->clearLink( $this->getPrefixedDBkey() );
+
+ # Log the move
+ $log = new LogPage( 'move' );
+ $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText()) );
+
+ # Purge caches as per article creation
+ Article::onArticleCreate( $nt );
+
+ # Record the just-created redirect's linking to the page
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $newid,
+ 'pl_namespace' => $nt->getNamespace(),
+ 'pl_title' => $nt->getDBkey() ),
+ $fname );
+
+ # Purge old title from squid
+ # The new title, and links to the new title, are purged in Article::onArticleCreate()
+ $this->purgeSquid();
+ }
+
+ /**
+ * Checks if $this can be moved to a given Title
+ * - Selects for update, so don't call it unless you mean business
+ *
+ * @param Title &$nt the new title to check
+ * @access public
+ */
+ function isValidMoveTarget( $nt ) {
+
+ $fname = 'Title::isValidMoveTarget';
+ $dbw =& wfGetDB( DB_MASTER );
+
+ # Is it a redirect?
+ $id = $nt->getArticleID();
+ $obj = $dbw->selectRow( array( 'page', 'revision', 'text'),
+ array( 'page_is_redirect','old_text','old_flags' ),
+ array( 'page_id' => $id, 'page_latest=rev_id', 'rev_text_id=old_id' ),
+ $fname, 'FOR UPDATE' );
+
+ if ( !$obj || 0 == $obj->page_is_redirect ) {
+ # Not a redirect
+ wfDebug( __METHOD__ . ": not a redirect\n" );
+ return false;
+ }
+ $text = Revision::getRevisionText( $obj );
+
+ # Does the redirect point to the source?
+ # Or is it a broken self-redirect, usually caused by namespace collisions?
+ if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
+ $redirTitle = Title::newFromText( $m[1] );
+ if( !is_object( $redirTitle ) ||
+ ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
+ $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+ wfDebug( __METHOD__ . ": redirect points to other page\n" );
+ return false;
+ }
+ } else {
+ # Fail safe
+ wfDebug( __METHOD__ . ": failsafe\n" );
+ return false;
+ }
+
+ # Does the article have a history?
+ $row = $dbw->selectRow( array( 'page', 'revision'),
+ array( 'rev_id' ),
+ array( 'page_namespace' => $nt->getNamespace(),
+ 'page_title' => $nt->getDBkey(),
+ 'page_id=rev_page AND page_latest != rev_id'
+ ), $fname, 'FOR UPDATE'
+ );
+
+ # Return true if there was no history
+ return $row === false;
+ }
+
+ /**
+ * Create a redirect; fails if the title already exists; does
+ * not notify RC
+ *
+ * @param Title $dest the destination of the redirect
+ * @param string $comment the comment string describing the move
+ * @return bool true on success
+ * @access public
+ */
+ function createRedirect( $dest, $comment ) {
+ if ( $this->getArticleID() ) {
+ return false;
+ }
+
+ $fname = 'Title::createRedirect';
+ $dbw =& wfGetDB( DB_MASTER );
+
+ $article = new Article( $this );
+ $newid = $article->insertOn( $dbw );
+ $revision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => "#REDIRECT [[" . $dest->getPrefixedText() . "]]\n",
+ ) );
+ $revisionId = $revision->insertOn( $dbw );
+ $article->updateRevisionOn( $dbw, $revision, 0 );
+
+ # Link table
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $newid,
+ 'pl_namespace' => $dest->getNamespace(),
+ 'pl_title' => $dest->getDbKey()
+ ), $fname
+ );
+
+ Article::onArticleCreate( $this );
+ return true;
+ }
+
+ /**
+ * Get categories to which this Title belongs and return an array of
+ * categories' names.
+ *
+ * @return array an array of parents in the form:
+ * $parent => $currentarticle
+ * @access public
+ */
+ function getParentCategories() {
+ global $wgContLang;
+
+ $titlekey = $this->getArticleId();
+ $dbr =& wfGetDB( DB_SLAVE );
+ $categorylinks = $dbr->tableName( 'categorylinks' );
+
+ # NEW SQL
+ $sql = "SELECT * FROM $categorylinks"
+ ." WHERE cl_from='$titlekey'"
+ ." AND cl_from <> '0'"
+ ." ORDER BY cl_sortkey";
+
+ $res = $dbr->query ( $sql ) ;
+
+ if($dbr->numRows($res) > 0) {
+ while ( $x = $dbr->fetchObject ( $res ) )
+ //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to);
+ $data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
+ $dbr->freeResult ( $res ) ;
+ } else {
+ $data = '';
+ }
+ return $data;
+ }
+
+ /**
+ * Get a tree of parent categories
+ * @param array $children an array with the children in the keys, to check for circular refs
+ * @return array
+ * @access public
+ */
+ function getParentCategoryTree( $children = array() ) {
+ $parents = $this->getParentCategories();
+
+ if($parents != '') {
+ foreach($parents as $parent => $current) {
+ if ( array_key_exists( $parent, $children ) ) {
+ # Circular reference
+ $stack[$parent] = array();
+ } else {
+ $nt = Title::newFromText($parent);
+ $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) );
+ }
+ }
+ return $stack;
+ } else {
+ return array();
+ }
+ }
+
+
+ /**
+ * Get an associative array for selecting this title from
+ * the "page" table
+ *
+ * @return array
+ * @access public
+ */
+ function pageCond() {
+ return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
+ }
+
+ /**
+ * Get the revision ID of the previous revision
+ *
+ * @param integer $revision Revision ID. Get the revision that was before this one.
+ * @return interger $oldrevision|false
+ */
+ function getPreviousRevisionID( $revision ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $dbr->selectField( 'revision', 'rev_id',
+ 'rev_page=' . intval( $this->getArticleId() ) .
+ ' AND rev_id<' . intval( $revision ) . ' ORDER BY rev_id DESC' );
+ }
+
+ /**
+ * Get the revision ID of the next revision
+ *
+ * @param integer $revision Revision ID. Get the revision that was after this one.
+ * @return interger $oldrevision|false
+ */
+ function getNextRevisionID( $revision ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $dbr->selectField( 'revision', 'rev_id',
+ 'rev_page=' . intval( $this->getArticleId() ) .
+ ' AND rev_id>' . intval( $revision ) . ' ORDER BY rev_id' );
+ }
+
+ /**
+ * Compare with another title.
+ *
+ * @param Title $title
+ * @return bool
+ */
+ function equals( $title ) {
+ // Note: === is necessary for proper matching of number-like titles.
+ return $this->getInterwiki() === $title->getInterwiki()
+ && $this->getNamespace() == $title->getNamespace()
+ && $this->getDbkey() === $title->getDbkey();
+ }
+
+ /**
+ * Check if page exists
+ * @return bool
+ */
+ function exists() {
+ return $this->getArticleId() != 0;
+ }
+
+ /**
+ * Should a link should be displayed as a known link, just based on its title?
+ *
+ * Currently, a self-link with a fragment and special pages are in
+ * this category. Special pages never exist in the database.
+ */
+ function isAlwaysKnown() {
+ return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform )
+ || NS_SPECIAL == $this->mNamespace;
+ }
+
+ /**
+ * Update page_touched timestamps and send squid purge messages for
+ * pages linking to this title. May be sent to the job queue depending
+ * on the number of links. Typically called on create and delete.
+ */
+ function touchLinks() {
+ $u = new HTMLCacheUpdate( $this, 'pagelinks' );
+ $u->doUpdate();
+
+ if ( $this->getNamespace() == NS_CATEGORY ) {
+ $u = new HTMLCacheUpdate( $this, 'categorylinks' );
+ $u->doUpdate();
+ }
+ }
+
+ function trackbackURL() {
+ global $wgTitle, $wgScriptPath, $wgServer;
+
+ return "$wgServer$wgScriptPath/trackback.php?article="
+ . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey()));
+ }
+
+ function trackbackRDF() {
+ $url = htmlspecialchars($this->getFullURL());
+ $title = htmlspecialchars($this->getText());
+ $tburl = $this->trackbackURL();
+
+ return "
+<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
+ xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
+ xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
+<rdf:Description
+ rdf:about=\"$url\"
+ dc:identifier=\"$url\"
+ dc:title=\"$title\"
+ trackback:ping=\"$tburl\" />
+</rdf:RDF>";
+ }
+
+ /**
+ * Generate strings used for xml 'id' names in monobook tabs
+ * @return string
+ */
+ function getNamespaceKey() {
+ switch ($this->getNamespace()) {
+ case NS_MAIN:
+ case NS_TALK:
+ return 'nstab-main';
+ case NS_USER:
+ case NS_USER_TALK:
+ return 'nstab-user';
+ case NS_MEDIA:
+ return 'nstab-media';
+ case NS_SPECIAL:
+ return 'nstab-special';
+ case NS_PROJECT:
+ case NS_PROJECT_TALK:
+ return 'nstab-project';
+ case NS_IMAGE:
+ case NS_IMAGE_TALK:
+ return 'nstab-image';
+ case NS_MEDIAWIKI:
+ case NS_MEDIAWIKI_TALK:
+ return 'nstab-mediawiki';
+ case NS_TEMPLATE:
+ case NS_TEMPLATE_TALK:
+ return 'nstab-template';
+ case NS_HELP:
+ case NS_HELP_TALK:
+ return 'nstab-help';
+ case NS_CATEGORY:
+ case NS_CATEGORY_TALK:
+ return 'nstab-category';
+ default:
+ return 'nstab-' . strtolower( $this->getSubjectNsText() );
+ }
+ }
+}
+?>
diff --git a/includes/User.php b/includes/User.php
new file mode 100644
index 00000000..f2426284
--- /dev/null
+++ b/includes/User.php
@@ -0,0 +1,1986 @@
+<?php
+/**
+ * See user.txt
+ *
+ * @package MediaWiki
+ */
+
+# Number of characters in user_token field
+define( 'USER_TOKEN_LENGTH', 32 );
+
+# Serialized record version
+define( 'MW_USER_VERSION', 3 );
+
+/**
+ *
+ * @package MediaWiki
+ */
+class User {
+ /*
+ * When adding a new private variable, dont forget to add it to __sleep()
+ */
+ /**@{{
+ * @private
+ */
+ var $mBlockedby; //!<
+ var $mBlockreason; //!<
+ var $mDataLoaded; //!<
+ var $mEmail; //!<
+ var $mEmailAuthenticated; //!<
+ var $mGroups; //!<
+ var $mHash; //!<
+ var $mId; //!<
+ var $mName; //!<
+ var $mNewpassword; //!<
+ var $mNewtalk; //!<
+ var $mOptions; //!<
+ var $mPassword; //!<
+ var $mRealName; //!<
+ var $mRegistration; //!<
+ var $mRights; //!<
+ var $mSkin; //!<
+ var $mToken; //!<
+ var $mTouched; //!<
+ var $mVersion; //!< serialized version
+ /**@}} */
+
+ /** Constructor using User:loadDefaults() */
+ function User() {
+ $this->loadDefaults();
+ $this->mVersion = MW_USER_VERSION;
+ }
+
+ /**
+ * Static factory method
+ * @param string $name Username, validated by Title:newFromText()
+ * @param bool $validate Validate username
+ * @return User
+ * @static
+ */
+ function newFromName( $name, $validate = true ) {
+ # Force usernames to capital
+ global $wgContLang;
+ $name = $wgContLang->ucfirst( $name );
+
+ # Clean up name according to title rules
+ $t = Title::newFromText( $name );
+ if( is_null( $t ) ) {
+ return null;
+ }
+
+ # Reject various classes of invalid names
+ $canonicalName = $t->getText();
+ global $wgAuth;
+ $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
+
+ if( $validate && !User::isValidUserName( $canonicalName ) ) {
+ return null;
+ }
+
+ $u = new User();
+ $u->setName( $canonicalName );
+ $u->setId( $u->idFromName( $canonicalName ) );
+ return $u;
+ }
+
+ /**
+ * Factory method to fetch whichever use has a given email confirmation code.
+ * This code is generated when an account is created or its e-mail address
+ * has changed.
+ *
+ * If the code is invalid or has expired, returns NULL.
+ *
+ * @param string $code
+ * @return User
+ * @static
+ */
+ function newFromConfirmationCode( $code ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ $name = $dbr->selectField( 'user', 'user_name', array(
+ 'user_email_token' => md5( $code ),
+ 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
+ ) );
+ if( is_string( $name ) ) {
+ return User::newFromName( $name );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Serialze sleep function, for better cache efficiency and avoidance of
+ * silly "incomplete type" errors when skins are cached. The array should
+ * contain names of private variables (see at top of User.php).
+ */
+ function __sleep() {
+ return array(
+'mBlockedby',
+'mBlockreason',
+'mDataLoaded',
+'mEmail',
+'mEmailAuthenticated',
+'mGroups',
+'mHash',
+'mId',
+'mName',
+'mNewpassword',
+'mNewtalk',
+'mOptions',
+'mPassword',
+'mRealName',
+'mRegistration',
+'mRights',
+'mToken',
+'mTouched',
+'mVersion',
+);
+ }
+
+ /**
+ * Get username given an id.
+ * @param integer $id Database user id
+ * @return string Nickname of a user
+ * @static
+ */
+ function whoIs( $id ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
+ }
+
+ /**
+ * Get real username given an id.
+ * @param integer $id Database user id
+ * @return string Realname of a user
+ * @static
+ */
+ function whoIsReal( $id ) {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
+ }
+
+ /**
+ * Get database id given a user name
+ * @param string $name Nickname of a user
+ * @return integer|null Database user id (null: if non existent
+ * @static
+ */
+ function idFromName( $name ) {
+ $fname = "User::idFromName";
+
+ $nt = Title::newFromText( $name );
+ if( is_null( $nt ) ) {
+ # Illegal name
+ return null;
+ }
+ $dbr =& wfGetDB( DB_SLAVE );
+ $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
+
+ if ( $s === false ) {
+ return 0;
+ } else {
+ return $s->user_id;
+ }
+ }
+
+ /**
+ * Does the string match an anonymous IPv4 address?
+ *
+ * This function exists for username validation, in order to reject
+ * usernames which are similar in form to IP addresses. Strings such
+ * as 300.300.300.300 will return true because it looks like an IP
+ * address, despite not being strictly valid.
+ *
+ * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
+ * address because the usemod software would "cloak" anonymous IP
+ * addresses like this, if we allowed accounts like this to be created
+ * new users could get the old edits of these anonymous users.
+ *
+ * @bug 3631
+ *
+ * @static
+ * @param string $name Nickname of a user
+ * @return bool
+ */
+ function isIP( $name ) {
+ return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name);
+ /*return preg_match("/^
+ (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
+ (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
+ (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
+ (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
+ $/x", $name);*/
+ }
+
+ /**
+ * Is the input a valid username?
+ *
+ * Checks if the input is a valid username, we don't want an empty string,
+ * an IP address, anything that containins slashes (would mess up subpages),
+ * is longer than the maximum allowed username size or doesn't begin with
+ * a capital letter.
+ *
+ * @param string $name
+ * @return bool
+ * @static
+ */
+ function isValidUserName( $name ) {
+ global $wgContLang, $wgMaxNameChars;
+
+ if ( $name == ''
+ || User::isIP( $name )
+ || strpos( $name, '/' ) !== false
+ || strlen( $name ) > $wgMaxNameChars
+ || $name != $wgContLang->ucfirst( $name ) )
+ return false;
+
+ // Ensure that the name can't be misresolved as a different title,
+ // such as with extra namespace keys at the start.
+ $parsed = Title::newFromText( $name );
+ if( is_null( $parsed )
+ || $parsed->getNamespace()
+ || strcmp( $name, $parsed->getPrefixedText() ) )
+ return false;
+
+ // Check an additional blacklist of troublemaker characters.
+ // Should these be merged into the title char list?
+ $unicodeBlacklist = '/[' .
+ '\x{0080}-\x{009f}' . # iso-8859-1 control chars
+ '\x{00a0}' . # non-breaking space
+ '\x{2000}-\x{200f}' . # various whitespace
+ '\x{2028}-\x{202f}' . # breaks and control chars
+ '\x{3000}' . # ideographic space
+ '\x{e000}-\x{f8ff}' . # private use
+ ']/u';
+ if( preg_match( $unicodeBlacklist, $name ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Is the input a valid password?
+ *
+ * @param string $password
+ * @return bool
+ * @static
+ */
+ function isValidPassword( $password ) {
+ global $wgMinimalPasswordLength;
+ return strlen( $password ) >= $wgMinimalPasswordLength;
+ }
+
+ /**
+ * Does the string match roughly an email address ?
+ *
+ * There used to be a regular expression here, it got removed because it
+ * rejected valid addresses. Actually just check if there is '@' somewhere
+ * in the given address.
+ *
+ * @todo Check for RFC 2822 compilance
+ * @bug 959
+ *
+ * @param string $addr email address
+ * @static
+ * @return bool
+ */
+ function isValidEmailAddr ( $addr ) {
+ return ( trim( $addr ) != '' ) &&
+ (false !== strpos( $addr, '@' ) );
+ }
+
+ /**
+ * Count the number of edits of a user
+ *
+ * @param int $uid The user ID to check
+ * @return int
+ */
+ function edits( $uid ) {
+ $fname = 'User::edits';
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $dbr->selectField(
+ 'revision', 'count(*)',
+ array( 'rev_user' => $uid ),
+ $fname
+ );
+ }
+
+ /**
+ * probably return a random password
+ * @return string probably a random password
+ * @static
+ * @todo Check what is doing really [AV]
+ */
+ function randomPassword() {
+ global $wgMinimalPasswordLength;
+ $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
+ $l = strlen( $pwchars ) - 1;
+
+ $pwlength = max( 7, $wgMinimalPasswordLength );
+ $digit = mt_rand(0, $pwlength - 1);
+ $np = '';
+ for ( $i = 0; $i < $pwlength; $i++ ) {
+ $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
+ }
+ return $np;
+ }
+
+ /**
+ * Set properties to default
+ * Used at construction. It will load per language default settings only
+ * if we have an available language object.
+ */
+ function loadDefaults() {
+ static $n=0;
+ $n++;
+ $fname = 'User::loadDefaults' . $n;
+ wfProfileIn( $fname );
+
+ global $wgCookiePrefix;
+ global $wgNamespacesToBeSearchedDefault;
+
+ $this->mId = 0;
+ $this->mNewtalk = -1;
+ $this->mName = false;
+ $this->mRealName = $this->mEmail = '';
+ $this->mEmailAuthenticated = null;
+ $this->mPassword = $this->mNewpassword = '';
+ $this->mRights = array();
+ $this->mGroups = array();
+ $this->mOptions = User::getDefaultOptions();
+
+ foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
+ $this->mOptions['searchNs'.$nsnum] = $val;
+ }
+ unset( $this->mSkin );
+ $this->mDataLoaded = false;
+ $this->mBlockedby = -1; # Unset
+ $this->setToken(); # Random
+ $this->mHash = false;
+
+ if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
+ $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
+ }
+ else {
+ $this->mTouched = '0'; # Allow any pages to be cached
+ }
+
+ $this->mRegistration = wfTimestamp( TS_MW );
+
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Combine the language default options with any site-specific options
+ * and add the default language variants.
+ *
+ * @return array
+ * @static
+ * @private
+ */
+ function getDefaultOptions() {
+ /**
+ * Site defaults will override the global/language defaults
+ */
+ global $wgContLang, $wgDefaultUserOptions;
+ $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions();
+
+ /**
+ * default language setting
+ */
+ $variant = $wgContLang->getPreferredVariant();
+ $defOpt['variant'] = $variant;
+ $defOpt['language'] = $variant;
+
+ return $defOpt;
+ }
+
+ /**
+ * Get a given default option value.
+ *
+ * @param string $opt
+ * @return string
+ * @static
+ * @public
+ */
+ function getDefaultOption( $opt ) {
+ $defOpts = User::getDefaultOptions();
+ if( isset( $defOpts[$opt] ) ) {
+ return $defOpts[$opt];
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get blocking information
+ * @private
+ * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
+ * non-critical checks are done against slaves. Check when actually saving should be done against
+ * master.
+ */
+ function getBlockedStatus( $bFromSlave = true ) {
+ global $wgEnableSorbs, $wgProxyWhitelist;
+
+ if ( -1 != $this->mBlockedby ) {
+ wfDebug( "User::getBlockedStatus: already loaded.\n" );
+ return;
+ }
+
+ $fname = 'User::getBlockedStatus';
+ wfProfileIn( $fname );
+ wfDebug( "$fname: checking...\n" );
+
+ $this->mBlockedby = 0;
+ $ip = wfGetIP();
+
+ # User/IP blocking
+ $block = new Block();
+ $block->fromMaster( !$bFromSlave );
+ if ( $block->load( $ip , $this->mId ) ) {
+ wfDebug( "$fname: Found block.\n" );
+ $this->mBlockedby = $block->mBy;
+ $this->mBlockreason = $block->mReason;
+ if ( $this->isLoggedIn() ) {
+ $this->spreadBlock();
+ }
+ } else {
+ wfDebug( "$fname: No block.\n" );
+ }
+
+ # Proxy blocking
+ # FIXME ? proxyunbannable is to deprecate the old isSysop()
+ if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
+
+ # Local list
+ if ( wfIsLocallyBlockedProxy( $ip ) ) {
+ $this->mBlockedby = wfMsg( 'proxyblocker' );
+ $this->mBlockreason = wfMsg( 'proxyblockreason' );
+ }
+
+ # DNSBL
+ if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
+ if ( $this->inSorbsBlacklist( $ip ) ) {
+ $this->mBlockedby = wfMsg( 'sorbs' );
+ $this->mBlockreason = wfMsg( 'sorbsreason' );
+ }
+ }
+ }
+
+ # Extensions
+ wfRunHooks( 'GetBlockedStatus', array( &$this ) );
+
+ wfProfileOut( $fname );
+ }
+
+ function inSorbsBlacklist( $ip ) {
+ global $wgEnableSorbs;
+ return $wgEnableSorbs &&
+ $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
+ }
+
+ function inDnsBlacklist( $ip, $base ) {
+ $fname = 'User::inDnsBlacklist';
+ wfProfileIn( $fname );
+
+ $found = false;
+ $host = '';
+
+ if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
+ # Make hostname
+ for ( $i=4; $i>=1; $i-- ) {
+ $host .= $m[$i] . '.';
+ }
+ $host .= $base;
+
+ # Send query
+ $ipList = gethostbynamel( $host );
+
+ if ( $ipList ) {
+ wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
+ $found = true;
+ } else {
+ wfDebug( "Requested $host, not found in $base.\n" );
+ }
+ }
+
+ wfProfileOut( $fname );
+ return $found;
+ }
+
+ /**
+ * Primitive rate limits: enforce maximum actions per time period
+ * to put a brake on flooding.
+ *
+ * Note: when using a shared cache like memcached, IP-address
+ * last-hit counters will be shared across wikis.
+ *
+ * @return bool true if a rate limiter was tripped
+ * @public
+ */
+ function pingLimiter( $action='edit' ) {
+ global $wgRateLimits, $wgRateLimitsExcludedGroups;
+ if( !isset( $wgRateLimits[$action] ) ) {
+ return false;
+ }
+
+ # Some groups shouldn't trigger the ping limiter, ever
+ foreach( $this->getGroups() as $group ) {
+ if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
+ return false;
+ }
+
+ global $wgMemc, $wgDBname, $wgRateLimitLog;
+ $fname = 'User::pingLimiter';
+ wfProfileIn( $fname );
+
+ $limits = $wgRateLimits[$action];
+ $keys = array();
+ $id = $this->getId();
+ $ip = wfGetIP();
+
+ if( isset( $limits['anon'] ) && $id == 0 ) {
+ $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
+ }
+
+ if( isset( $limits['user'] ) && $id != 0 ) {
+ $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
+ }
+ if( $this->isNewbie() ) {
+ if( isset( $limits['newbie'] ) && $id != 0 ) {
+ $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
+ }
+ if( isset( $limits['ip'] ) ) {
+ $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
+ }
+ if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
+ $subnet = $matches[1];
+ $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
+ }
+ }
+
+ $triggered = false;
+ foreach( $keys as $key => $limit ) {
+ list( $max, $period ) = $limit;
+ $summary = "(limit $max in {$period}s)";
+ $count = $wgMemc->get( $key );
+ if( $count ) {
+ if( $count > $max ) {
+ wfDebug( "$fname: tripped! $key at $count $summary\n" );
+ if( $wgRateLimitLog ) {
+ @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
+ }
+ $triggered = true;
+ } else {
+ wfDebug( "$fname: ok. $key at $count $summary\n" );
+ }
+ } else {
+ wfDebug( "$fname: adding record for $key $summary\n" );
+ $wgMemc->add( $key, 1, intval( $period ) );
+ }
+ $wgMemc->incr( $key );
+ }
+
+ wfProfileOut( $fname );
+ return $triggered;
+ }
+
+ /**
+ * Check if user is blocked
+ * @return bool True if blocked, false otherwise
+ */
+ function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
+ wfDebug( "User::isBlocked: enter\n" );
+ $this->getBlockedStatus( $bFromSlave );
+ return $this->mBlockedby !== 0;
+ }
+
+ /**
+ * Check if user is blocked from editing a particular article
+ */
+ function isBlockedFrom( $title, $bFromSlave = false ) {
+ global $wgBlockAllowsUTEdit;
+ $fname = 'User::isBlockedFrom';
+ wfProfileIn( $fname );
+ wfDebug( "$fname: enter\n" );
+
+ if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
+ $title->getNamespace() == NS_USER_TALK )
+ {
+ $blocked = false;
+ wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
+ } else {
+ wfDebug( "$fname: asking isBlocked()\n" );
+ $blocked = $this->isBlocked( $bFromSlave );
+ }
+ wfProfileOut( $fname );
+ return $blocked;
+ }
+
+ /**
+ * Get name of blocker
+ * @return string name of blocker
+ */
+ function blockedBy() {
+ $this->getBlockedStatus();
+ return $this->mBlockedby;
+ }
+
+ /**
+ * Get blocking reason
+ * @return string Blocking reason
+ */
+ function blockedFor() {
+ $this->getBlockedStatus();
+ return $this->mBlockreason;
+ }
+
+ /**
+ * Initialise php session
+ */
+ function SetupSession() {
+ global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
+ if( $wgSessionsInMemcached ) {
+ require_once( 'MemcachedSessions.php' );
+ } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
+ # If it's left on 'user' or another setting from another
+ # application, it will end up failing. Try to recover.
+ ini_set ( 'session.save_handler', 'files' );
+ }
+ session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
+ session_cache_limiter( 'private, must-revalidate' );
+ @session_start();
+ }
+
+ /**
+ * Create a new user object using data from session
+ * @static
+ */
+ function loadFromSession() {
+ global $wgMemc, $wgDBname, $wgCookiePrefix;
+
+ if ( isset( $_SESSION['wsUserID'] ) ) {
+ if ( 0 != $_SESSION['wsUserID'] ) {
+ $sId = $_SESSION['wsUserID'];
+ } else {
+ return new User();
+ }
+ } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
+ $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
+ $_SESSION['wsUserID'] = $sId;
+ } else {
+ return new User();
+ }
+ if ( isset( $_SESSION['wsUserName'] ) ) {
+ $sName = $_SESSION['wsUserName'];
+ } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
+ $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
+ $_SESSION['wsUserName'] = $sName;
+ } else {
+ return new User();
+ }
+
+ $passwordCorrect = FALSE;
+ $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
+ if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
+ # Expire old serialized objects; they may be corrupt.
+ $user = false;
+ }
+ if($makenew = !$user) {
+ wfDebug( "User::loadFromSession() unable to load from memcached\n" );
+ $user = new User();
+ $user->mId = $sId;
+ $user->loadFromDatabase();
+ } else {
+ wfDebug( "User::loadFromSession() got from cache!\n" );
+ }
+
+ if ( isset( $_SESSION['wsToken'] ) ) {
+ $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
+ } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
+ $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
+ } else {
+ return new User(); # Can't log in from session
+ }
+
+ if ( ( $sName == $user->mName ) && $passwordCorrect ) {
+ if($makenew) {
+ if($wgMemc->set( $key, $user ))
+ wfDebug( "User::loadFromSession() successfully saved user\n" );
+ else
+ wfDebug( "User::loadFromSession() unable to save to memcached\n" );
+ }
+ return $user;
+ }
+ return new User(); # Can't log in from session
+ }
+
+ /**
+ * Load a user from the database
+ */
+ function loadFromDatabase() {
+ $fname = "User::loadFromDatabase";
+
+ # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
+ # loading in a command line script, don't assume all command line scripts need it like this
+ #if ( $this->mDataLoaded || $wgCommandLineMode ) {
+ if ( $this->mDataLoaded ) {
+ return;
+ }
+
+ # Paranoia
+ $this->mId = intval( $this->mId );
+
+ /** Anonymous user */
+ if( !$this->mId ) {
+ /** Get rights */
+ $this->mRights = $this->getGroupPermissions( array( '*' ) );
+ $this->mDataLoaded = true;
+ return;
+ } # the following stuff is for non-anonymous users only
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
+ 'user_email_authenticated',
+ 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
+ array( 'user_id' => $this->mId ), $fname );
+
+ if ( $s !== false ) {
+ $this->mName = $s->user_name;
+ $this->mEmail = $s->user_email;
+ $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
+ $this->mRealName = $s->user_real_name;
+ $this->mPassword = $s->user_password;
+ $this->mNewpassword = $s->user_newpassword;
+ $this->decodeOptions( $s->user_options );
+ $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
+ $this->mToken = $s->user_token;
+ $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
+
+ $res = $dbr->select( 'user_groups',
+ array( 'ug_group' ),
+ array( 'ug_user' => $this->mId ),
+ $fname );
+ $this->mGroups = array();
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $this->mGroups[] = $row->ug_group;
+ }
+ $implicitGroups = array( '*', 'user' );
+
+ global $wgAutoConfirmAge;
+ $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
+ if( $accountAge >= $wgAutoConfirmAge ) {
+ $implicitGroups[] = 'autoconfirmed';
+ }
+
+ # Implicit group for users whose email addresses are confirmed
+ global $wgEmailAuthentication;
+ if( $this->isValidEmailAddr( $this->mEmail ) ) {
+ if( $wgEmailAuthentication ) {
+ if( $this->mEmailAuthenticated )
+ $implicitGroups[] = 'emailconfirmed';
+ } else {
+ $implicitGroups[] = 'emailconfirmed';
+ }
+ }
+
+ $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
+ $this->mRights = $this->getGroupPermissions( $effectiveGroups );
+ }
+
+ $this->mDataLoaded = true;
+ }
+
+ function getID() { return $this->mId; }
+ function setID( $v ) {
+ $this->mId = $v;
+ $this->mDataLoaded = false;
+ }
+
+ function getName() {
+ $this->loadFromDatabase();
+ if ( $this->mName === false ) {
+ $this->mName = wfGetIP();
+ }
+ return $this->mName;
+ }
+
+ function setName( $str ) {
+ $this->loadFromDatabase();
+ $this->mName = $str;
+ }
+
+
+ /**
+ * Return the title dbkey form of the name, for eg user pages.
+ * @return string
+ * @public
+ */
+ function getTitleKey() {
+ return str_replace( ' ', '_', $this->getName() );
+ }
+
+ function getNewtalk() {
+ $this->loadFromDatabase();
+
+ # Load the newtalk status if it is unloaded (mNewtalk=-1)
+ if( $this->mNewtalk === -1 ) {
+ $this->mNewtalk = false; # reset talk page status
+
+ # Check memcached separately for anons, who have no
+ # entire User object stored in there.
+ if( !$this->mId ) {
+ global $wgDBname, $wgMemc;
+ $key = "$wgDBname:newtalk:ip:" . $this->getName();
+ $newtalk = $wgMemc->get( $key );
+ if( is_integer( $newtalk ) ) {
+ $this->mNewtalk = (bool)$newtalk;
+ } else {
+ $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
+ $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
+ }
+ } else {
+ $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
+ }
+ }
+
+ return (bool)$this->mNewtalk;
+ }
+
+ /**
+ * Return the talk page(s) this user has new messages on.
+ */
+ function getNewMessageLinks() {
+ global $wgDBname;
+ $talks = array();
+ if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
+ return $talks;
+
+ if (!$this->getNewtalk())
+ return array();
+ $up = $this->getUserPage();
+ $utp = $up->getTalkPage();
+ return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
+ }
+
+
+ /**
+ * Perform a user_newtalk check on current slaves; if the memcached data
+ * is funky we don't want newtalk state to get stuck on save, as that's
+ * damn annoying.
+ *
+ * @param string $field
+ * @param mixed $id
+ * @return bool
+ * @private
+ */
+ function checkNewtalk( $field, $id ) {
+ $fname = 'User::checkNewtalk';
+ $dbr =& wfGetDB( DB_SLAVE );
+ $ok = $dbr->selectField( 'user_newtalk', $field,
+ array( $field => $id ), $fname );
+ return $ok !== false;
+ }
+
+ /**
+ * Add or update the
+ * @param string $field
+ * @param mixed $id
+ * @private
+ */
+ function updateNewtalk( $field, $id ) {
+ $fname = 'User::updateNewtalk';
+ if( $this->checkNewtalk( $field, $id ) ) {
+ wfDebug( "$fname already set ($field, $id), ignoring\n" );
+ return false;
+ }
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->insert( 'user_newtalk',
+ array( $field => $id ),
+ $fname,
+ 'IGNORE' );
+ wfDebug( "$fname: set on ($field, $id)\n" );
+ return true;
+ }
+
+ /**
+ * Clear the new messages flag for the given user
+ * @param string $field
+ * @param mixed $id
+ * @private
+ */
+ function deleteNewtalk( $field, $id ) {
+ $fname = 'User::deleteNewtalk';
+ if( !$this->checkNewtalk( $field, $id ) ) {
+ wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
+ return false;
+ }
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->delete( 'user_newtalk',
+ array( $field => $id ),
+ $fname );
+ wfDebug( "$fname: killed on ($field, $id)\n" );
+ return true;
+ }
+
+ /**
+ * Update the 'You have new messages!' status.
+ * @param bool $val
+ */
+ function setNewtalk( $val ) {
+ if( wfReadOnly() ) {
+ return;
+ }
+
+ $this->loadFromDatabase();
+ $this->mNewtalk = $val;
+
+ $fname = 'User::setNewtalk';
+
+ if( $this->isAnon() ) {
+ $field = 'user_ip';
+ $id = $this->getName();
+ } else {
+ $field = 'user_id';
+ $id = $this->getId();
+ }
+
+ if( $val ) {
+ $changed = $this->updateNewtalk( $field, $id );
+ } else {
+ $changed = $this->deleteNewtalk( $field, $id );
+ }
+
+ if( $changed ) {
+ if( $this->isAnon() ) {
+ // Anons have a separate memcached space, since
+ // user records aren't kept for them.
+ global $wgDBname, $wgMemc;
+ $key = "$wgDBname:newtalk:ip:$val";
+ $wgMemc->set( $key, $val ? 1 : 0 );
+ } else {
+ if( $val ) {
+ // Make sure the user page is watched, so a notification
+ // will be sent out if enabled.
+ $this->addWatch( $this->getTalkPage() );
+ }
+ }
+ $this->invalidateCache();
+ $this->saveSettings();
+ }
+ }
+
+ function invalidateCache() {
+ global $wgClockSkewFudge;
+ $this->loadFromDatabase();
+ $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
+ # Don't forget to save the options after this or
+ # it won't take effect!
+ }
+
+ function validateCache( $timestamp ) {
+ $this->loadFromDatabase();
+ return ($timestamp >= $this->mTouched);
+ }
+
+ /**
+ * Encrypt a password.
+ * It can eventuall salt a password @see User::addSalt()
+ * @param string $p clear Password.
+ * @return string Encrypted password.
+ */
+ function encryptPassword( $p ) {
+ return wfEncryptPassword( $this->mId, $p );
+ }
+
+ # Set the password and reset the random token
+ function setPassword( $str ) {
+ $this->loadFromDatabase();
+ $this->setToken();
+ $this->mPassword = $this->encryptPassword( $str );
+ $this->mNewpassword = '';
+ }
+
+ # Set the random token (used for persistent authentication)
+ function setToken( $token = false ) {
+ global $wgSecretKey, $wgProxyKey, $wgDBname;
+ if ( !$token ) {
+ if ( $wgSecretKey ) {
+ $key = $wgSecretKey;
+ } elseif ( $wgProxyKey ) {
+ $key = $wgProxyKey;
+ } else {
+ $key = microtime();
+ }
+ $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
+ } else {
+ $this->mToken = $token;
+ }
+ }
+
+
+ function setCookiePassword( $str ) {
+ $this->loadFromDatabase();
+ $this->mCookiePassword = md5( $str );
+ }
+
+ function setNewpassword( $str ) {
+ $this->loadFromDatabase();
+ $this->mNewpassword = $this->encryptPassword( $str );
+ }
+
+ function getEmail() {
+ $this->loadFromDatabase();
+ return $this->mEmail;
+ }
+
+ function getEmailAuthenticationTimestamp() {
+ $this->loadFromDatabase();
+ return $this->mEmailAuthenticated;
+ }
+
+ function setEmail( $str ) {
+ $this->loadFromDatabase();
+ $this->mEmail = $str;
+ }
+
+ function getRealName() {
+ $this->loadFromDatabase();
+ return $this->mRealName;
+ }
+
+ function setRealName( $str ) {
+ $this->loadFromDatabase();
+ $this->mRealName = $str;
+ }
+
+ /**
+ * @param string $oname The option to check
+ * @return string
+ */
+ function getOption( $oname ) {
+ $this->loadFromDatabase();
+ if ( array_key_exists( $oname, $this->mOptions ) ) {
+ return trim( $this->mOptions[$oname] );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * @param string $oname The option to check
+ * @return bool False if the option is not selected, true if it is
+ */
+ function getBoolOption( $oname ) {
+ return (bool)$this->getOption( $oname );
+ }
+
+ /**
+ * Get an option as an integer value from the source string.
+ * @param string $oname The option to check
+ * @param int $default Optional value to return if option is unset/blank.
+ * @return int
+ */
+ function getIntOption( $oname, $default=0 ) {
+ $val = $this->getOption( $oname );
+ if( $val == '' ) {
+ $val = $default;
+ }
+ return intval( $val );
+ }
+
+ function setOption( $oname, $val ) {
+ $this->loadFromDatabase();
+ if ( $oname == 'skin' ) {
+ # Clear cached skin, so the new one displays immediately in Special:Preferences
+ unset( $this->mSkin );
+ }
+ // Filter out any newlines that may have passed through input validation.
+ // Newlines are used to separate items in the options blob.
+ $val = str_replace( "\r\n", "\n", $val );
+ $val = str_replace( "\r", "\n", $val );
+ $val = str_replace( "\n", " ", $val );
+ $this->mOptions[$oname] = $val;
+ $this->invalidateCache();
+ }
+
+ function getRights() {
+ $this->loadFromDatabase();
+ return $this->mRights;
+ }
+
+ /**
+ * Get the list of explicit group memberships this user has.
+ * The implicit * and user groups are not included.
+ * @return array of strings
+ */
+ function getGroups() {
+ $this->loadFromDatabase();
+ return $this->mGroups;
+ }
+
+ /**
+ * Get the list of implicit group memberships this user has.
+ * This includes all explicit groups, plus 'user' if logged in
+ * and '*' for all accounts.
+ * @return array of strings
+ */
+ function getEffectiveGroups() {
+ $base = array( '*' );
+ if( $this->isLoggedIn() ) {
+ $base[] = 'user';
+ }
+ return array_merge( $base, $this->getGroups() );
+ }
+
+ /**
+ * Add the user to the given group.
+ * This takes immediate effect.
+ * @string $group
+ */
+ function addGroup( $group ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->insert( 'user_groups',
+ array(
+ 'ug_user' => $this->getID(),
+ 'ug_group' => $group,
+ ),
+ 'User::addGroup',
+ array( 'IGNORE' ) );
+
+ $this->mGroups = array_merge( $this->mGroups, array( $group ) );
+ $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
+
+ $this->invalidateCache();
+ $this->saveSettings();
+ }
+
+ /**
+ * Remove the user from the given group.
+ * This takes immediate effect.
+ * @string $group
+ */
+ function removeGroup( $group ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->delete( 'user_groups',
+ array(
+ 'ug_user' => $this->getID(),
+ 'ug_group' => $group,
+ ),
+ 'User::removeGroup' );
+
+ $this->mGroups = array_diff( $this->mGroups, array( $group ) );
+ $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
+
+ $this->invalidateCache();
+ $this->saveSettings();
+ }
+
+
+ /**
+ * A more legible check for non-anonymousness.
+ * Returns true if the user is not an anonymous visitor.
+ *
+ * @return bool
+ */
+ function isLoggedIn() {
+ return( $this->getID() != 0 );
+ }
+
+ /**
+ * A more legible check for anonymousness.
+ * Returns true if the user is an anonymous visitor.
+ *
+ * @return bool
+ */
+ function isAnon() {
+ return !$this->isLoggedIn();
+ }
+
+ /**
+ * Deprecated in 1.6, die in 1.7, to be removed in 1.8
+ * @deprecated
+ */
+ function isSysop() {
+ throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" );
+ #return $this->isAllowed( 'protect' );
+ }
+
+ /**
+ * Deprecated in 1.6, die in 1.7, to be removed in 1.8
+ * @deprecated
+ */
+ function isDeveloper() {
+ throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
+ #return $this->isAllowed( 'siteadmin' );
+ }
+
+ /**
+ * Deprecated in 1.6, die in 1.7, to be removed in 1.8
+ * @deprecated
+ */
+ function isBureaucrat() {
+ throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
+ #return $this->isAllowed( 'makesysop' );
+ }
+
+ /**
+ * Whether the user is a bot
+ * @todo need to be migrated to the new user level management sytem
+ */
+ function isBot() {
+ $this->loadFromDatabase();
+ return in_array( 'bot', $this->mRights );
+ }
+
+ /**
+ * Check if user is allowed to access a feature / make an action
+ * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
+ * @return boolean True: action is allowed, False: action should not be allowed
+ */
+ function isAllowed($action='') {
+ if ( $action === '' )
+ // In the spirit of DWIM
+ return true;
+
+ $this->loadFromDatabase();
+ return in_array( $action , $this->mRights );
+ }
+
+ /**
+ * Load a skin if it doesn't exist or return it
+ * @todo FIXME : need to check the old failback system [AV]
+ */
+ function &getSkin() {
+ global $IP, $wgRequest;
+ if ( ! isset( $this->mSkin ) ) {
+ $fname = 'User::getSkin';
+ wfProfileIn( $fname );
+
+ # get the user skin
+ $userSkin = $this->getOption( 'skin' );
+ $userSkin = $wgRequest->getVal('useskin', $userSkin);
+
+ $this->mSkin =& Skin::newFromKey( $userSkin );
+ wfProfileOut( $fname );
+ }
+ return $this->mSkin;
+ }
+
+ /**#@+
+ * @param string $title Article title to look at
+ */
+
+ /**
+ * Check watched status of an article
+ * @return bool True if article is watched
+ */
+ function isWatched( $title ) {
+ $wl = WatchedItem::fromUserTitle( $this, $title );
+ return $wl->isWatched();
+ }
+
+ /**
+ * Watch an article
+ */
+ function addWatch( $title ) {
+ $wl = WatchedItem::fromUserTitle( $this, $title );
+ $wl->addWatch();
+ $this->invalidateCache();
+ }
+
+ /**
+ * Stop watching an article
+ */
+ function removeWatch( $title ) {
+ $wl = WatchedItem::fromUserTitle( $this, $title );
+ $wl->removeWatch();
+ $this->invalidateCache();
+ }
+
+ /**
+ * Clear the user's notification timestamp for the given title.
+ * If e-notif e-mails are on, they will receive notification mails on
+ * the next change of the page if it's watched etc.
+ */
+ function clearNotification( &$title ) {
+ global $wgUser, $wgUseEnotif;
+
+
+ if ($title->getNamespace() == NS_USER_TALK &&
+ $title->getText() == $this->getName() ) {
+ if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
+ return;
+ $this->setNewtalk( false );
+ }
+
+ if( !$wgUseEnotif ) {
+ return;
+ }
+
+ if( $this->isAnon() ) {
+ // Nothing else to do...
+ return;
+ }
+
+ // Only update the timestamp if the page is being watched.
+ // The query to find out if it is watched is cached both in memcached and per-invocation,
+ // and when it does have to be executed, it can be on a slave
+ // If this is the user's newtalk page, we always update the timestamp
+ if ($title->getNamespace() == NS_USER_TALK &&
+ $title->getText() == $wgUser->getName())
+ {
+ $watched = true;
+ } elseif ( $this->getID() == $wgUser->getID() ) {
+ $watched = $title->userIsWatching();
+ } else {
+ $watched = true;
+ }
+
+ // If the page is watched by the user (or may be watched), update the timestamp on any
+ // any matching rows
+ if ( $watched ) {
+ $dbw =& wfGetDB( DB_MASTER );
+ $success = $dbw->update( 'watchlist',
+ array( /* SET */
+ 'wl_notificationtimestamp' => NULL
+ ), array( /* WHERE */
+ 'wl_title' => $title->getDBkey(),
+ 'wl_namespace' => $title->getNamespace(),
+ 'wl_user' => $this->getID()
+ ), 'User::clearLastVisited'
+ );
+ }
+ }
+
+ /**#@-*/
+
+ /**
+ * Resets all of the given user's page-change notification timestamps.
+ * If e-notif e-mails are on, they will receive notification mails on
+ * the next change of any watched page.
+ *
+ * @param int $currentUser user ID number
+ * @public
+ */
+ function clearAllNotifications( $currentUser ) {
+ global $wgUseEnotif;
+ if ( !$wgUseEnotif ) {
+ $this->setNewtalk( false );
+ return;
+ }
+ if( $currentUser != 0 ) {
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $success = $dbw->update( 'watchlist',
+ array( /* SET */
+ 'wl_notificationtimestamp' => 0
+ ), array( /* WHERE */
+ 'wl_user' => $currentUser
+ ), 'UserMailer::clearAll'
+ );
+
+ # we also need to clear here the "you have new message" notification for the own user_talk page
+ # This is cleared one page view later in Article::viewUpdates();
+ }
+ }
+
+ /**
+ * @private
+ * @return string Encoding options
+ */
+ function encodeOptions() {
+ $a = array();
+ foreach ( $this->mOptions as $oname => $oval ) {
+ array_push( $a, $oname.'='.$oval );
+ }
+ $s = implode( "\n", $a );
+ return $s;
+ }
+
+ /**
+ * @private
+ */
+ function decodeOptions( $str ) {
+ $a = explode( "\n", $str );
+ foreach ( $a as $s ) {
+ if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
+ $this->mOptions[$m[1]] = $m[2];
+ }
+ }
+ }
+
+ function setCookies() {
+ global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
+ if ( 0 == $this->mId ) return;
+ $this->loadFromDatabase();
+ $exp = time() + $wgCookieExpiration;
+
+ $_SESSION['wsUserID'] = $this->mId;
+ setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+
+ $_SESSION['wsUserName'] = $this->getName();
+ setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+
+ $_SESSION['wsToken'] = $this->mToken;
+ if ( 1 == $this->getOption( 'rememberpassword' ) ) {
+ setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ } else {
+ setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
+ }
+ }
+
+ /**
+ * Logout user
+ * It will clean the session cookie
+ */
+ function logout() {
+ global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
+ $this->loadDefaults();
+ $this->setLoaded( true );
+
+ $_SESSION['wsUserID'] = 0;
+
+ setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+
+ # Remember when user logged out, to prevent seeing cached pages
+ setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ }
+
+ /**
+ * Save object settings into database
+ */
+ function saveSettings() {
+ global $wgMemc, $wgDBname;
+ $fname = 'User::saveSettings';
+
+ if ( wfReadOnly() ) { return; }
+ if ( 0 == $this->mId ) { return; }
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->update( 'user',
+ array( /* SET */
+ 'user_name' => $this->mName,
+ 'user_password' => $this->mPassword,
+ 'user_newpassword' => $this->mNewpassword,
+ 'user_real_name' => $this->mRealName,
+ 'user_email' => $this->mEmail,
+ 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+ 'user_options' => $this->encodeOptions(),
+ 'user_touched' => $dbw->timestamp($this->mTouched),
+ 'user_token' => $this->mToken
+ ), array( /* WHERE */
+ 'user_id' => $this->mId
+ ), $fname
+ );
+ $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
+ }
+
+
+ /**
+ * Checks if a user with the given name exists, returns the ID
+ */
+ function idForName() {
+ $fname = 'User::idForName';
+
+ $gotid = 0;
+ $s = trim( $this->getName() );
+ if ( 0 == strcmp( '', $s ) ) return 0;
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
+ if ( $id === false ) {
+ $id = 0;
+ }
+ return $id;
+ }
+
+ /**
+ * Add user object to the database
+ */
+ function addToDatabase() {
+ $fname = 'User::addToDatabase';
+ $dbw =& wfGetDB( DB_MASTER );
+ $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
+ $dbw->insert( 'user',
+ array(
+ 'user_id' => $seqVal,
+ 'user_name' => $this->mName,
+ 'user_password' => $this->mPassword,
+ 'user_newpassword' => $this->mNewpassword,
+ 'user_email' => $this->mEmail,
+ 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+ 'user_real_name' => $this->mRealName,
+ 'user_options' => $this->encodeOptions(),
+ 'user_token' => $this->mToken,
+ 'user_registration' => $dbw->timestamp( $this->mRegistration ),
+ ), $fname
+ );
+ $this->mId = $dbw->insertId();
+ }
+
+ function spreadBlock() {
+ # If the (non-anonymous) user is blocked, this function will block any IP address
+ # that they successfully log on from.
+ $fname = 'User::spreadBlock';
+
+ wfDebug( "User:spreadBlock()\n" );
+ if ( $this->mId == 0 ) {
+ return;
+ }
+
+ $userblock = Block::newFromDB( '', $this->mId );
+ if ( !$userblock->isValid() ) {
+ return;
+ }
+
+ # Check if this IP address is already blocked
+ $ipblock = Block::newFromDB( wfGetIP() );
+ if ( $ipblock->isValid() ) {
+ # If the user is already blocked. Then check if the autoblock would
+ # excede the user block. If it would excede, then do nothing, else
+ # prolong block time
+ if ($userblock->mExpiry &&
+ ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
+ return;
+ }
+ # Just update the timestamp
+ $ipblock->updateTimestamp();
+ return;
+ }
+
+ # Make a new block object with the desired properties
+ wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
+ $ipblock->mAddress = wfGetIP();
+ $ipblock->mUser = 0;
+ $ipblock->mBy = $userblock->mBy;
+ $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
+ $ipblock->mTimestamp = wfTimestampNow();
+ $ipblock->mAuto = 1;
+ # If the user is already blocked with an expiry date, we don't
+ # want to pile on top of that!
+ if($userblock->mExpiry) {
+ $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
+ } else {
+ $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
+ }
+
+ # Insert it
+ $ipblock->insert();
+
+ }
+
+ /**
+ * Generate a string which will be different for any combination of
+ * user options which would produce different parser output.
+ * This will be used as part of the hash key for the parser cache,
+ * so users will the same options can share the same cached data
+ * safely.
+ *
+ * Extensions which require it should install 'PageRenderingHash' hook,
+ * which will give them a chance to modify this key based on their own
+ * settings.
+ *
+ * @return string
+ */
+ function getPageRenderingHash() {
+ global $wgContLang;
+ if( $this->mHash ){
+ return $this->mHash;
+ }
+
+ // stubthreshold is only included below for completeness,
+ // it will always be 0 when this function is called by parsercache.
+
+ $confstr = $this->getOption( 'math' );
+ $confstr .= '!' . $this->getOption( 'stubthreshold' );
+ $confstr .= '!' . $this->getOption( 'date' );
+ $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
+ $confstr .= '!' . $this->getOption( 'language' );
+ $confstr .= '!' . $this->getOption( 'thumbsize' );
+ // add in language specific options, if any
+ $extra = $wgContLang->getExtraHashOptions();
+ $confstr .= $extra;
+
+ // Give a chance for extensions to modify the hash, if they have
+ // extra options or other effects on the parser cache.
+ wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
+
+ $this->mHash = $confstr;
+ return $confstr;
+ }
+
+ function isAllowedToCreateAccount() {
+ return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
+ }
+
+ /**
+ * Set mDataLoaded, return previous value
+ * Use this to prevent DB access in command-line scripts or similar situations
+ */
+ function setLoaded( $loaded ) {
+ return wfSetVar( $this->mDataLoaded, $loaded );
+ }
+
+ /**
+ * Get this user's personal page title.
+ *
+ * @return Title
+ * @public
+ */
+ function getUserPage() {
+ return Title::makeTitle( NS_USER, $this->getName() );
+ }
+
+ /**
+ * Get this user's talk page title.
+ *
+ * @return Title
+ * @public
+ */
+ function getTalkPage() {
+ $title = $this->getUserPage();
+ return $title->getTalkPage();
+ }
+
+ /**
+ * @static
+ */
+ function getMaxID() {
+ static $res; // cache
+
+ if ( isset( $res ) )
+ return $res;
+ else {
+ $dbr =& wfGetDB( DB_SLAVE );
+ return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
+ }
+ }
+
+ /**
+ * Determine whether the user is a newbie. Newbies are either
+ * anonymous IPs, or the most recently created accounts.
+ * @return bool True if it is a newbie.
+ */
+ function isNewbie() {
+ return !$this->isAllowed( 'autoconfirmed' );
+ }
+
+ /**
+ * Check to see if the given clear-text password is one of the accepted passwords
+ * @param string $password User password.
+ * @return bool True if the given password is correct otherwise False.
+ */
+ function checkPassword( $password ) {
+ global $wgAuth, $wgMinimalPasswordLength;
+ $this->loadFromDatabase();
+
+ // Even though we stop people from creating passwords that
+ // are shorter than this, doesn't mean people wont be able
+ // to. Certain authentication plugins do NOT want to save
+ // domain passwords in a mysql database, so we should
+ // check this (incase $wgAuth->strict() is false).
+ if( strlen( $password ) < $wgMinimalPasswordLength ) {
+ return false;
+ }
+
+ if( $wgAuth->authenticate( $this->getName(), $password ) ) {
+ return true;
+ } elseif( $wgAuth->strict() ) {
+ /* Auth plugin doesn't allow local authentication */
+ return false;
+ }
+ $ep = $this->encryptPassword( $password );
+ if ( 0 == strcmp( $ep, $this->mPassword ) ) {
+ return true;
+ } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
+ return true;
+ } elseif ( function_exists( 'iconv' ) ) {
+ # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+ # Check for this with iconv
+ $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
+ if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Initialize (if necessary) and return a session token value
+ * which can be used in edit forms to show that the user's
+ * login credentials aren't being hijacked with a foreign form
+ * submission.
+ *
+ * @param mixed $salt - Optional function-specific data for hash.
+ * Use a string or an array of strings.
+ * @return string
+ * @public
+ */
+ function editToken( $salt = '' ) {
+ if( !isset( $_SESSION['wsEditToken'] ) ) {
+ $token = $this->generateToken();
+ $_SESSION['wsEditToken'] = $token;
+ } else {
+ $token = $_SESSION['wsEditToken'];
+ }
+ if( is_array( $salt ) ) {
+ $salt = implode( '|', $salt );
+ }
+ return md5( $token . $salt );
+ }
+
+ /**
+ * Generate a hex-y looking random token for various uses.
+ * Could be made more cryptographically sure if someone cares.
+ * @return string
+ */
+ function generateToken( $salt = '' ) {
+ $token = dechex( mt_rand() ) . dechex( mt_rand() );
+ return md5( $token . $salt );
+ }
+
+ /**
+ * Check given value against the token value stored in the session.
+ * A match should confirm that the form was submitted from the
+ * user's own login session, not a form submission from a third-party
+ * site.
+ *
+ * @param string $val - the input value to compare
+ * @param string $salt - Optional function-specific data for hash
+ * @return bool
+ * @public
+ */
+ function matchEditToken( $val, $salt = '' ) {
+ global $wgMemc;
+ $sessionToken = $this->editToken( $salt );
+ if ( $val != $sessionToken ) {
+ wfDebug( "User::matchEditToken: broken session data\n" );
+ }
+ return $val == $sessionToken;
+ }
+
+ /**
+ * Generate a new e-mail confirmation token and send a confirmation
+ * mail to the user's given address.
+ *
+ * @return mixed True on success, a WikiError object on failure.
+ */
+ function sendConfirmationMail() {
+ global $wgContLang;
+ $url = $this->confirmationTokenUrl( $expiration );
+ return $this->sendMail( wfMsg( 'confirmemail_subject' ),
+ wfMsg( 'confirmemail_body',
+ wfGetIP(),
+ $this->getName(),
+ $url,
+ $wgContLang->timeanddate( $expiration, false ) ) );
+ }
+
+ /**
+ * Send an e-mail to this user's account. Does not check for
+ * confirmed status or validity.
+ *
+ * @param string $subject
+ * @param string $body
+ * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
+ * @return mixed True on success, a WikiError object on failure.
+ */
+ function sendMail( $subject, $body, $from = null ) {
+ if( is_null( $from ) ) {
+ global $wgPasswordSender;
+ $from = $wgPasswordSender;
+ }
+
+ require_once( 'UserMailer.php' );
+ $to = new MailAddress( $this );
+ $sender = new MailAddress( $from );
+ $error = userMailer( $to, $sender, $subject, $body );
+
+ if( $error == '' ) {
+ return true;
+ } else {
+ return new WikiError( $error );
+ }
+ }
+
+ /**
+ * Generate, store, and return a new e-mail confirmation code.
+ * A hash (unsalted since it's used as a key) is stored.
+ * @param &$expiration mixed output: accepts the expiration time
+ * @return string
+ * @private
+ */
+ function confirmationToken( &$expiration ) {
+ $fname = 'User::confirmationToken';
+
+ $now = time();
+ $expires = $now + 7 * 24 * 60 * 60;
+ $expiration = wfTimestamp( TS_MW, $expires );
+
+ $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
+ $hash = md5( $token );
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->update( 'user',
+ array( 'user_email_token' => $hash,
+ 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
+ array( 'user_id' => $this->mId ),
+ $fname );
+
+ return $token;
+ }
+
+ /**
+ * Generate and store a new e-mail confirmation token, and return
+ * the URL the user can use to confirm.
+ * @param &$expiration mixed output: accepts the expiration time
+ * @return string
+ * @private
+ */
+ function confirmationTokenUrl( &$expiration ) {
+ $token = $this->confirmationToken( $expiration );
+ $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
+ return $title->getFullUrl();
+ }
+
+ /**
+ * Mark the e-mail address confirmed and save.
+ */
+ function confirmEmail() {
+ $this->loadFromDatabase();
+ $this->mEmailAuthenticated = wfTimestampNow();
+ $this->saveSettings();
+ return true;
+ }
+
+ /**
+ * Is this user allowed to send e-mails within limits of current
+ * site configuration?
+ * @return bool
+ */
+ function canSendEmail() {
+ return $this->isEmailConfirmed();
+ }
+
+ /**
+ * Is this user allowed to receive e-mails within limits of current
+ * site configuration?
+ * @return bool
+ */
+ function canReceiveEmail() {
+ return $this->canSendEmail() && !$this->getOption( 'disablemail' );
+ }
+
+ /**
+ * Is this user's e-mail address valid-looking and confirmed within
+ * limits of the current site configuration?
+ *
+ * If $wgEmailAuthentication is on, this may require the user to have
+ * confirmed their address by returning a code or using a password
+ * sent to the address from the wiki.
+ *
+ * @return bool
+ */
+ function isEmailConfirmed() {
+ global $wgEmailAuthentication;
+ $this->loadFromDatabase();
+ $confirmed = true;
+ if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
+ if( $this->isAnon() )
+ return false;
+ if( !$this->isValidEmailAddr( $this->mEmail ) )
+ return false;
+ if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
+ return false;
+ return true;
+ } else {
+ return $confirmed;
+ }
+ }
+
+ /**
+ * @param array $groups list of groups
+ * @return array list of permission key names for given groups combined
+ * @static
+ */
+ function getGroupPermissions( $groups ) {
+ global $wgGroupPermissions;
+ $rights = array();
+ foreach( $groups as $group ) {
+ if( isset( $wgGroupPermissions[$group] ) ) {
+ $rights = array_merge( $rights,
+ array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
+ }
+ }
+ return $rights;
+ }
+
+ /**
+ * @param string $group key name
+ * @return string localized descriptive name for group, if provided
+ * @static
+ */
+ function getGroupName( $group ) {
+ $key = "group-$group";
+ $name = wfMsg( $key );
+ if( $name == '' || $name == "&lt;$key&gt;" ) {
+ return $group;
+ } else {
+ return $name;
+ }
+ }
+
+ /**
+ * @param string $group key name
+ * @return string localized descriptive name for member of a group, if provided
+ * @static
+ */
+ function getGroupMember( $group ) {
+ $key = "group-$group-member";
+ $name = wfMsg( $key );
+ if( $name == '' || $name == "&lt;$key&gt;" ) {
+ return $group;
+ } else {
+ return $name;
+ }
+ }
+
+
+ /**
+ * Return the set of defined explicit groups.
+ * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
+ * groups are not included, as they are defined
+ * automatically, not in the database.
+ * @return array
+ * @static
+ */
+ function getAllGroups() {
+ global $wgGroupPermissions;
+ return array_diff(
+ array_keys( $wgGroupPermissions ),
+ array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
+ }
+
+ /**
+ * Get the title of a page describing a particular group
+ *
+ * @param $group Name of the group
+ * @return mixed
+ */
+ function getGroupPage( $group ) {
+ $page = wfMsgForContent( 'grouppage-' . $group );
+ if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
+ $title = Title::newFromText( $page );
+ if( is_object( $title ) )
+ return $title;
+ }
+ return false;
+ }
+
+
+}
+
+?>
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
new file mode 100644
index 00000000..8de39a64
--- /dev/null
+++ b/includes/UserMailer.php
@@ -0,0 +1,414 @@
+<?php
+/**
+ * UserMailer.php
+ * Copyright (C) 2004 Thomas Gries <mail@tgries.de>
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (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
+ *
+ * @author <brion@pobox.com>
+ * @author <mail@tgries.de>
+ *
+ * @package MediaWiki
+ */
+
+/**
+ * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name
+ */
+function wfRFC822Phrase( $phrase ) {
+ $phrase = strtr( $phrase, array( "\r" => '', "\n" => '', '"' => '' ) );
+ return '"' . $phrase . '"';
+}
+
+class MailAddress {
+ /**
+ * @param mixed $address String with an email address, or a User object
+ * @param string $name Human-readable name if a string address is given
+ */
+ function MailAddress( $address, $name=null ) {
+ if( is_object( $address ) && is_a( $address, 'User' ) ) {
+ $this->address = $address->getEmail();
+ $this->name = $address->getName();
+ } else {
+ $this->address = strval( $address );
+ $this->name = strval( $name );
+ }
+ }
+
+ /**
+ * Return formatted and quoted address to insert into SMTP headers
+ * @return string
+ */
+ function toString() {
+ if( $this->name != '' ) {
+ $quoted = wfQuotedPrintable( $this->name );
+ if( strpos( $quoted, '.' ) !== false ) {
+ $quoted = '"' . $quoted . '"';
+ }
+ return "$quoted <{$this->address}>";
+ } else {
+ return $this->address;
+ }
+ }
+}
+
+/**
+ * This function will perform a direct (authenticated) login to
+ * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
+ * array of parameters. It requires PEAR:Mail to do that.
+ * Otherwise it just uses the standard PHP 'mail' function.
+ *
+ * @param $to MailAddress: recipient's email
+ * @param $from MailAddress: sender's email
+ * @param $subject String: email's subject.
+ * @param $body String: email's text.
+ * @param $replyto String: optional reply-to email (default: false).
+ */
+function userMailer( $to, $from, $subject, $body, $replyto=false ) {
+ global $wgUser, $wgSMTP, $wgOutputEncoding, $wgErrorString;
+
+ if (is_array( $wgSMTP )) {
+ require_once( 'Mail.php' );
+
+ $timestamp = time();
+ $dest = $to->address;
+
+ $headers['From'] = $from->toString();
+ $headers['To'] = $to->toString();
+ if ( $replyto ) {
+ $headers['Reply-To'] = $replyto;
+ }
+ $headers['Subject'] = wfQuotedPrintable( $subject );
+ $headers['Date'] = date( 'r' );
+ $headers['MIME-Version'] = '1.0';
+ $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding;
+ $headers['Content-transfer-encoding'] = '8bit';
+ $headers['Message-ID'] = "<{$timestamp}" . $wgUser->getName() . '@' . $wgSMTP['IDHost'] . '>'; // FIXME
+ $headers['X-Mailer'] = 'MediaWiki mailer';
+
+ // Create the mail object using the Mail::factory method
+ $mail_object =& Mail::factory('smtp', $wgSMTP);
+ wfDebug( "Sending mail via PEAR::Mail to $dest\n" );
+ $mailResult =& $mail_object->send($dest, $headers, $body);
+
+ # Based on the result return an error string,
+ if ($mailResult === true) {
+ return '';
+ } elseif (is_object($mailResult)) {
+ wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
+ return $mailResult->getMessage();
+ } else {
+ wfDebug( "PEAR::Mail failed, unknown error result\n" );
+ return 'Mail object return unknown error.';
+ }
+ } else {
+ # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently
+ # (fifth parameter of the PHP mail function, see some lines below)
+ $headers =
+ "MIME-Version: 1.0\n" .
+ "Content-type: text/plain; charset={$wgOutputEncoding}\n" .
+ "Content-Transfer-Encoding: 8bit\n" .
+ "X-Mailer: MediaWiki mailer\n".
+ 'From: ' . $from->toString() . "\n";
+ if ($replyto) {
+ $headers .= "Reply-To: $replyto\n";
+ }
+
+ $dest = $to->toString();
+
+ $wgErrorString = '';
+ set_error_handler( 'mailErrorHandler' );
+ wfDebug( "Sending mail via internal mail() function to $dest\n" );
+ mail( $dest, wfQuotedPrintable( $subject ), $body, $headers );
+ restore_error_handler();
+
+ if ( $wgErrorString ) {
+ wfDebug( "Error sending mail: $wgErrorString\n" );
+ }
+ return $wgErrorString;
+ }
+}
+
+/**
+ * Get the mail error message in global $wgErrorString
+ *
+ * @param $code Integer: error number
+ * @param $string String: error message
+ */
+function mailErrorHandler( $code, $string ) {
+ global $wgErrorString;
+ $wgErrorString = preg_replace( "/^mail\(\): /", '', $string );
+}
+
+
+/**
+ * This module processes the email notifications when the current page is
+ * changed. It looks up the table watchlist to find out which users are watching
+ * that page.
+ *
+ * The current implementation sends independent emails to each watching user for
+ * the following reason:
+ *
+ * - Each watching user will be notified about the page edit time expressed in
+ * his/her local time (UTC is shown additionally). To achieve this, we need to
+ * find the individual timeoffset of each watching user from the preferences..
+ *
+ * Suggested improvement to slack down the number of sent emails: We could think
+ * of sending out bulk mails (bcc:user1,user2...) for all these users having the
+ * same timeoffset in their preferences.
+ *
+ * Visit the documentation pages under http://meta.wikipedia.com/Enotif
+ *
+ * @package MediaWiki
+ *
+ */
+class EmailNotification {
+ /**@{{
+ * @private
+ */
+ var $to, $subject, $body, $replyto, $from;
+ var $user, $title, $timestamp, $summary, $minorEdit, $oldid;
+
+ /**@}}*/
+
+ /**
+ * @todo document
+ * @param $title Title object
+ * @param $timestamp
+ * @param $summary
+ * @param $minorEdit
+ * @param $oldid (default: false)
+ */
+ function notifyOnPageChange(&$title, $timestamp, $summary, $minorEdit, $oldid=false) {
+
+ # we use $wgEmergencyContact as sender's address
+ global $wgUser, $wgEnotifWatchlist;
+ global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker;
+
+ $fname = 'UserMailer::notifyOnPageChange';
+ wfProfileIn( $fname );
+
+ # The following code is only run, if several conditions are met:
+ # 1. EmailNotification for pages (other than user_talk pages) must be enabled
+ # 2. minor edits (changes) are only regarded if the global flag indicates so
+
+ $isUserTalkPage = ($title->getNamespace() == NS_USER_TALK);
+ $enotifusertalkpage = ($isUserTalkPage && $wgEnotifUserTalk);
+ $enotifwatchlistpage = $wgEnotifWatchlist;
+
+ if ( (!$minorEdit || $wgEnotifMinorEdits) ) {
+ if( $wgEnotifWatchlist ) {
+ // Send updates to watchers other than the current editor
+ $userCondition = 'wl_user <> ' . intval( $wgUser->getId() );
+ } elseif( $wgEnotifUserTalk && $title->getNamespace() == NS_USER_TALK ) {
+ $targetUser = User::newFromName( $title->getText() );
+ if( is_null( $targetUser ) ) {
+ wfDebug( "$fname: user-talk-only mode; no such user\n" );
+ $userCondition = false;
+ } elseif( $targetUser->getId() == $wgUser->getId() ) {
+ wfDebug( "$fname: user-talk-only mode; editor is target user\n" );
+ $userCondition = false;
+ } else {
+ // Don't notify anyone other than the owner of the talk page
+ $userCondition = 'wl_user = ' . intval( $targetUser->getId() );
+ }
+ } else {
+ // Notifications disabled
+ $userCondition = false;
+ }
+ if( $userCondition ) {
+ $dbr =& wfGetDB( DB_MASTER );
+ extract( $dbr->tableNames( 'watchlist' ) );
+
+ $res = $dbr->select( 'watchlist', array( 'wl_user' ),
+ array(
+ 'wl_title' => $title->getDBkey(),
+ 'wl_namespace' => $title->getNamespace(),
+ $userCondition,
+ 'wl_notificationtimestamp IS NULL',
+ ), $fname );
+
+ # if anyone is watching ... set up the email message text which is
+ # common for all receipients ...
+ if ( $dbr->numRows( $res ) > 0 ) {
+ $this->title =& $title;
+ $this->timestamp = $timestamp;
+ $this->summary = $summary;
+ $this->minorEdit = $minorEdit;
+ $this->oldid = $oldid;
+
+ $this->composeCommonMailtext();
+ $watchingUser = new User();
+
+ # ... now do for all watching users ... if the options fit
+ for ($i = 1; $i <= $dbr->numRows( $res ); $i++) {
+
+ $wuser = $dbr->fetchObject( $res );
+ $watchingUser->setID($wuser->wl_user);
+ if ( ( $enotifwatchlistpage && $watchingUser->getOption('enotifwatchlistpages') ) ||
+ ( $enotifusertalkpage && $watchingUser->getOption('enotifusertalkpages') )
+ && (!$minorEdit || ($wgEnotifMinorEdits && $watchingUser->getOption('enotifminoredits') ) )
+ && ($watchingUser->isEmailConfirmed() ) ) {
+ # ... adjust remaining text and page edit time placeholders
+ # which needs to be personalized for each user
+ $this->composeAndSendPersonalisedMail( $watchingUser );
+
+ } # if the watching user has an email address in the preferences
+ }
+ }
+ } # if anyone is watching
+ } # if $wgEnotifWatchlist = true
+
+ if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) {
+ # mark the changed watch-listed page with a timestamp, so that the page is
+ # listed with an "updated since your last visit" icon in the watch list, ...
+ $dbw =& wfGetDB( DB_MASTER );
+ $success = $dbw->update( 'watchlist',
+ array( /* SET */
+ 'wl_notificationtimestamp' => $dbw->timestamp($timestamp)
+ ), array( /* WHERE */
+ 'wl_title' => $title->getDBkey(),
+ 'wl_namespace' => $title->getNamespace(),
+ ), 'UserMailer::NotifyOnChange'
+ );
+ # FIXME what do we do on failure ?
+ }
+
+ } # function NotifyOnChange
+
+ /**
+ * @private
+ */
+ function composeCommonMailtext() {
+ global $wgUser, $wgEmergencyContact, $wgNoReplyAddress;
+ global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
+
+ $summary = ($this->summary == '') ? ' - ' : $this->summary;
+ $medit = ($this->minorEdit) ? wfMsg( 'minoredit' ) : '';
+
+ # You as the WikiAdmin and Sysops can make use of plenty of
+ # named variables when composing your notification emails while
+ # simply editing the Meta pages
+
+ $subject = wfMsgForContent( 'enotif_subject' );
+ $body = wfMsgForContent( 'enotif_body' );
+ $from = ''; /* fail safe */
+ $replyto = ''; /* fail safe */
+ $keys = array();
+
+ # regarding the use of oldid as an indicator for the last visited version, see also
+ # http://bugzilla.wikipeda.org/show_bug.cgi?id=603 "Delete + undelete cycle doesn't preserve old_id"
+ # However, in the case of a new page which is already watched, we have no previous version to compare
+ if( $this->oldid ) {
+ $difflink = $this->title->getFullUrl( 'diff=0&oldid=' . $this->oldid );
+ $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastvisited', $difflink );
+ $keys['$OLDID'] = $this->oldid;
+ $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'changed' );
+ } else {
+ $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_newpagetext' );
+ # clear $OLDID placeholder in the message template
+ $keys['$OLDID'] = '';
+ $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'created' );
+ }
+
+ $body = strtr( $body, $keys );
+ $pagetitle = $this->title->getPrefixedText();
+ $keys['$PAGETITLE'] = $pagetitle;
+ $keys['$PAGETITLE_URL'] = $this->title->getFullUrl();
+
+ $keys['$PAGEMINOREDIT'] = $medit;
+ $keys['$PAGESUMMARY'] = $summary;
+
+ $subject = strtr( $subject, $keys );
+
+ # Reveal the page editor's address as REPLY-TO address only if
+ # the user has not opted-out and the option is enabled at the
+ # global configuration level.
+ $name = $wgUser->getName();
+ $adminAddress = new MailAddress( $wgEmergencyContact, 'WikiAdmin' );
+ $editorAddress = new MailAddress( $wgUser );
+ if( $wgEnotifRevealEditorAddress
+ && ( $wgUser->getEmail() != '' )
+ && $wgUser->getOption( 'enotifrevealaddr' ) ) {
+ if( $wgEnotifFromEditor ) {
+ $from = $editorAddress;
+ } else {
+ $from = $adminAddress;
+ $replyto = $editorAddress;
+ }
+ } else {
+ $from = $adminAddress;
+ $replyto = $wgNoReplyAddress;
+ }
+
+ if( $wgUser->isIP( $name ) ) {
+ #real anon (user:xxx.xxx.xxx.xxx)
+ $subject = str_replace('$PAGEEDITOR', 'anonymous user '. $name, $subject);
+ $keys['$PAGEEDITOR'] = 'anonymous user ' . $name;
+ $keys['$PAGEEDITOR_EMAIL'] = wfMsgForContent( 'noemailtitle' );
+ } else {
+ $subject = str_replace('$PAGEEDITOR', $name, $subject);
+ $keys['$PAGEEDITOR'] = $name;
+ $emailPage = Title::makeTitle( NS_SPECIAL, 'Emailuser/' . $name );
+ $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl();
+ }
+ $userPage = $wgUser->getUserPage();
+ $keys['$PAGEEDITOR_WIKI'] = $userPage->getFullUrl();
+ $body = strtr( $body, $keys );
+ $body = wordwrap( $body, 72 );
+
+ # now save this as the constant user-independent part of the message
+ $this->from = $from;
+ $this->replyto = $replyto;
+ $this->subject = $subject;
+ $this->body = $body;
+ }
+
+
+
+ /**
+ * Does the per-user customizations to a notification e-mail (name,
+ * timestamp in proper timezone, etc) and sends it out.
+ * Returns true if the mail was sent successfully.
+ *
+ * @param User $watchingUser
+ * @param object $mail
+ * @return bool
+ * @private
+ */
+ function composeAndSendPersonalisedMail( $watchingUser ) {
+ global $wgLang;
+ // From the PHP manual:
+ // Note: The to parameter cannot be an address in the form of "Something <someone@example.com>".
+ // The mail command will not parse this properly while talking with the MTA.
+ $to = new MailAddress( $watchingUser );
+ $body = str_replace( '$WATCHINGUSERNAME', $watchingUser->getName() , $this->body );
+
+ $timecorrection = $watchingUser->getOption( 'timecorrection' );
+
+ # $PAGEEDITDATE is the time and date of the page change
+ # expressed in terms of individual local time of the notification
+ # recipient, i.e. watching user
+ $body = str_replace('$PAGEEDITDATE',
+ $wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ),
+ $body);
+
+ $error = userMailer( $to, $this->from, $this->subject, $body, $this->replyto );
+ return ($error == '');
+ }
+
+} # end of class EmailNotification
+?>
diff --git a/includes/Utf8Case.php b/includes/Utf8Case.php
new file mode 100644
index 00000000..9a2c7302
--- /dev/null
+++ b/includes/Utf8Case.php
@@ -0,0 +1,1506 @@
+<?php
+/**
+ * Simple 1:1 upper/lowercase switching arrays for utf-8 text
+ * Won't get context-sensitive things yet
+ *
+ * Hack for bugs in ucfirst() and company
+ *
+ * These are pulled from memcached if possible, as this is faster than filling
+ * up a big array manually. See also languages/LanguageUtf8.php
+ * @package MediaWiki
+ * @subpackage Language
+ */
+
+/*
+ * Translation array to get upper case character
+ */
+$wikiUpperChars = array(
+ "a" => "A",
+ "b" => "B",
+ "c" => "C",
+ "d" => "D",
+ "e" => "E",
+ "f" => "F",
+ "g" => "G",
+ "h" => "H",
+ "i" => "I",
+ "j" => "J",
+ "k" => "K",
+ "l" => "L",
+ "m" => "M",
+ "n" => "N",
+ "o" => "O",
+ "p" => "P",
+ "q" => "Q",
+ "r" => "R",
+ "s" => "S",
+ "t" => "T",
+ "u" => "U",
+ "v" => "V",
+ "w" => "W",
+ "x" => "X",
+ "y" => "Y",
+ "z" => "Z",
+ "\xc2\xb5" => "\xce\x9c",
+ "\xc3\xa0" => "\xc3\x80",
+ "\xc3\xa1" => "\xc3\x81",
+ "\xc3\xa2" => "\xc3\x82",
+ "\xc3\xa3" => "\xc3\x83",
+ "\xc3\xa4" => "\xc3\x84",
+ "\xc3\xa5" => "\xc3\x85",
+ "\xc3\xa6" => "\xc3\x86",
+ "\xc3\xa7" => "\xc3\x87",
+ "\xc3\xa8" => "\xc3\x88",
+ "\xc3\xa9" => "\xc3\x89",
+ "\xc3\xaa" => "\xc3\x8a",
+ "\xc3\xab" => "\xc3\x8b",
+ "\xc3\xac" => "\xc3\x8c",
+ "\xc3\xad" => "\xc3\x8d",
+ "\xc3\xae" => "\xc3\x8e",
+ "\xc3\xaf" => "\xc3\x8f",
+ "\xc3\xb0" => "\xc3\x90",
+ "\xc3\xb1" => "\xc3\x91",
+ "\xc3\xb2" => "\xc3\x92",
+ "\xc3\xb3" => "\xc3\x93",
+ "\xc3\xb4" => "\xc3\x94",
+ "\xc3\xb5" => "\xc3\x95",
+ "\xc3\xb6" => "\xc3\x96",
+ "\xc3\xb8" => "\xc3\x98",
+ "\xc3\xb9" => "\xc3\x99",
+ "\xc3\xba" => "\xc3\x9a",
+ "\xc3\xbb" => "\xc3\x9b",
+ "\xc3\xbc" => "\xc3\x9c",
+ "\xc3\xbd" => "\xc3\x9d",
+ "\xc3\xbe" => "\xc3\x9e",
+ "\xc3\xbf" => "\xc5\xb8",
+ "\xc4\x81" => "\xc4\x80",
+ "\xc4\x83" => "\xc4\x82",
+ "\xc4\x85" => "\xc4\x84",
+ "\xc4\x87" => "\xc4\x86",
+ "\xc4\x89" => "\xc4\x88",
+ "\xc4\x8b" => "\xc4\x8a",
+ "\xc4\x8d" => "\xc4\x8c",
+ "\xc4\x8f" => "\xc4\x8e",
+ "\xc4\x91" => "\xc4\x90",
+ "\xc4\x93" => "\xc4\x92",
+ "\xc4\x95" => "\xc4\x94",
+ "\xc4\x97" => "\xc4\x96",
+ "\xc4\x99" => "\xc4\x98",
+ "\xc4\x9b" => "\xc4\x9a",
+ "\xc4\x9d" => "\xc4\x9c",
+ "\xc4\x9f" => "\xc4\x9e",
+ "\xc4\xa1" => "\xc4\xa0",
+ "\xc4\xa3" => "\xc4\xa2",
+ "\xc4\xa5" => "\xc4\xa4",
+ "\xc4\xa7" => "\xc4\xa6",
+ "\xc4\xa9" => "\xc4\xa8",
+ "\xc4\xab" => "\xc4\xaa",
+ "\xc4\xad" => "\xc4\xac",
+ "\xc4\xaf" => "\xc4\xae",
+ "\xc4\xb1" => "I",
+ "\xc4\xb3" => "\xc4\xb2",
+ "\xc4\xb5" => "\xc4\xb4",
+ "\xc4\xb7" => "\xc4\xb6",
+ "\xc4\xba" => "\xc4\xb9",
+ "\xc4\xbc" => "\xc4\xbb",
+ "\xc4\xbe" => "\xc4\xbd",
+ "\xc5\x80" => "\xc4\xbf",
+ "\xc5\x82" => "\xc5\x81",
+ "\xc5\x84" => "\xc5\x83",
+ "\xc5\x86" => "\xc5\x85",
+ "\xc5\x88" => "\xc5\x87",
+ "\xc5\x8b" => "\xc5\x8a",
+ "\xc5\x8d" => "\xc5\x8c",
+ "\xc5\x8f" => "\xc5\x8e",
+ "\xc5\x91" => "\xc5\x90",
+ "\xc5\x93" => "\xc5\x92",
+ "\xc5\x95" => "\xc5\x94",
+ "\xc5\x97" => "\xc5\x96",
+ "\xc5\x99" => "\xc5\x98",
+ "\xc5\x9b" => "\xc5\x9a",
+ "\xc5\x9d" => "\xc5\x9c",
+ "\xc5\x9f" => "\xc5\x9e",
+ "\xc5\xa1" => "\xc5\xa0",
+ "\xc5\xa3" => "\xc5\xa2",
+ "\xc5\xa5" => "\xc5\xa4",
+ "\xc5\xa7" => "\xc5\xa6",
+ "\xc5\xa9" => "\xc5\xa8",
+ "\xc5\xab" => "\xc5\xaa",
+ "\xc5\xad" => "\xc5\xac",
+ "\xc5\xaf" => "\xc5\xae",
+ "\xc5\xb1" => "\xc5\xb0",
+ "\xc5\xb3" => "\xc5\xb2",
+ "\xc5\xb5" => "\xc5\xb4",
+ "\xc5\xb7" => "\xc5\xb6",
+ "\xc5\xba" => "\xc5\xb9",
+ "\xc5\xbc" => "\xc5\xbb",
+ "\xc5\xbe" => "\xc5\xbd",
+ "\xc5\xbf" => "S",
+ "\xc6\x83" => "\xc6\x82",
+ "\xc6\x85" => "\xc6\x84",
+ "\xc6\x88" => "\xc6\x87",
+ "\xc6\x8c" => "\xc6\x8b",
+ "\xc6\x92" => "\xc6\x91",
+ "\xc6\x95" => "\xc7\xb6",
+ "\xc6\x99" => "\xc6\x98",
+ "\xc6\xa1" => "\xc6\xa0",
+ "\xc6\xa3" => "\xc6\xa2",
+ "\xc6\xa5" => "\xc6\xa4",
+ "\xc6\xa8" => "\xc6\xa7",
+ "\xc6\xad" => "\xc6\xac",
+ "\xc6\xb0" => "\xc6\xaf",
+ "\xc6\xb4" => "\xc6\xb3",
+ "\xc6\xb6" => "\xc6\xb5",
+ "\xc6\xb9" => "\xc6\xb8",
+ "\xc6\xbd" => "\xc6\xbc",
+ "\xc6\xbf" => "\xc7\xb7",
+ "\xc7\x85" => "\xc7\x84",
+ "\xc7\x86" => "\xc7\x84",
+ "\xc7\x88" => "\xc7\x87",
+ "\xc7\x89" => "\xc7\x87",
+ "\xc7\x8b" => "\xc7\x8a",
+ "\xc7\x8c" => "\xc7\x8a",
+ "\xc7\x8e" => "\xc7\x8d",
+ "\xc7\x90" => "\xc7\x8f",
+ "\xc7\x92" => "\xc7\x91",
+ "\xc7\x94" => "\xc7\x93",
+ "\xc7\x96" => "\xc7\x95",
+ "\xc7\x98" => "\xc7\x97",
+ "\xc7\x9a" => "\xc7\x99",
+ "\xc7\x9c" => "\xc7\x9b",
+ "\xc7\x9d" => "\xc6\x8e",
+ "\xc7\x9f" => "\xc7\x9e",
+ "\xc7\xa1" => "\xc7\xa0",
+ "\xc7\xa3" => "\xc7\xa2",
+ "\xc7\xa5" => "\xc7\xa4",
+ "\xc7\xa7" => "\xc7\xa6",
+ "\xc7\xa9" => "\xc7\xa8",
+ "\xc7\xab" => "\xc7\xaa",
+ "\xc7\xad" => "\xc7\xac",
+ "\xc7\xaf" => "\xc7\xae",
+ "\xc7\xb2" => "\xc7\xb1",
+ "\xc7\xb3" => "\xc7\xb1",
+ "\xc7\xb5" => "\xc7\xb4",
+ "\xc7\xb9" => "\xc7\xb8",
+ "\xc7\xbb" => "\xc7\xba",
+ "\xc7\xbd" => "\xc7\xbc",
+ "\xc7\xbf" => "\xc7\xbe",
+ "\xc8\x81" => "\xc8\x80",
+ "\xc8\x83" => "\xc8\x82",
+ "\xc8\x85" => "\xc8\x84",
+ "\xc8\x87" => "\xc8\x86",
+ "\xc8\x89" => "\xc8\x88",
+ "\xc8\x8b" => "\xc8\x8a",
+ "\xc8\x8d" => "\xc8\x8c",
+ "\xc8\x8f" => "\xc8\x8e",
+ "\xc8\x91" => "\xc8\x90",
+ "\xc8\x93" => "\xc8\x92",
+ "\xc8\x95" => "\xc8\x94",
+ "\xc8\x97" => "\xc8\x96",
+ "\xc8\x99" => "\xc8\x98",
+ "\xc8\x9b" => "\xc8\x9a",
+ "\xc8\x9d" => "\xc8\x9c",
+ "\xc8\x9f" => "\xc8\x9e",
+ "\xc8\xa3" => "\xc8\xa2",
+ "\xc8\xa5" => "\xc8\xa4",
+ "\xc8\xa7" => "\xc8\xa6",
+ "\xc8\xa9" => "\xc8\xa8",
+ "\xc8\xab" => "\xc8\xaa",
+ "\xc8\xad" => "\xc8\xac",
+ "\xc8\xaf" => "\xc8\xae",
+ "\xc8\xb1" => "\xc8\xb0",
+ "\xc8\xb3" => "\xc8\xb2",
+ "\xc9\x93" => "\xc6\x81",
+ "\xc9\x94" => "\xc6\x86",
+ "\xc9\x96" => "\xc6\x89",
+ "\xc9\x97" => "\xc6\x8a",
+ "\xc9\x99" => "\xc6\x8f",
+ "\xc9\x9b" => "\xc6\x90",
+ "\xc9\xa0" => "\xc6\x93",
+ "\xc9\xa3" => "\xc6\x94",
+ "\xc9\xa8" => "\xc6\x97",
+ "\xc9\xa9" => "\xc6\x96",
+ "\xc9\xaf" => "\xc6\x9c",
+ "\xc9\xb2" => "\xc6\x9d",
+ "\xc9\xb5" => "\xc6\x9f",
+ "\xca\x80" => "\xc6\xa6",
+ "\xca\x83" => "\xc6\xa9",
+ "\xca\x88" => "\xc6\xae",
+ "\xca\x8a" => "\xc6\xb1",
+ "\xca\x8b" => "\xc6\xb2",
+ "\xca\x92" => "\xc6\xb7",
+ "\xcd\x85" => "\xce\x99",
+ "\xce\xac" => "\xce\x86",
+ "\xce\xad" => "\xce\x88",
+ "\xce\xae" => "\xce\x89",
+ "\xce\xaf" => "\xce\x8a",
+ "\xce\xb1" => "\xce\x91",
+ "\xce\xb2" => "\xce\x92",
+ "\xce\xb3" => "\xce\x93",
+ "\xce\xb4" => "\xce\x94",
+ "\xce\xb5" => "\xce\x95",
+ "\xce\xb6" => "\xce\x96",
+ "\xce\xb7" => "\xce\x97",
+ "\xce\xb8" => "\xce\x98",
+ "\xce\xb9" => "\xce\x99",
+ "\xce\xba" => "\xce\x9a",
+ "\xce\xbb" => "\xce\x9b",
+ "\xce\xbc" => "\xce\x9c",
+ "\xce\xbd" => "\xce\x9d",
+ "\xce\xbe" => "\xce\x9e",
+ "\xce\xbf" => "\xce\x9f",
+ "\xcf\x80" => "\xce\xa0",
+ "\xcf\x81" => "\xce\xa1",
+ "\xcf\x82" => "\xce\xa3",
+ "\xcf\x83" => "\xce\xa3",
+ "\xcf\x84" => "\xce\xa4",
+ "\xcf\x85" => "\xce\xa5",
+ "\xcf\x86" => "\xce\xa6",
+ "\xcf\x87" => "\xce\xa7",
+ "\xcf\x88" => "\xce\xa8",
+ "\xcf\x89" => "\xce\xa9",
+ "\xcf\x8a" => "\xce\xaa",
+ "\xcf\x8b" => "\xce\xab",
+ "\xcf\x8c" => "\xce\x8c",
+ "\xcf\x8d" => "\xce\x8e",
+ "\xcf\x8e" => "\xce\x8f",
+ "\xcf\x90" => "\xce\x92",
+ "\xcf\x91" => "\xce\x98",
+ "\xcf\x95" => "\xce\xa6",
+ "\xcf\x96" => "\xce\xa0",
+ "\xcf\x9b" => "\xcf\x9a",
+ "\xcf\x9d" => "\xcf\x9c",
+ "\xcf\x9f" => "\xcf\x9e",
+ "\xcf\xa1" => "\xcf\xa0",
+ "\xcf\xa3" => "\xcf\xa2",
+ "\xcf\xa5" => "\xcf\xa4",
+ "\xcf\xa7" => "\xcf\xa6",
+ "\xcf\xa9" => "\xcf\xa8",
+ "\xcf\xab" => "\xcf\xaa",
+ "\xcf\xad" => "\xcf\xac",
+ "\xcf\xaf" => "\xcf\xae",
+ "\xcf\xb0" => "\xce\x9a",
+ "\xcf\xb1" => "\xce\xa1",
+ "\xcf\xb2" => "\xce\xa3",
+ "\xcf\xb5" => "\xce\x95",
+ "\xd0\xb0" => "\xd0\x90",
+ "\xd0\xb1" => "\xd0\x91",
+ "\xd0\xb2" => "\xd0\x92",
+ "\xd0\xb3" => "\xd0\x93",
+ "\xd0\xb4" => "\xd0\x94",
+ "\xd0\xb5" => "\xd0\x95",
+ "\xd0\xb6" => "\xd0\x96",
+ "\xd0\xb7" => "\xd0\x97",
+ "\xd0\xb8" => "\xd0\x98",
+ "\xd0\xb9" => "\xd0\x99",
+ "\xd0\xba" => "\xd0\x9a",
+ "\xd0\xbb" => "\xd0\x9b",
+ "\xd0\xbc" => "\xd0\x9c",
+ "\xd0\xbd" => "\xd0\x9d",
+ "\xd0\xbe" => "\xd0\x9e",
+ "\xd0\xbf" => "\xd0\x9f",
+ "\xd1\x80" => "\xd0\xa0",
+ "\xd1\x81" => "\xd0\xa1",
+ "\xd1\x82" => "\xd0\xa2",
+ "\xd1\x83" => "\xd0\xa3",
+ "\xd1\x84" => "\xd0\xa4",
+ "\xd1\x85" => "\xd0\xa5",
+ "\xd1\x86" => "\xd0\xa6",
+ "\xd1\x87" => "\xd0\xa7",
+ "\xd1\x88" => "\xd0\xa8",
+ "\xd1\x89" => "\xd0\xa9",
+ "\xd1\x8a" => "\xd0\xaa",
+ "\xd1\x8b" => "\xd0\xab",
+ "\xd1\x8c" => "\xd0\xac",
+ "\xd1\x8d" => "\xd0\xad",
+ "\xd1\x8e" => "\xd0\xae",
+ "\xd1\x8f" => "\xd0\xaf",
+ "\xd1\x90" => "\xd0\x80",
+ "\xd1\x91" => "\xd0\x81",
+ "\xd1\x92" => "\xd0\x82",
+ "\xd1\x93" => "\xd0\x83",
+ "\xd1\x94" => "\xd0\x84",
+ "\xd1\x95" => "\xd0\x85",
+ "\xd1\x96" => "\xd0\x86",
+ "\xd1\x97" => "\xd0\x87",
+ "\xd1\x98" => "\xd0\x88",
+ "\xd1\x99" => "\xd0\x89",
+ "\xd1\x9a" => "\xd0\x8a",
+ "\xd1\x9b" => "\xd0\x8b",
+ "\xd1\x9c" => "\xd0\x8c",
+ "\xd1\x9d" => "\xd0\x8d",
+ "\xd1\x9e" => "\xd0\x8e",
+ "\xd1\x9f" => "\xd0\x8f",
+ "\xd1\xa1" => "\xd1\xa0",
+ "\xd1\xa3" => "\xd1\xa2",
+ "\xd1\xa5" => "\xd1\xa4",
+ "\xd1\xa7" => "\xd1\xa6",
+ "\xd1\xa9" => "\xd1\xa8",
+ "\xd1\xab" => "\xd1\xaa",
+ "\xd1\xad" => "\xd1\xac",
+ "\xd1\xaf" => "\xd1\xae",
+ "\xd1\xb1" => "\xd1\xb0",
+ "\xd1\xb3" => "\xd1\xb2",
+ "\xd1\xb5" => "\xd1\xb4",
+ "\xd1\xb7" => "\xd1\xb6",
+ "\xd1\xb9" => "\xd1\xb8",
+ "\xd1\xbb" => "\xd1\xba",
+ "\xd1\xbd" => "\xd1\xbc",
+ "\xd1\xbf" => "\xd1\xbe",
+ "\xd2\x81" => "\xd2\x80",
+ "\xd2\x8d" => "\xd2\x8c",
+ "\xd2\x8f" => "\xd2\x8e",
+ "\xd2\x91" => "\xd2\x90",
+ "\xd2\x93" => "\xd2\x92",
+ "\xd2\x95" => "\xd2\x94",
+ "\xd2\x97" => "\xd2\x96",
+ "\xd2\x99" => "\xd2\x98",
+ "\xd2\x9b" => "\xd2\x9a",
+ "\xd2\x9d" => "\xd2\x9c",
+ "\xd2\x9f" => "\xd2\x9e",
+ "\xd2\xa1" => "\xd2\xa0",
+ "\xd2\xa3" => "\xd2\xa2",
+ "\xd2\xa5" => "\xd2\xa4",
+ "\xd2\xa7" => "\xd2\xa6",
+ "\xd2\xa9" => "\xd2\xa8",
+ "\xd2\xab" => "\xd2\xaa",
+ "\xd2\xad" => "\xd2\xac",
+ "\xd2\xaf" => "\xd2\xae",
+ "\xd2\xb1" => "\xd2\xb0",
+ "\xd2\xb3" => "\xd2\xb2",
+ "\xd2\xb5" => "\xd2\xb4",
+ "\xd2\xb7" => "\xd2\xb6",
+ "\xd2\xb9" => "\xd2\xb8",
+ "\xd2\xbb" => "\xd2\xba",
+ "\xd2\xbd" => "\xd2\xbc",
+ "\xd2\xbf" => "\xd2\xbe",
+ "\xd3\x82" => "\xd3\x81",
+ "\xd3\x84" => "\xd3\x83",
+ "\xd3\x88" => "\xd3\x87",
+ "\xd3\x8c" => "\xd3\x8b",
+ "\xd3\x91" => "\xd3\x90",
+ "\xd3\x93" => "\xd3\x92",
+ "\xd3\x95" => "\xd3\x94",
+ "\xd3\x97" => "\xd3\x96",
+ "\xd3\x99" => "\xd3\x98",
+ "\xd3\x9b" => "\xd3\x9a",
+ "\xd3\x9d" => "\xd3\x9c",
+ "\xd3\x9f" => "\xd3\x9e",
+ "\xd3\xa1" => "\xd3\xa0",
+ "\xd3\xa3" => "\xd3\xa2",
+ "\xd3\xa5" => "\xd3\xa4",
+ "\xd3\xa7" => "\xd3\xa6",
+ "\xd3\xa9" => "\xd3\xa8",
+ "\xd3\xab" => "\xd3\xaa",
+ "\xd3\xad" => "\xd3\xac",
+ "\xd3\xaf" => "\xd3\xae",
+ "\xd3\xb1" => "\xd3\xb0",
+ "\xd3\xb3" => "\xd3\xb2",
+ "\xd3\xb5" => "\xd3\xb4",
+ "\xd3\xb9" => "\xd3\xb8",
+ "\xd5\xa1" => "\xd4\xb1",
+ "\xd5\xa2" => "\xd4\xb2",
+ "\xd5\xa3" => "\xd4\xb3",
+ "\xd5\xa4" => "\xd4\xb4",
+ "\xd5\xa5" => "\xd4\xb5",
+ "\xd5\xa6" => "\xd4\xb6",
+ "\xd5\xa7" => "\xd4\xb7",
+ "\xd5\xa8" => "\xd4\xb8",
+ "\xd5\xa9" => "\xd4\xb9",
+ "\xd5\xaa" => "\xd4\xba",
+ "\xd5\xab" => "\xd4\xbb",
+ "\xd5\xac" => "\xd4\xbc",
+ "\xd5\xad" => "\xd4\xbd",
+ "\xd5\xae" => "\xd4\xbe",
+ "\xd5\xaf" => "\xd4\xbf",
+ "\xd5\xb0" => "\xd5\x80",
+ "\xd5\xb1" => "\xd5\x81",
+ "\xd5\xb2" => "\xd5\x82",
+ "\xd5\xb3" => "\xd5\x83",
+ "\xd5\xb4" => "\xd5\x84",
+ "\xd5\xb5" => "\xd5\x85",
+ "\xd5\xb6" => "\xd5\x86",
+ "\xd5\xb7" => "\xd5\x87",
+ "\xd5\xb8" => "\xd5\x88",
+ "\xd5\xb9" => "\xd5\x89",
+ "\xd5\xba" => "\xd5\x8a",
+ "\xd5\xbb" => "\xd5\x8b",
+ "\xd5\xbc" => "\xd5\x8c",
+ "\xd5\xbd" => "\xd5\x8d",
+ "\xd5\xbe" => "\xd5\x8e",
+ "\xd5\xbf" => "\xd5\x8f",
+ "\xd6\x80" => "\xd5\x90",
+ "\xd6\x81" => "\xd5\x91",
+ "\xd6\x82" => "\xd5\x92",
+ "\xd6\x83" => "\xd5\x93",
+ "\xd6\x84" => "\xd5\x94",
+ "\xd6\x85" => "\xd5\x95",
+ "\xd6\x86" => "\xd5\x96",
+ "\xe1\xb8\x81" => "\xe1\xb8\x80",
+ "\xe1\xb8\x83" => "\xe1\xb8\x82",
+ "\xe1\xb8\x85" => "\xe1\xb8\x84",
+ "\xe1\xb8\x87" => "\xe1\xb8\x86",
+ "\xe1\xb8\x89" => "\xe1\xb8\x88",
+ "\xe1\xb8\x8b" => "\xe1\xb8\x8a",
+ "\xe1\xb8\x8d" => "\xe1\xb8\x8c",
+ "\xe1\xb8\x8f" => "\xe1\xb8\x8e",
+ "\xe1\xb8\x91" => "\xe1\xb8\x90",
+ "\xe1\xb8\x93" => "\xe1\xb8\x92",
+ "\xe1\xb8\x95" => "\xe1\xb8\x94",
+ "\xe1\xb8\x97" => "\xe1\xb8\x96",
+ "\xe1\xb8\x99" => "\xe1\xb8\x98",
+ "\xe1\xb8\x9b" => "\xe1\xb8\x9a",
+ "\xe1\xb8\x9d" => "\xe1\xb8\x9c",
+ "\xe1\xb8\x9f" => "\xe1\xb8\x9e",
+ "\xe1\xb8\xa1" => "\xe1\xb8\xa0",
+ "\xe1\xb8\xa3" => "\xe1\xb8\xa2",
+ "\xe1\xb8\xa5" => "\xe1\xb8\xa4",
+ "\xe1\xb8\xa7" => "\xe1\xb8\xa6",
+ "\xe1\xb8\xa9" => "\xe1\xb8\xa8",
+ "\xe1\xb8\xab" => "\xe1\xb8\xaa",
+ "\xe1\xb8\xad" => "\xe1\xb8\xac",
+ "\xe1\xb8\xaf" => "\xe1\xb8\xae",
+ "\xe1\xb8\xb1" => "\xe1\xb8\xb0",
+ "\xe1\xb8\xb3" => "\xe1\xb8\xb2",
+ "\xe1\xb8\xb5" => "\xe1\xb8\xb4",
+ "\xe1\xb8\xb7" => "\xe1\xb8\xb6",
+ "\xe1\xb8\xb9" => "\xe1\xb8\xb8",
+ "\xe1\xb8\xbb" => "\xe1\xb8\xba",
+ "\xe1\xb8\xbd" => "\xe1\xb8\xbc",
+ "\xe1\xb8\xbf" => "\xe1\xb8\xbe",
+ "\xe1\xb9\x81" => "\xe1\xb9\x80",
+ "\xe1\xb9\x83" => "\xe1\xb9\x82",
+ "\xe1\xb9\x85" => "\xe1\xb9\x84",
+ "\xe1\xb9\x87" => "\xe1\xb9\x86",
+ "\xe1\xb9\x89" => "\xe1\xb9\x88",
+ "\xe1\xb9\x8b" => "\xe1\xb9\x8a",
+ "\xe1\xb9\x8d" => "\xe1\xb9\x8c",
+ "\xe1\xb9\x8f" => "\xe1\xb9\x8e",
+ "\xe1\xb9\x91" => "\xe1\xb9\x90",
+ "\xe1\xb9\x93" => "\xe1\xb9\x92",
+ "\xe1\xb9\x95" => "\xe1\xb9\x94",
+ "\xe1\xb9\x97" => "\xe1\xb9\x96",
+ "\xe1\xb9\x99" => "\xe1\xb9\x98",
+ "\xe1\xb9\x9b" => "\xe1\xb9\x9a",
+ "\xe1\xb9\x9d" => "\xe1\xb9\x9c",
+ "\xe1\xb9\x9f" => "\xe1\xb9\x9e",
+ "\xe1\xb9\xa1" => "\xe1\xb9\xa0",
+ "\xe1\xb9\xa3" => "\xe1\xb9\xa2",
+ "\xe1\xb9\xa5" => "\xe1\xb9\xa4",
+ "\xe1\xb9\xa7" => "\xe1\xb9\xa6",
+ "\xe1\xb9\xa9" => "\xe1\xb9\xa8",
+ "\xe1\xb9\xab" => "\xe1\xb9\xaa",
+ "\xe1\xb9\xad" => "\xe1\xb9\xac",
+ "\xe1\xb9\xaf" => "\xe1\xb9\xae",
+ "\xe1\xb9\xb1" => "\xe1\xb9\xb0",
+ "\xe1\xb9\xb3" => "\xe1\xb9\xb2",
+ "\xe1\xb9\xb5" => "\xe1\xb9\xb4",
+ "\xe1\xb9\xb7" => "\xe1\xb9\xb6",
+ "\xe1\xb9\xb9" => "\xe1\xb9\xb8",
+ "\xe1\xb9\xbb" => "\xe1\xb9\xba",
+ "\xe1\xb9\xbd" => "\xe1\xb9\xbc",
+ "\xe1\xb9\xbf" => "\xe1\xb9\xbe",
+ "\xe1\xba\x81" => "\xe1\xba\x80",
+ "\xe1\xba\x83" => "\xe1\xba\x82",
+ "\xe1\xba\x85" => "\xe1\xba\x84",
+ "\xe1\xba\x87" => "\xe1\xba\x86",
+ "\xe1\xba\x89" => "\xe1\xba\x88",
+ "\xe1\xba\x8b" => "\xe1\xba\x8a",
+ "\xe1\xba\x8d" => "\xe1\xba\x8c",
+ "\xe1\xba\x8f" => "\xe1\xba\x8e",
+ "\xe1\xba\x91" => "\xe1\xba\x90",
+ "\xe1\xba\x93" => "\xe1\xba\x92",
+ "\xe1\xba\x95" => "\xe1\xba\x94",
+ "\xe1\xba\x9b" => "\xe1\xb9\xa0",
+ "\xe1\xba\xa1" => "\xe1\xba\xa0",
+ "\xe1\xba\xa3" => "\xe1\xba\xa2",
+ "\xe1\xba\xa5" => "\xe1\xba\xa4",
+ "\xe1\xba\xa7" => "\xe1\xba\xa6",
+ "\xe1\xba\xa9" => "\xe1\xba\xa8",
+ "\xe1\xba\xab" => "\xe1\xba\xaa",
+ "\xe1\xba\xad" => "\xe1\xba\xac",
+ "\xe1\xba\xaf" => "\xe1\xba\xae",
+ "\xe1\xba\xb1" => "\xe1\xba\xb0",
+ "\xe1\xba\xb3" => "\xe1\xba\xb2",
+ "\xe1\xba\xb5" => "\xe1\xba\xb4",
+ "\xe1\xba\xb7" => "\xe1\xba\xb6",
+ "\xe1\xba\xb9" => "\xe1\xba\xb8",
+ "\xe1\xba\xbb" => "\xe1\xba\xba",
+ "\xe1\xba\xbd" => "\xe1\xba\xbc",
+ "\xe1\xba\xbf" => "\xe1\xba\xbe",
+ "\xe1\xbb\x81" => "\xe1\xbb\x80",
+ "\xe1\xbb\x83" => "\xe1\xbb\x82",
+ "\xe1\xbb\x85" => "\xe1\xbb\x84",
+ "\xe1\xbb\x87" => "\xe1\xbb\x86",
+ "\xe1\xbb\x89" => "\xe1\xbb\x88",
+ "\xe1\xbb\x8b" => "\xe1\xbb\x8a",
+ "\xe1\xbb\x8d" => "\xe1\xbb\x8c",
+ "\xe1\xbb\x8f" => "\xe1\xbb\x8e",
+ "\xe1\xbb\x91" => "\xe1\xbb\x90",
+ "\xe1\xbb\x93" => "\xe1\xbb\x92",
+ "\xe1\xbb\x95" => "\xe1\xbb\x94",
+ "\xe1\xbb\x97" => "\xe1\xbb\x96",
+ "\xe1\xbb\x99" => "\xe1\xbb\x98",
+ "\xe1\xbb\x9b" => "\xe1\xbb\x9a",
+ "\xe1\xbb\x9d" => "\xe1\xbb\x9c",
+ "\xe1\xbb\x9f" => "\xe1\xbb\x9e",
+ "\xe1\xbb\xa1" => "\xe1\xbb\xa0",
+ "\xe1\xbb\xa3" => "\xe1\xbb\xa2",
+ "\xe1\xbb\xa5" => "\xe1\xbb\xa4",
+ "\xe1\xbb\xa7" => "\xe1\xbb\xa6",
+ "\xe1\xbb\xa9" => "\xe1\xbb\xa8",
+ "\xe1\xbb\xab" => "\xe1\xbb\xaa",
+ "\xe1\xbb\xad" => "\xe1\xbb\xac",
+ "\xe1\xbb\xaf" => "\xe1\xbb\xae",
+ "\xe1\xbb\xb1" => "\xe1\xbb\xb0",
+ "\xe1\xbb\xb3" => "\xe1\xbb\xb2",
+ "\xe1\xbb\xb5" => "\xe1\xbb\xb4",
+ "\xe1\xbb\xb7" => "\xe1\xbb\xb6",
+ "\xe1\xbb\xb9" => "\xe1\xbb\xb8",
+ "\xe1\xbc\x80" => "\xe1\xbc\x88",
+ "\xe1\xbc\x81" => "\xe1\xbc\x89",
+ "\xe1\xbc\x82" => "\xe1\xbc\x8a",
+ "\xe1\xbc\x83" => "\xe1\xbc\x8b",
+ "\xe1\xbc\x84" => "\xe1\xbc\x8c",
+ "\xe1\xbc\x85" => "\xe1\xbc\x8d",
+ "\xe1\xbc\x86" => "\xe1\xbc\x8e",
+ "\xe1\xbc\x87" => "\xe1\xbc\x8f",
+ "\xe1\xbc\x90" => "\xe1\xbc\x98",
+ "\xe1\xbc\x91" => "\xe1\xbc\x99",
+ "\xe1\xbc\x92" => "\xe1\xbc\x9a",
+ "\xe1\xbc\x93" => "\xe1\xbc\x9b",
+ "\xe1\xbc\x94" => "\xe1\xbc\x9c",
+ "\xe1\xbc\x95" => "\xe1\xbc\x9d",
+ "\xe1\xbc\xa0" => "\xe1\xbc\xa8",
+ "\xe1\xbc\xa1" => "\xe1\xbc\xa9",
+ "\xe1\xbc\xa2" => "\xe1\xbc\xaa",
+ "\xe1\xbc\xa3" => "\xe1\xbc\xab",
+ "\xe1\xbc\xa4" => "\xe1\xbc\xac",
+ "\xe1\xbc\xa5" => "\xe1\xbc\xad",
+ "\xe1\xbc\xa6" => "\xe1\xbc\xae",
+ "\xe1\xbc\xa7" => "\xe1\xbc\xaf",
+ "\xe1\xbc\xb0" => "\xe1\xbc\xb8",
+ "\xe1\xbc\xb1" => "\xe1\xbc\xb9",
+ "\xe1\xbc\xb2" => "\xe1\xbc\xba",
+ "\xe1\xbc\xb3" => "\xe1\xbc\xbb",
+ "\xe1\xbc\xb4" => "\xe1\xbc\xbc",
+ "\xe1\xbc\xb5" => "\xe1\xbc\xbd",
+ "\xe1\xbc\xb6" => "\xe1\xbc\xbe",
+ "\xe1\xbc\xb7" => "\xe1\xbc\xbf",
+ "\xe1\xbd\x80" => "\xe1\xbd\x88",
+ "\xe1\xbd\x81" => "\xe1\xbd\x89",
+ "\xe1\xbd\x82" => "\xe1\xbd\x8a",
+ "\xe1\xbd\x83" => "\xe1\xbd\x8b",
+ "\xe1\xbd\x84" => "\xe1\xbd\x8c",
+ "\xe1\xbd\x85" => "\xe1\xbd\x8d",
+ "\xe1\xbd\x91" => "\xe1\xbd\x99",
+ "\xe1\xbd\x93" => "\xe1\xbd\x9b",
+ "\xe1\xbd\x95" => "\xe1\xbd\x9d",
+ "\xe1\xbd\x97" => "\xe1\xbd\x9f",
+ "\xe1\xbd\xa0" => "\xe1\xbd\xa8",
+ "\xe1\xbd\xa1" => "\xe1\xbd\xa9",
+ "\xe1\xbd\xa2" => "\xe1\xbd\xaa",
+ "\xe1\xbd\xa3" => "\xe1\xbd\xab",
+ "\xe1\xbd\xa4" => "\xe1\xbd\xac",
+ "\xe1\xbd\xa5" => "\xe1\xbd\xad",
+ "\xe1\xbd\xa6" => "\xe1\xbd\xae",
+ "\xe1\xbd\xa7" => "\xe1\xbd\xaf",
+ "\xe1\xbd\xb0" => "\xe1\xbe\xba",
+ "\xe1\xbd\xb1" => "\xe1\xbe\xbb",
+ "\xe1\xbd\xb2" => "\xe1\xbf\x88",
+ "\xe1\xbd\xb3" => "\xe1\xbf\x89",
+ "\xe1\xbd\xb4" => "\xe1\xbf\x8a",
+ "\xe1\xbd\xb5" => "\xe1\xbf\x8b",
+ "\xe1\xbd\xb6" => "\xe1\xbf\x9a",
+ "\xe1\xbd\xb7" => "\xe1\xbf\x9b",
+ "\xe1\xbd\xb8" => "\xe1\xbf\xb8",
+ "\xe1\xbd\xb9" => "\xe1\xbf\xb9",
+ "\xe1\xbd\xba" => "\xe1\xbf\xaa",
+ "\xe1\xbd\xbb" => "\xe1\xbf\xab",
+ "\xe1\xbd\xbc" => "\xe1\xbf\xba",
+ "\xe1\xbd\xbd" => "\xe1\xbf\xbb",
+ "\xe1\xbe\x80" => "\xe1\xbe\x88",
+ "\xe1\xbe\x81" => "\xe1\xbe\x89",
+ "\xe1\xbe\x82" => "\xe1\xbe\x8a",
+ "\xe1\xbe\x83" => "\xe1\xbe\x8b",
+ "\xe1\xbe\x84" => "\xe1\xbe\x8c",
+ "\xe1\xbe\x85" => "\xe1\xbe\x8d",
+ "\xe1\xbe\x86" => "\xe1\xbe\x8e",
+ "\xe1\xbe\x87" => "\xe1\xbe\x8f",
+ "\xe1\xbe\x90" => "\xe1\xbe\x98",
+ "\xe1\xbe\x91" => "\xe1\xbe\x99",
+ "\xe1\xbe\x92" => "\xe1\xbe\x9a",
+ "\xe1\xbe\x93" => "\xe1\xbe\x9b",
+ "\xe1\xbe\x94" => "\xe1\xbe\x9c",
+ "\xe1\xbe\x95" => "\xe1\xbe\x9d",
+ "\xe1\xbe\x96" => "\xe1\xbe\x9e",
+ "\xe1\xbe\x97" => "\xe1\xbe\x9f",
+ "\xe1\xbe\xa0" => "\xe1\xbe\xa8",
+ "\xe1\xbe\xa1" => "\xe1\xbe\xa9",
+ "\xe1\xbe\xa2" => "\xe1\xbe\xaa",
+ "\xe1\xbe\xa3" => "\xe1\xbe\xab",
+ "\xe1\xbe\xa4" => "\xe1\xbe\xac",
+ "\xe1\xbe\xa5" => "\xe1\xbe\xad",
+ "\xe1\xbe\xa6" => "\xe1\xbe\xae",
+ "\xe1\xbe\xa7" => "\xe1\xbe\xaf",
+ "\xe1\xbe\xb0" => "\xe1\xbe\xb8",
+ "\xe1\xbe\xb1" => "\xe1\xbe\xb9",
+ "\xe1\xbe\xb3" => "\xe1\xbe\xbc",
+ "\xe1\xbe\xbe" => "\xce\x99",
+ "\xe1\xbf\x83" => "\xe1\xbf\x8c",
+ "\xe1\xbf\x90" => "\xe1\xbf\x98",
+ "\xe1\xbf\x91" => "\xe1\xbf\x99",
+ "\xe1\xbf\xa0" => "\xe1\xbf\xa8",
+ "\xe1\xbf\xa1" => "\xe1\xbf\xa9",
+ "\xe1\xbf\xa5" => "\xe1\xbf\xac",
+ "\xe1\xbf\xb3" => "\xe1\xbf\xbc",
+ "\xe2\x85\xb0" => "\xe2\x85\xa0",
+ "\xe2\x85\xb1" => "\xe2\x85\xa1",
+ "\xe2\x85\xb2" => "\xe2\x85\xa2",
+ "\xe2\x85\xb3" => "\xe2\x85\xa3",
+ "\xe2\x85\xb4" => "\xe2\x85\xa4",
+ "\xe2\x85\xb5" => "\xe2\x85\xa5",
+ "\xe2\x85\xb6" => "\xe2\x85\xa6",
+ "\xe2\x85\xb7" => "\xe2\x85\xa7",
+ "\xe2\x85\xb8" => "\xe2\x85\xa8",
+ "\xe2\x85\xb9" => "\xe2\x85\xa9",
+ "\xe2\x85\xba" => "\xe2\x85\xaa",
+ "\xe2\x85\xbb" => "\xe2\x85\xab",
+ "\xe2\x85\xbc" => "\xe2\x85\xac",
+ "\xe2\x85\xbd" => "\xe2\x85\xad",
+ "\xe2\x85\xbe" => "\xe2\x85\xae",
+ "\xe2\x85\xbf" => "\xe2\x85\xaf",
+ "\xe2\x93\x90" => "\xe2\x92\xb6",
+ "\xe2\x93\x91" => "\xe2\x92\xb7",
+ "\xe2\x93\x92" => "\xe2\x92\xb8",
+ "\xe2\x93\x93" => "\xe2\x92\xb9",
+ "\xe2\x93\x94" => "\xe2\x92\xba",
+ "\xe2\x93\x95" => "\xe2\x92\xbb",
+ "\xe2\x93\x96" => "\xe2\x92\xbc",
+ "\xe2\x93\x97" => "\xe2\x92\xbd",
+ "\xe2\x93\x98" => "\xe2\x92\xbe",
+ "\xe2\x93\x99" => "\xe2\x92\xbf",
+ "\xe2\x93\x9a" => "\xe2\x93\x80",
+ "\xe2\x93\x9b" => "\xe2\x93\x81",
+ "\xe2\x93\x9c" => "\xe2\x93\x82",
+ "\xe2\x93\x9d" => "\xe2\x93\x83",
+ "\xe2\x93\x9e" => "\xe2\x93\x84",
+ "\xe2\x93\x9f" => "\xe2\x93\x85",
+ "\xe2\x93\xa0" => "\xe2\x93\x86",
+ "\xe2\x93\xa1" => "\xe2\x93\x87",
+ "\xe2\x93\xa2" => "\xe2\x93\x88",
+ "\xe2\x93\xa3" => "\xe2\x93\x89",
+ "\xe2\x93\xa4" => "\xe2\x93\x8a",
+ "\xe2\x93\xa5" => "\xe2\x93\x8b",
+ "\xe2\x93\xa6" => "\xe2\x93\x8c",
+ "\xe2\x93\xa7" => "\xe2\x93\x8d",
+ "\xe2\x93\xa8" => "\xe2\x93\x8e",
+ "\xe2\x93\xa9" => "\xe2\x93\x8f",
+ "\xef\xbd\x81" => "\xef\xbc\xa1",
+ "\xef\xbd\x82" => "\xef\xbc\xa2",
+ "\xef\xbd\x83" => "\xef\xbc\xa3",
+ "\xef\xbd\x84" => "\xef\xbc\xa4",
+ "\xef\xbd\x85" => "\xef\xbc\xa5",
+ "\xef\xbd\x86" => "\xef\xbc\xa6",
+ "\xef\xbd\x87" => "\xef\xbc\xa7",
+ "\xef\xbd\x88" => "\xef\xbc\xa8",
+ "\xef\xbd\x89" => "\xef\xbc\xa9",
+ "\xef\xbd\x8a" => "\xef\xbc\xaa",
+ "\xef\xbd\x8b" => "\xef\xbc\xab",
+ "\xef\xbd\x8c" => "\xef\xbc\xac",
+ "\xef\xbd\x8d" => "\xef\xbc\xad",
+ "\xef\xbd\x8e" => "\xef\xbc\xae",
+ "\xef\xbd\x8f" => "\xef\xbc\xaf",
+ "\xef\xbd\x90" => "\xef\xbc\xb0",
+ "\xef\xbd\x91" => "\xef\xbc\xb1",
+ "\xef\xbd\x92" => "\xef\xbc\xb2",
+ "\xef\xbd\x93" => "\xef\xbc\xb3",
+ "\xef\xbd\x94" => "\xef\xbc\xb4",
+ "\xef\xbd\x95" => "\xef\xbc\xb5",
+ "\xef\xbd\x96" => "\xef\xbc\xb6",
+ "\xef\xbd\x97" => "\xef\xbc\xb7",
+ "\xef\xbd\x98" => "\xef\xbc\xb8",
+ "\xef\xbd\x99" => "\xef\xbc\xb9",
+ "\xef\xbd\x9a" => "\xef\xbc\xba",
+ "\xf0\x90\x90\xa8" => "\xf0\x90\x90\x80",
+ "\xf0\x90\x90\xa9" => "\xf0\x90\x90\x81",
+ "\xf0\x90\x90\xaa" => "\xf0\x90\x90\x82",
+ "\xf0\x90\x90\xab" => "\xf0\x90\x90\x83",
+ "\xf0\x90\x90\xac" => "\xf0\x90\x90\x84",
+ "\xf0\x90\x90\xad" => "\xf0\x90\x90\x85",
+ "\xf0\x90\x90\xae" => "\xf0\x90\x90\x86",
+ "\xf0\x90\x90\xaf" => "\xf0\x90\x90\x87",
+ "\xf0\x90\x90\xb0" => "\xf0\x90\x90\x88",
+ "\xf0\x90\x90\xb1" => "\xf0\x90\x90\x89",
+ "\xf0\x90\x90\xb2" => "\xf0\x90\x90\x8a",
+ "\xf0\x90\x90\xb3" => "\xf0\x90\x90\x8b",
+ "\xf0\x90\x90\xb4" => "\xf0\x90\x90\x8c",
+ "\xf0\x90\x90\xb5" => "\xf0\x90\x90\x8d",
+ "\xf0\x90\x90\xb6" => "\xf0\x90\x90\x8e",
+ "\xf0\x90\x90\xb7" => "\xf0\x90\x90\x8f",
+ "\xf0\x90\x90\xb8" => "\xf0\x90\x90\x90",
+ "\xf0\x90\x90\xb9" => "\xf0\x90\x90\x91",
+ "\xf0\x90\x90\xba" => "\xf0\x90\x90\x92",
+ "\xf0\x90\x90\xbb" => "\xf0\x90\x90\x93",
+ "\xf0\x90\x90\xbc" => "\xf0\x90\x90\x94",
+ "\xf0\x90\x90\xbd" => "\xf0\x90\x90\x95",
+ "\xf0\x90\x90\xbe" => "\xf0\x90\x90\x96",
+ "\xf0\x90\x90\xbf" => "\xf0\x90\x90\x97",
+ "\xf0\x90\x91\x80" => "\xf0\x90\x90\x98",
+ "\xf0\x90\x91\x81" => "\xf0\x90\x90\x99",
+ "\xf0\x90\x91\x82" => "\xf0\x90\x90\x9a",
+ "\xf0\x90\x91\x83" => "\xf0\x90\x90\x9b",
+ "\xf0\x90\x91\x84" => "\xf0\x90\x90\x9c",
+ "\xf0\x90\x91\x85" => "\xf0\x90\x90\x9d",
+ "\xf0\x90\x91\x86" => "\xf0\x90\x90\x9e",
+ "\xf0\x90\x91\x87" => "\xf0\x90\x90\x9f",
+ "\xf0\x90\x91\x88" => "\xf0\x90\x90\xa0",
+ "\xf0\x90\x91\x89" => "\xf0\x90\x90\xa1",
+ "\xf0\x90\x91\x8a" => "\xf0\x90\x90\xa2",
+ "\xf0\x90\x91\x8b" => "\xf0\x90\x90\xa3",
+ "\xf0\x90\x91\x8c" => "\xf0\x90\x90\xa4",
+ "\xf0\x90\x91\x8d" => "\xf0\x90\x90\xa5"
+);
+
+/*
+ * Translation array to get lower case character
+ */
+$wikiLowerChars = array (
+ "A" => "a",
+ "B" => "b",
+ "C" => "c",
+ "D" => "d",
+ "E" => "e",
+ "F" => "f",
+ "G" => "g",
+ "H" => "h",
+ "I" => "i",
+ "J" => "j",
+ "K" => "k",
+ "L" => "l",
+ "M" => "m",
+ "N" => "n",
+ "O" => "o",
+ "P" => "p",
+ "Q" => "q",
+ "R" => "r",
+ "S" => "s",
+ "T" => "t",
+ "U" => "u",
+ "V" => "v",
+ "W" => "w",
+ "X" => "x",
+ "Y" => "y",
+ "Z" => "z",
+ "\xc3\x80" => "\xc3\xa0",
+ "\xc3\x81" => "\xc3\xa1",
+ "\xc3\x82" => "\xc3\xa2",
+ "\xc3\x83" => "\xc3\xa3",
+ "\xc3\x84" => "\xc3\xa4",
+ "\xc3\x85" => "\xc3\xa5",
+ "\xc3\x86" => "\xc3\xa6",
+ "\xc3\x87" => "\xc3\xa7",
+ "\xc3\x88" => "\xc3\xa8",
+ "\xc3\x89" => "\xc3\xa9",
+ "\xc3\x8a" => "\xc3\xaa",
+ "\xc3\x8b" => "\xc3\xab",
+ "\xc3\x8c" => "\xc3\xac",
+ "\xc3\x8d" => "\xc3\xad",
+ "\xc3\x8e" => "\xc3\xae",
+ "\xc3\x8f" => "\xc3\xaf",
+ "\xc3\x90" => "\xc3\xb0",
+ "\xc3\x91" => "\xc3\xb1",
+ "\xc3\x92" => "\xc3\xb2",
+ "\xc3\x93" => "\xc3\xb3",
+ "\xc3\x94" => "\xc3\xb4",
+ "\xc3\x95" => "\xc3\xb5",
+ "\xc3\x96" => "\xc3\xb6",
+ "\xc3\x98" => "\xc3\xb8",
+ "\xc3\x99" => "\xc3\xb9",
+ "\xc3\x9a" => "\xc3\xba",
+ "\xc3\x9b" => "\xc3\xbb",
+ "\xc3\x9c" => "\xc3\xbc",
+ "\xc3\x9d" => "\xc3\xbd",
+ "\xc3\x9e" => "\xc3\xbe",
+ "\xc4\x80" => "\xc4\x81",
+ "\xc4\x82" => "\xc4\x83",
+ "\xc4\x84" => "\xc4\x85",
+ "\xc4\x86" => "\xc4\x87",
+ "\xc4\x88" => "\xc4\x89",
+ "\xc4\x8a" => "\xc4\x8b",
+ "\xc4\x8c" => "\xc4\x8d",
+ "\xc4\x8e" => "\xc4\x8f",
+ "\xc4\x90" => "\xc4\x91",
+ "\xc4\x92" => "\xc4\x93",
+ "\xc4\x94" => "\xc4\x95",
+ "\xc4\x96" => "\xc4\x97",
+ "\xc4\x98" => "\xc4\x99",
+ "\xc4\x9a" => "\xc4\x9b",
+ "\xc4\x9c" => "\xc4\x9d",
+ "\xc4\x9e" => "\xc4\x9f",
+ "\xc4\xa0" => "\xc4\xa1",
+ "\xc4\xa2" => "\xc4\xa3",
+ "\xc4\xa4" => "\xc4\xa5",
+ "\xc4\xa6" => "\xc4\xa7",
+ "\xc4\xa8" => "\xc4\xa9",
+ "\xc4\xaa" => "\xc4\xab",
+ "\xc4\xac" => "\xc4\xad",
+ "\xc4\xae" => "\xc4\xaf",
+ "\xc4\xb0" => "i",
+ "\xc4\xb2" => "\xc4\xb3",
+ "\xc4\xb4" => "\xc4\xb5",
+ "\xc4\xb6" => "\xc4\xb7",
+ "\xc4\xb9" => "\xc4\xba",
+ "\xc4\xbb" => "\xc4\xbc",
+ "\xc4\xbd" => "\xc4\xbe",
+ "\xc4\xbf" => "\xc5\x80",
+ "\xc5\x81" => "\xc5\x82",
+ "\xc5\x83" => "\xc5\x84",
+ "\xc5\x85" => "\xc5\x86",
+ "\xc5\x87" => "\xc5\x88",
+ "\xc5\x8a" => "\xc5\x8b",
+ "\xc5\x8c" => "\xc5\x8d",
+ "\xc5\x8e" => "\xc5\x8f",
+ "\xc5\x90" => "\xc5\x91",
+ "\xc5\x92" => "\xc5\x93",
+ "\xc5\x94" => "\xc5\x95",
+ "\xc5\x96" => "\xc5\x97",
+ "\xc5\x98" => "\xc5\x99",
+ "\xc5\x9a" => "\xc5\x9b",
+ "\xc5\x9c" => "\xc5\x9d",
+ "\xc5\x9e" => "\xc5\x9f",
+ "\xc5\xa0" => "\xc5\xa1",
+ "\xc5\xa2" => "\xc5\xa3",
+ "\xc5\xa4" => "\xc5\xa5",
+ "\xc5\xa6" => "\xc5\xa7",
+ "\xc5\xa8" => "\xc5\xa9",
+ "\xc5\xaa" => "\xc5\xab",
+ "\xc5\xac" => "\xc5\xad",
+ "\xc5\xae" => "\xc5\xaf",
+ "\xc5\xb0" => "\xc5\xb1",
+ "\xc5\xb2" => "\xc5\xb3",
+ "\xc5\xb4" => "\xc5\xb5",
+ "\xc5\xb6" => "\xc5\xb7",
+ "\xc5\xb8" => "\xc3\xbf",
+ "\xc5\xb9" => "\xc5\xba",
+ "\xc5\xbb" => "\xc5\xbc",
+ "\xc5\xbd" => "\xc5\xbe",
+ "\xc6\x81" => "\xc9\x93",
+ "\xc6\x82" => "\xc6\x83",
+ "\xc6\x84" => "\xc6\x85",
+ "\xc6\x86" => "\xc9\x94",
+ "\xc6\x87" => "\xc6\x88",
+ "\xc6\x89" => "\xc9\x96",
+ "\xc6\x8a" => "\xc9\x97",
+ "\xc6\x8b" => "\xc6\x8c",
+ "\xc6\x8e" => "\xc7\x9d",
+ "\xc6\x8f" => "\xc9\x99",
+ "\xc6\x90" => "\xc9\x9b",
+ "\xc6\x91" => "\xc6\x92",
+ "\xc6\x93" => "\xc9\xa0",
+ "\xc6\x94" => "\xc9\xa3",
+ "\xc6\x96" => "\xc9\xa9",
+ "\xc6\x97" => "\xc9\xa8",
+ "\xc6\x98" => "\xc6\x99",
+ "\xc6\x9c" => "\xc9\xaf",
+ "\xc6\x9d" => "\xc9\xb2",
+ "\xc6\x9f" => "\xc9\xb5",
+ "\xc6\xa0" => "\xc6\xa1",
+ "\xc6\xa2" => "\xc6\xa3",
+ "\xc6\xa4" => "\xc6\xa5",
+ "\xc6\xa6" => "\xca\x80",
+ "\xc6\xa7" => "\xc6\xa8",
+ "\xc6\xa9" => "\xca\x83",
+ "\xc6\xac" => "\xc6\xad",
+ "\xc6\xae" => "\xca\x88",
+ "\xc6\xaf" => "\xc6\xb0",
+ "\xc6\xb1" => "\xca\x8a",
+ "\xc6\xb2" => "\xca\x8b",
+ "\xc6\xb3" => "\xc6\xb4",
+ "\xc6\xb5" => "\xc6\xb6",
+ "\xc6\xb7" => "\xca\x92",
+ "\xc6\xb8" => "\xc6\xb9",
+ "\xc6\xbc" => "\xc6\xbd",
+ "\xc7\x84" => "\xc7\x86",
+ "\xc7\x85" => "\xc7\x86",
+ "\xc7\x87" => "\xc7\x89",
+ "\xc7\x88" => "\xc7\x89",
+ "\xc7\x8a" => "\xc7\x8c",
+ "\xc7\x8b" => "\xc7\x8c",
+ "\xc7\x8d" => "\xc7\x8e",
+ "\xc7\x8f" => "\xc7\x90",
+ "\xc7\x91" => "\xc7\x92",
+ "\xc7\x93" => "\xc7\x94",
+ "\xc7\x95" => "\xc7\x96",
+ "\xc7\x97" => "\xc7\x98",
+ "\xc7\x99" => "\xc7\x9a",
+ "\xc7\x9b" => "\xc7\x9c",
+ "\xc7\x9e" => "\xc7\x9f",
+ "\xc7\xa0" => "\xc7\xa1",
+ "\xc7\xa2" => "\xc7\xa3",
+ "\xc7\xa4" => "\xc7\xa5",
+ "\xc7\xa6" => "\xc7\xa7",
+ "\xc7\xa8" => "\xc7\xa9",
+ "\xc7\xaa" => "\xc7\xab",
+ "\xc7\xac" => "\xc7\xad",
+ "\xc7\xae" => "\xc7\xaf",
+ "\xc7\xb1" => "\xc7\xb3",
+ "\xc7\xb2" => "\xc7\xb3",
+ "\xc7\xb4" => "\xc7\xb5",
+ "\xc7\xb6" => "\xc6\x95",
+ "\xc7\xb7" => "\xc6\xbf",
+ "\xc7\xb8" => "\xc7\xb9",
+ "\xc7\xba" => "\xc7\xbb",
+ "\xc7\xbc" => "\xc7\xbd",
+ "\xc7\xbe" => "\xc7\xbf",
+ "\xc8\x80" => "\xc8\x81",
+ "\xc8\x82" => "\xc8\x83",
+ "\xc8\x84" => "\xc8\x85",
+ "\xc8\x86" => "\xc8\x87",
+ "\xc8\x88" => "\xc8\x89",
+ "\xc8\x8a" => "\xc8\x8b",
+ "\xc8\x8c" => "\xc8\x8d",
+ "\xc8\x8e" => "\xc8\x8f",
+ "\xc8\x90" => "\xc8\x91",
+ "\xc8\x92" => "\xc8\x93",
+ "\xc8\x94" => "\xc8\x95",
+ "\xc8\x96" => "\xc8\x97",
+ "\xc8\x98" => "\xc8\x99",
+ "\xc8\x9a" => "\xc8\x9b",
+ "\xc8\x9c" => "\xc8\x9d",
+ "\xc8\x9e" => "\xc8\x9f",
+ "\xc8\xa2" => "\xc8\xa3",
+ "\xc8\xa4" => "\xc8\xa5",
+ "\xc8\xa6" => "\xc8\xa7",
+ "\xc8\xa8" => "\xc8\xa9",
+ "\xc8\xaa" => "\xc8\xab",
+ "\xc8\xac" => "\xc8\xad",
+ "\xc8\xae" => "\xc8\xaf",
+ "\xc8\xb0" => "\xc8\xb1",
+ "\xc8\xb2" => "\xc8\xb3",
+ "\xce\x86" => "\xce\xac",
+ "\xce\x88" => "\xce\xad",
+ "\xce\x89" => "\xce\xae",
+ "\xce\x8a" => "\xce\xaf",
+ "\xce\x8c" => "\xcf\x8c",
+ "\xce\x8e" => "\xcf\x8d",
+ "\xce\x8f" => "\xcf\x8e",
+ "\xce\x91" => "\xce\xb1",
+ "\xce\x92" => "\xce\xb2",
+ "\xce\x93" => "\xce\xb3",
+ "\xce\x94" => "\xce\xb4",
+ "\xce\x95" => "\xce\xb5",
+ "\xce\x96" => "\xce\xb6",
+ "\xce\x97" => "\xce\xb7",
+ "\xce\x98" => "\xce\xb8",
+ "\xce\x99" => "\xce\xb9",
+ "\xce\x9a" => "\xce\xba",
+ "\xce\x9b" => "\xce\xbb",
+ "\xce\x9c" => "\xce\xbc",
+ "\xce\x9d" => "\xce\xbd",
+ "\xce\x9e" => "\xce\xbe",
+ "\xce\x9f" => "\xce\xbf",
+ "\xce\xa0" => "\xcf\x80",
+ "\xce\xa1" => "\xcf\x81",
+ "\xce\xa3" => "\xcf\x83",
+ "\xce\xa4" => "\xcf\x84",
+ "\xce\xa5" => "\xcf\x85",
+ "\xce\xa6" => "\xcf\x86",
+ "\xce\xa7" => "\xcf\x87",
+ "\xce\xa8" => "\xcf\x88",
+ "\xce\xa9" => "\xcf\x89",
+ "\xce\xaa" => "\xcf\x8a",
+ "\xce\xab" => "\xcf\x8b",
+ "\xcf\x9a" => "\xcf\x9b",
+ "\xcf\x9c" => "\xcf\x9d",
+ "\xcf\x9e" => "\xcf\x9f",
+ "\xcf\xa0" => "\xcf\xa1",
+ "\xcf\xa2" => "\xcf\xa3",
+ "\xcf\xa4" => "\xcf\xa5",
+ "\xcf\xa6" => "\xcf\xa7",
+ "\xcf\xa8" => "\xcf\xa9",
+ "\xcf\xaa" => "\xcf\xab",
+ "\xcf\xac" => "\xcf\xad",
+ "\xcf\xae" => "\xcf\xaf",
+ "\xcf\xb4" => "\xce\xb8",
+ "\xd0\x80" => "\xd1\x90",
+ "\xd0\x81" => "\xd1\x91",
+ "\xd0\x82" => "\xd1\x92",
+ "\xd0\x83" => "\xd1\x93",
+ "\xd0\x84" => "\xd1\x94",
+ "\xd0\x85" => "\xd1\x95",
+ "\xd0\x86" => "\xd1\x96",
+ "\xd0\x87" => "\xd1\x97",
+ "\xd0\x88" => "\xd1\x98",
+ "\xd0\x89" => "\xd1\x99",
+ "\xd0\x8a" => "\xd1\x9a",
+ "\xd0\x8b" => "\xd1\x9b",
+ "\xd0\x8c" => "\xd1\x9c",
+ "\xd0\x8d" => "\xd1\x9d",
+ "\xd0\x8e" => "\xd1\x9e",
+ "\xd0\x8f" => "\xd1\x9f",
+ "\xd0\x90" => "\xd0\xb0",
+ "\xd0\x91" => "\xd0\xb1",
+ "\xd0\x92" => "\xd0\xb2",
+ "\xd0\x93" => "\xd0\xb3",
+ "\xd0\x94" => "\xd0\xb4",
+ "\xd0\x95" => "\xd0\xb5",
+ "\xd0\x96" => "\xd0\xb6",
+ "\xd0\x97" => "\xd0\xb7",
+ "\xd0\x98" => "\xd0\xb8",
+ "\xd0\x99" => "\xd0\xb9",
+ "\xd0\x9a" => "\xd0\xba",
+ "\xd0\x9b" => "\xd0\xbb",
+ "\xd0\x9c" => "\xd0\xbc",
+ "\xd0\x9d" => "\xd0\xbd",
+ "\xd0\x9e" => "\xd0\xbe",
+ "\xd0\x9f" => "\xd0\xbf",
+ "\xd0\xa0" => "\xd1\x80",
+ "\xd0\xa1" => "\xd1\x81",
+ "\xd0\xa2" => "\xd1\x82",
+ "\xd0\xa3" => "\xd1\x83",
+ "\xd0\xa4" => "\xd1\x84",
+ "\xd0\xa5" => "\xd1\x85",
+ "\xd0\xa6" => "\xd1\x86",
+ "\xd0\xa7" => "\xd1\x87",
+ "\xd0\xa8" => "\xd1\x88",
+ "\xd0\xa9" => "\xd1\x89",
+ "\xd0\xaa" => "\xd1\x8a",
+ "\xd0\xab" => "\xd1\x8b",
+ "\xd0\xac" => "\xd1\x8c",
+ "\xd0\xad" => "\xd1\x8d",
+ "\xd0\xae" => "\xd1\x8e",
+ "\xd0\xaf" => "\xd1\x8f",
+ "\xd1\xa0" => "\xd1\xa1",
+ "\xd1\xa2" => "\xd1\xa3",
+ "\xd1\xa4" => "\xd1\xa5",
+ "\xd1\xa6" => "\xd1\xa7",
+ "\xd1\xa8" => "\xd1\xa9",
+ "\xd1\xaa" => "\xd1\xab",
+ "\xd1\xac" => "\xd1\xad",
+ "\xd1\xae" => "\xd1\xaf",
+ "\xd1\xb0" => "\xd1\xb1",
+ "\xd1\xb2" => "\xd1\xb3",
+ "\xd1\xb4" => "\xd1\xb5",
+ "\xd1\xb6" => "\xd1\xb7",
+ "\xd1\xb8" => "\xd1\xb9",
+ "\xd1\xba" => "\xd1\xbb",
+ "\xd1\xbc" => "\xd1\xbd",
+ "\xd1\xbe" => "\xd1\xbf",
+ "\xd2\x80" => "\xd2\x81",
+ "\xd2\x8c" => "\xd2\x8d",
+ "\xd2\x8e" => "\xd2\x8f",
+ "\xd2\x90" => "\xd2\x91",
+ "\xd2\x92" => "\xd2\x93",
+ "\xd2\x94" => "\xd2\x95",
+ "\xd2\x96" => "\xd2\x97",
+ "\xd2\x98" => "\xd2\x99",
+ "\xd2\x9a" => "\xd2\x9b",
+ "\xd2\x9c" => "\xd2\x9d",
+ "\xd2\x9e" => "\xd2\x9f",
+ "\xd2\xa0" => "\xd2\xa1",
+ "\xd2\xa2" => "\xd2\xa3",
+ "\xd2\xa4" => "\xd2\xa5",
+ "\xd2\xa6" => "\xd2\xa7",
+ "\xd2\xa8" => "\xd2\xa9",
+ "\xd2\xaa" => "\xd2\xab",
+ "\xd2\xac" => "\xd2\xad",
+ "\xd2\xae" => "\xd2\xaf",
+ "\xd2\xb0" => "\xd2\xb1",
+ "\xd2\xb2" => "\xd2\xb3",
+ "\xd2\xb4" => "\xd2\xb5",
+ "\xd2\xb6" => "\xd2\xb7",
+ "\xd2\xb8" => "\xd2\xb9",
+ "\xd2\xba" => "\xd2\xbb",
+ "\xd2\xbc" => "\xd2\xbd",
+ "\xd2\xbe" => "\xd2\xbf",
+ "\xd3\x81" => "\xd3\x82",
+ "\xd3\x83" => "\xd3\x84",
+ "\xd3\x87" => "\xd3\x88",
+ "\xd3\x8b" => "\xd3\x8c",
+ "\xd3\x90" => "\xd3\x91",
+ "\xd3\x92" => "\xd3\x93",
+ "\xd3\x94" => "\xd3\x95",
+ "\xd3\x96" => "\xd3\x97",
+ "\xd3\x98" => "\xd3\x99",
+ "\xd3\x9a" => "\xd3\x9b",
+ "\xd3\x9c" => "\xd3\x9d",
+ "\xd3\x9e" => "\xd3\x9f",
+ "\xd3\xa0" => "\xd3\xa1",
+ "\xd3\xa2" => "\xd3\xa3",
+ "\xd3\xa4" => "\xd3\xa5",
+ "\xd3\xa6" => "\xd3\xa7",
+ "\xd3\xa8" => "\xd3\xa9",
+ "\xd3\xaa" => "\xd3\xab",
+ "\xd3\xac" => "\xd3\xad",
+ "\xd3\xae" => "\xd3\xaf",
+ "\xd3\xb0" => "\xd3\xb1",
+ "\xd3\xb2" => "\xd3\xb3",
+ "\xd3\xb4" => "\xd3\xb5",
+ "\xd3\xb8" => "\xd3\xb9",
+ "\xd4\xb1" => "\xd5\xa1",
+ "\xd4\xb2" => "\xd5\xa2",
+ "\xd4\xb3" => "\xd5\xa3",
+ "\xd4\xb4" => "\xd5\xa4",
+ "\xd4\xb5" => "\xd5\xa5",
+ "\xd4\xb6" => "\xd5\xa6",
+ "\xd4\xb7" => "\xd5\xa7",
+ "\xd4\xb8" => "\xd5\xa8",
+ "\xd4\xb9" => "\xd5\xa9",
+ "\xd4\xba" => "\xd5\xaa",
+ "\xd4\xbb" => "\xd5\xab",
+ "\xd4\xbc" => "\xd5\xac",
+ "\xd4\xbd" => "\xd5\xad",
+ "\xd4\xbe" => "\xd5\xae",
+ "\xd4\xbf" => "\xd5\xaf",
+ "\xd5\x80" => "\xd5\xb0",
+ "\xd5\x81" => "\xd5\xb1",
+ "\xd5\x82" => "\xd5\xb2",
+ "\xd5\x83" => "\xd5\xb3",
+ "\xd5\x84" => "\xd5\xb4",
+ "\xd5\x85" => "\xd5\xb5",
+ "\xd5\x86" => "\xd5\xb6",
+ "\xd5\x87" => "\xd5\xb7",
+ "\xd5\x88" => "\xd5\xb8",
+ "\xd5\x89" => "\xd5\xb9",
+ "\xd5\x8a" => "\xd5\xba",
+ "\xd5\x8b" => "\xd5\xbb",
+ "\xd5\x8c" => "\xd5\xbc",
+ "\xd5\x8d" => "\xd5\xbd",
+ "\xd5\x8e" => "\xd5\xbe",
+ "\xd5\x8f" => "\xd5\xbf",
+ "\xd5\x90" => "\xd6\x80",
+ "\xd5\x91" => "\xd6\x81",
+ "\xd5\x92" => "\xd6\x82",
+ "\xd5\x93" => "\xd6\x83",
+ "\xd5\x94" => "\xd6\x84",
+ "\xd5\x95" => "\xd6\x85",
+ "\xd5\x96" => "\xd6\x86",
+ "\xe1\xb8\x80" => "\xe1\xb8\x81",
+ "\xe1\xb8\x82" => "\xe1\xb8\x83",
+ "\xe1\xb8\x84" => "\xe1\xb8\x85",
+ "\xe1\xb8\x86" => "\xe1\xb8\x87",
+ "\xe1\xb8\x88" => "\xe1\xb8\x89",
+ "\xe1\xb8\x8a" => "\xe1\xb8\x8b",
+ "\xe1\xb8\x8c" => "\xe1\xb8\x8d",
+ "\xe1\xb8\x8e" => "\xe1\xb8\x8f",
+ "\xe1\xb8\x90" => "\xe1\xb8\x91",
+ "\xe1\xb8\x92" => "\xe1\xb8\x93",
+ "\xe1\xb8\x94" => "\xe1\xb8\x95",
+ "\xe1\xb8\x96" => "\xe1\xb8\x97",
+ "\xe1\xb8\x98" => "\xe1\xb8\x99",
+ "\xe1\xb8\x9a" => "\xe1\xb8\x9b",
+ "\xe1\xb8\x9c" => "\xe1\xb8\x9d",
+ "\xe1\xb8\x9e" => "\xe1\xb8\x9f",
+ "\xe1\xb8\xa0" => "\xe1\xb8\xa1",
+ "\xe1\xb8\xa2" => "\xe1\xb8\xa3",
+ "\xe1\xb8\xa4" => "\xe1\xb8\xa5",
+ "\xe1\xb8\xa6" => "\xe1\xb8\xa7",
+ "\xe1\xb8\xa8" => "\xe1\xb8\xa9",
+ "\xe1\xb8\xaa" => "\xe1\xb8\xab",
+ "\xe1\xb8\xac" => "\xe1\xb8\xad",
+ "\xe1\xb8\xae" => "\xe1\xb8\xaf",
+ "\xe1\xb8\xb0" => "\xe1\xb8\xb1",
+ "\xe1\xb8\xb2" => "\xe1\xb8\xb3",
+ "\xe1\xb8\xb4" => "\xe1\xb8\xb5",
+ "\xe1\xb8\xb6" => "\xe1\xb8\xb7",
+ "\xe1\xb8\xb8" => "\xe1\xb8\xb9",
+ "\xe1\xb8\xba" => "\xe1\xb8\xbb",
+ "\xe1\xb8\xbc" => "\xe1\xb8\xbd",
+ "\xe1\xb8\xbe" => "\xe1\xb8\xbf",
+ "\xe1\xb9\x80" => "\xe1\xb9\x81",
+ "\xe1\xb9\x82" => "\xe1\xb9\x83",
+ "\xe1\xb9\x84" => "\xe1\xb9\x85",
+ "\xe1\xb9\x86" => "\xe1\xb9\x87",
+ "\xe1\xb9\x88" => "\xe1\xb9\x89",
+ "\xe1\xb9\x8a" => "\xe1\xb9\x8b",
+ "\xe1\xb9\x8c" => "\xe1\xb9\x8d",
+ "\xe1\xb9\x8e" => "\xe1\xb9\x8f",
+ "\xe1\xb9\x90" => "\xe1\xb9\x91",
+ "\xe1\xb9\x92" => "\xe1\xb9\x93",
+ "\xe1\xb9\x94" => "\xe1\xb9\x95",
+ "\xe1\xb9\x96" => "\xe1\xb9\x97",
+ "\xe1\xb9\x98" => "\xe1\xb9\x99",
+ "\xe1\xb9\x9a" => "\xe1\xb9\x9b",
+ "\xe1\xb9\x9c" => "\xe1\xb9\x9d",
+ "\xe1\xb9\x9e" => "\xe1\xb9\x9f",
+ "\xe1\xb9\xa0" => "\xe1\xb9\xa1",
+ "\xe1\xb9\xa2" => "\xe1\xb9\xa3",
+ "\xe1\xb9\xa4" => "\xe1\xb9\xa5",
+ "\xe1\xb9\xa6" => "\xe1\xb9\xa7",
+ "\xe1\xb9\xa8" => "\xe1\xb9\xa9",
+ "\xe1\xb9\xaa" => "\xe1\xb9\xab",
+ "\xe1\xb9\xac" => "\xe1\xb9\xad",
+ "\xe1\xb9\xae" => "\xe1\xb9\xaf",
+ "\xe1\xb9\xb0" => "\xe1\xb9\xb1",
+ "\xe1\xb9\xb2" => "\xe1\xb9\xb3",
+ "\xe1\xb9\xb4" => "\xe1\xb9\xb5",
+ "\xe1\xb9\xb6" => "\xe1\xb9\xb7",
+ "\xe1\xb9\xb8" => "\xe1\xb9\xb9",
+ "\xe1\xb9\xba" => "\xe1\xb9\xbb",
+ "\xe1\xb9\xbc" => "\xe1\xb9\xbd",
+ "\xe1\xb9\xbe" => "\xe1\xb9\xbf",
+ "\xe1\xba\x80" => "\xe1\xba\x81",
+ "\xe1\xba\x82" => "\xe1\xba\x83",
+ "\xe1\xba\x84" => "\xe1\xba\x85",
+ "\xe1\xba\x86" => "\xe1\xba\x87",
+ "\xe1\xba\x88" => "\xe1\xba\x89",
+ "\xe1\xba\x8a" => "\xe1\xba\x8b",
+ "\xe1\xba\x8c" => "\xe1\xba\x8d",
+ "\xe1\xba\x8e" => "\xe1\xba\x8f",
+ "\xe1\xba\x90" => "\xe1\xba\x91",
+ "\xe1\xba\x92" => "\xe1\xba\x93",
+ "\xe1\xba\x94" => "\xe1\xba\x95",
+ "\xe1\xba\xa0" => "\xe1\xba\xa1",
+ "\xe1\xba\xa2" => "\xe1\xba\xa3",
+ "\xe1\xba\xa4" => "\xe1\xba\xa5",
+ "\xe1\xba\xa6" => "\xe1\xba\xa7",
+ "\xe1\xba\xa8" => "\xe1\xba\xa9",
+ "\xe1\xba\xaa" => "\xe1\xba\xab",
+ "\xe1\xba\xac" => "\xe1\xba\xad",
+ "\xe1\xba\xae" => "\xe1\xba\xaf",
+ "\xe1\xba\xb0" => "\xe1\xba\xb1",
+ "\xe1\xba\xb2" => "\xe1\xba\xb3",
+ "\xe1\xba\xb4" => "\xe1\xba\xb5",
+ "\xe1\xba\xb6" => "\xe1\xba\xb7",
+ "\xe1\xba\xb8" => "\xe1\xba\xb9",
+ "\xe1\xba\xba" => "\xe1\xba\xbb",
+ "\xe1\xba\xbc" => "\xe1\xba\xbd",
+ "\xe1\xba\xbe" => "\xe1\xba\xbf",
+ "\xe1\xbb\x80" => "\xe1\xbb\x81",
+ "\xe1\xbb\x82" => "\xe1\xbb\x83",
+ "\xe1\xbb\x84" => "\xe1\xbb\x85",
+ "\xe1\xbb\x86" => "\xe1\xbb\x87",
+ "\xe1\xbb\x88" => "\xe1\xbb\x89",
+ "\xe1\xbb\x8a" => "\xe1\xbb\x8b",
+ "\xe1\xbb\x8c" => "\xe1\xbb\x8d",
+ "\xe1\xbb\x8e" => "\xe1\xbb\x8f",
+ "\xe1\xbb\x90" => "\xe1\xbb\x91",
+ "\xe1\xbb\x92" => "\xe1\xbb\x93",
+ "\xe1\xbb\x94" => "\xe1\xbb\x95",
+ "\xe1\xbb\x96" => "\xe1\xbb\x97",
+ "\xe1\xbb\x98" => "\xe1\xbb\x99",
+ "\xe1\xbb\x9a" => "\xe1\xbb\x9b",
+ "\xe1\xbb\x9c" => "\xe1\xbb\x9d",
+ "\xe1\xbb\x9e" => "\xe1\xbb\x9f",
+ "\xe1\xbb\xa0" => "\xe1\xbb\xa1",
+ "\xe1\xbb\xa2" => "\xe1\xbb\xa3",
+ "\xe1\xbb\xa4" => "\xe1\xbb\xa5",
+ "\xe1\xbb\xa6" => "\xe1\xbb\xa7",
+ "\xe1\xbb\xa8" => "\xe1\xbb\xa9",
+ "\xe1\xbb\xaa" => "\xe1\xbb\xab",
+ "\xe1\xbb\xac" => "\xe1\xbb\xad",
+ "\xe1\xbb\xae" => "\xe1\xbb\xaf",
+ "\xe1\xbb\xb0" => "\xe1\xbb\xb1",
+ "\xe1\xbb\xb2" => "\xe1\xbb\xb3",
+ "\xe1\xbb\xb4" => "\xe1\xbb\xb5",
+ "\xe1\xbb\xb6" => "\xe1\xbb\xb7",
+ "\xe1\xbb\xb8" => "\xe1\xbb\xb9",
+ "\xe1\xbc\x88" => "\xe1\xbc\x80",
+ "\xe1\xbc\x89" => "\xe1\xbc\x81",
+ "\xe1\xbc\x8a" => "\xe1\xbc\x82",
+ "\xe1\xbc\x8b" => "\xe1\xbc\x83",
+ "\xe1\xbc\x8c" => "\xe1\xbc\x84",
+ "\xe1\xbc\x8d" => "\xe1\xbc\x85",
+ "\xe1\xbc\x8e" => "\xe1\xbc\x86",
+ "\xe1\xbc\x8f" => "\xe1\xbc\x87",
+ "\xe1\xbc\x98" => "\xe1\xbc\x90",
+ "\xe1\xbc\x99" => "\xe1\xbc\x91",
+ "\xe1\xbc\x9a" => "\xe1\xbc\x92",
+ "\xe1\xbc\x9b" => "\xe1\xbc\x93",
+ "\xe1\xbc\x9c" => "\xe1\xbc\x94",
+ "\xe1\xbc\x9d" => "\xe1\xbc\x95",
+ "\xe1\xbc\xa8" => "\xe1\xbc\xa0",
+ "\xe1\xbc\xa9" => "\xe1\xbc\xa1",
+ "\xe1\xbc\xaa" => "\xe1\xbc\xa2",
+ "\xe1\xbc\xab" => "\xe1\xbc\xa3",
+ "\xe1\xbc\xac" => "\xe1\xbc\xa4",
+ "\xe1\xbc\xad" => "\xe1\xbc\xa5",
+ "\xe1\xbc\xae" => "\xe1\xbc\xa6",
+ "\xe1\xbc\xaf" => "\xe1\xbc\xa7",
+ "\xe1\xbc\xb8" => "\xe1\xbc\xb0",
+ "\xe1\xbc\xb9" => "\xe1\xbc\xb1",
+ "\xe1\xbc\xba" => "\xe1\xbc\xb2",
+ "\xe1\xbc\xbb" => "\xe1\xbc\xb3",
+ "\xe1\xbc\xbc" => "\xe1\xbc\xb4",
+ "\xe1\xbc\xbd" => "\xe1\xbc\xb5",
+ "\xe1\xbc\xbe" => "\xe1\xbc\xb6",
+ "\xe1\xbc\xbf" => "\xe1\xbc\xb7",
+ "\xe1\xbd\x88" => "\xe1\xbd\x80",
+ "\xe1\xbd\x89" => "\xe1\xbd\x81",
+ "\xe1\xbd\x8a" => "\xe1\xbd\x82",
+ "\xe1\xbd\x8b" => "\xe1\xbd\x83",
+ "\xe1\xbd\x8c" => "\xe1\xbd\x84",
+ "\xe1\xbd\x8d" => "\xe1\xbd\x85",
+ "\xe1\xbd\x99" => "\xe1\xbd\x91",
+ "\xe1\xbd\x9b" => "\xe1\xbd\x93",
+ "\xe1\xbd\x9d" => "\xe1\xbd\x95",
+ "\xe1\xbd\x9f" => "\xe1\xbd\x97",
+ "\xe1\xbd\xa8" => "\xe1\xbd\xa0",
+ "\xe1\xbd\xa9" => "\xe1\xbd\xa1",
+ "\xe1\xbd\xaa" => "\xe1\xbd\xa2",
+ "\xe1\xbd\xab" => "\xe1\xbd\xa3",
+ "\xe1\xbd\xac" => "\xe1\xbd\xa4",
+ "\xe1\xbd\xad" => "\xe1\xbd\xa5",
+ "\xe1\xbd\xae" => "\xe1\xbd\xa6",
+ "\xe1\xbd\xaf" => "\xe1\xbd\xa7",
+ "\xe1\xbe\x88" => "\xe1\xbe\x80",
+ "\xe1\xbe\x89" => "\xe1\xbe\x81",
+ "\xe1\xbe\x8a" => "\xe1\xbe\x82",
+ "\xe1\xbe\x8b" => "\xe1\xbe\x83",
+ "\xe1\xbe\x8c" => "\xe1\xbe\x84",
+ "\xe1\xbe\x8d" => "\xe1\xbe\x85",
+ "\xe1\xbe\x8e" => "\xe1\xbe\x86",
+ "\xe1\xbe\x8f" => "\xe1\xbe\x87",
+ "\xe1\xbe\x98" => "\xe1\xbe\x90",
+ "\xe1\xbe\x99" => "\xe1\xbe\x91",
+ "\xe1\xbe\x9a" => "\xe1\xbe\x92",
+ "\xe1\xbe\x9b" => "\xe1\xbe\x93",
+ "\xe1\xbe\x9c" => "\xe1\xbe\x94",
+ "\xe1\xbe\x9d" => "\xe1\xbe\x95",
+ "\xe1\xbe\x9e" => "\xe1\xbe\x96",
+ "\xe1\xbe\x9f" => "\xe1\xbe\x97",
+ "\xe1\xbe\xa8" => "\xe1\xbe\xa0",
+ "\xe1\xbe\xa9" => "\xe1\xbe\xa1",
+ "\xe1\xbe\xaa" => "\xe1\xbe\xa2",
+ "\xe1\xbe\xab" => "\xe1\xbe\xa3",
+ "\xe1\xbe\xac" => "\xe1\xbe\xa4",
+ "\xe1\xbe\xad" => "\xe1\xbe\xa5",
+ "\xe1\xbe\xae" => "\xe1\xbe\xa6",
+ "\xe1\xbe\xaf" => "\xe1\xbe\xa7",
+ "\xe1\xbe\xb8" => "\xe1\xbe\xb0",
+ "\xe1\xbe\xb9" => "\xe1\xbe\xb1",
+ "\xe1\xbe\xba" => "\xe1\xbd\xb0",
+ "\xe1\xbe\xbb" => "\xe1\xbd\xb1",
+ "\xe1\xbe\xbc" => "\xe1\xbe\xb3",
+ "\xe1\xbf\x88" => "\xe1\xbd\xb2",
+ "\xe1\xbf\x89" => "\xe1\xbd\xb3",
+ "\xe1\xbf\x8a" => "\xe1\xbd\xb4",
+ "\xe1\xbf\x8b" => "\xe1\xbd\xb5",
+ "\xe1\xbf\x8c" => "\xe1\xbf\x83",
+ "\xe1\xbf\x98" => "\xe1\xbf\x90",
+ "\xe1\xbf\x99" => "\xe1\xbf\x91",
+ "\xe1\xbf\x9a" => "\xe1\xbd\xb6",
+ "\xe1\xbf\x9b" => "\xe1\xbd\xb7",
+ "\xe1\xbf\xa8" => "\xe1\xbf\xa0",
+ "\xe1\xbf\xa9" => "\xe1\xbf\xa1",
+ "\xe1\xbf\xaa" => "\xe1\xbd\xba",
+ "\xe1\xbf\xab" => "\xe1\xbd\xbb",
+ "\xe1\xbf\xac" => "\xe1\xbf\xa5",
+ "\xe1\xbf\xb8" => "\xe1\xbd\xb8",
+ "\xe1\xbf\xb9" => "\xe1\xbd\xb9",
+ "\xe1\xbf\xba" => "\xe1\xbd\xbc",
+ "\xe1\xbf\xbb" => "\xe1\xbd\xbd",
+ "\xe1\xbf\xbc" => "\xe1\xbf\xb3",
+ "\xe2\x84\xa6" => "\xcf\x89",
+ "\xe2\x84\xaa" => "k",
+ "\xe2\x84\xab" => "\xc3\xa5",
+ "\xe2\x85\xa0" => "\xe2\x85\xb0",
+ "\xe2\x85\xa1" => "\xe2\x85\xb1",
+ "\xe2\x85\xa2" => "\xe2\x85\xb2",
+ "\xe2\x85\xa3" => "\xe2\x85\xb3",
+ "\xe2\x85\xa4" => "\xe2\x85\xb4",
+ "\xe2\x85\xa5" => "\xe2\x85\xb5",
+ "\xe2\x85\xa6" => "\xe2\x85\xb6",
+ "\xe2\x85\xa7" => "\xe2\x85\xb7",
+ "\xe2\x85\xa8" => "\xe2\x85\xb8",
+ "\xe2\x85\xa9" => "\xe2\x85\xb9",
+ "\xe2\x85\xaa" => "\xe2\x85\xba",
+ "\xe2\x85\xab" => "\xe2\x85\xbb",
+ "\xe2\x85\xac" => "\xe2\x85\xbc",
+ "\xe2\x85\xad" => "\xe2\x85\xbd",
+ "\xe2\x85\xae" => "\xe2\x85\xbe",
+ "\xe2\x85\xaf" => "\xe2\x85\xbf",
+ "\xe2\x92\xb6" => "\xe2\x93\x90",
+ "\xe2\x92\xb7" => "\xe2\x93\x91",
+ "\xe2\x92\xb8" => "\xe2\x93\x92",
+ "\xe2\x92\xb9" => "\xe2\x93\x93",
+ "\xe2\x92\xba" => "\xe2\x93\x94",
+ "\xe2\x92\xbb" => "\xe2\x93\x95",
+ "\xe2\x92\xbc" => "\xe2\x93\x96",
+ "\xe2\x92\xbd" => "\xe2\x93\x97",
+ "\xe2\x92\xbe" => "\xe2\x93\x98",
+ "\xe2\x92\xbf" => "\xe2\x93\x99",
+ "\xe2\x93\x80" => "\xe2\x93\x9a",
+ "\xe2\x93\x81" => "\xe2\x93\x9b",
+ "\xe2\x93\x82" => "\xe2\x93\x9c",
+ "\xe2\x93\x83" => "\xe2\x93\x9d",
+ "\xe2\x93\x84" => "\xe2\x93\x9e",
+ "\xe2\x93\x85" => "\xe2\x93\x9f",
+ "\xe2\x93\x86" => "\xe2\x93\xa0",
+ "\xe2\x93\x87" => "\xe2\x93\xa1",
+ "\xe2\x93\x88" => "\xe2\x93\xa2",
+ "\xe2\x93\x89" => "\xe2\x93\xa3",
+ "\xe2\x93\x8a" => "\xe2\x93\xa4",
+ "\xe2\x93\x8b" => "\xe2\x93\xa5",
+ "\xe2\x93\x8c" => "\xe2\x93\xa6",
+ "\xe2\x93\x8d" => "\xe2\x93\xa7",
+ "\xe2\x93\x8e" => "\xe2\x93\xa8",
+ "\xe2\x93\x8f" => "\xe2\x93\xa9",
+ "\xef\xbc\xa1" => "\xef\xbd\x81",
+ "\xef\xbc\xa2" => "\xef\xbd\x82",
+ "\xef\xbc\xa3" => "\xef\xbd\x83",
+ "\xef\xbc\xa4" => "\xef\xbd\x84",
+ "\xef\xbc\xa5" => "\xef\xbd\x85",
+ "\xef\xbc\xa6" => "\xef\xbd\x86",
+ "\xef\xbc\xa7" => "\xef\xbd\x87",
+ "\xef\xbc\xa8" => "\xef\xbd\x88",
+ "\xef\xbc\xa9" => "\xef\xbd\x89",
+ "\xef\xbc\xaa" => "\xef\xbd\x8a",
+ "\xef\xbc\xab" => "\xef\xbd\x8b",
+ "\xef\xbc\xac" => "\xef\xbd\x8c",
+ "\xef\xbc\xad" => "\xef\xbd\x8d",
+ "\xef\xbc\xae" => "\xef\xbd\x8e",
+ "\xef\xbc\xaf" => "\xef\xbd\x8f",
+ "\xef\xbc\xb0" => "\xef\xbd\x90",
+ "\xef\xbc\xb1" => "\xef\xbd\x91",
+ "\xef\xbc\xb2" => "\xef\xbd\x92",
+ "\xef\xbc\xb3" => "\xef\xbd\x93",
+ "\xef\xbc\xb4" => "\xef\xbd\x94",
+ "\xef\xbc\xb5" => "\xef\xbd\x95",
+ "\xef\xbc\xb6" => "\xef\xbd\x96",
+ "\xef\xbc\xb7" => "\xef\xbd\x97",
+ "\xef\xbc\xb8" => "\xef\xbd\x98",
+ "\xef\xbc\xb9" => "\xef\xbd\x99",
+ "\xef\xbc\xba" => "\xef\xbd\x9a",
+ "\xf0\x90\x90\x80" => "\xf0\x90\x90\xa8",
+ "\xf0\x90\x90\x81" => "\xf0\x90\x90\xa9",
+ "\xf0\x90\x90\x82" => "\xf0\x90\x90\xaa",
+ "\xf0\x90\x90\x83" => "\xf0\x90\x90\xab",
+ "\xf0\x90\x90\x84" => "\xf0\x90\x90\xac",
+ "\xf0\x90\x90\x85" => "\xf0\x90\x90\xad",
+ "\xf0\x90\x90\x86" => "\xf0\x90\x90\xae",
+ "\xf0\x90\x90\x87" => "\xf0\x90\x90\xaf",
+ "\xf0\x90\x90\x88" => "\xf0\x90\x90\xb0",
+ "\xf0\x90\x90\x89" => "\xf0\x90\x90\xb1",
+ "\xf0\x90\x90\x8a" => "\xf0\x90\x90\xb2",
+ "\xf0\x90\x90\x8b" => "\xf0\x90\x90\xb3",
+ "\xf0\x90\x90\x8c" => "\xf0\x90\x90\xb4",
+ "\xf0\x90\x90\x8d" => "\xf0\x90\x90\xb5",
+ "\xf0\x90\x90\x8e" => "\xf0\x90\x90\xb6",
+ "\xf0\x90\x90\x8f" => "\xf0\x90\x90\xb7",
+ "\xf0\x90\x90\x90" => "\xf0\x90\x90\xb8",
+ "\xf0\x90\x90\x91" => "\xf0\x90\x90\xb9",
+ "\xf0\x90\x90\x92" => "\xf0\x90\x90\xba",
+ "\xf0\x90\x90\x93" => "\xf0\x90\x90\xbb",
+ "\xf0\x90\x90\x94" => "\xf0\x90\x90\xbc",
+ "\xf0\x90\x90\x95" => "\xf0\x90\x90\xbd",
+ "\xf0\x90\x90\x96" => "\xf0\x90\x90\xbe",
+ "\xf0\x90\x90\x97" => "\xf0\x90\x90\xbf",
+ "\xf0\x90\x90\x98" => "\xf0\x90\x91\x80",
+ "\xf0\x90\x90\x99" => "\xf0\x90\x91\x81",
+ "\xf0\x90\x90\x9a" => "\xf0\x90\x91\x82",
+ "\xf0\x90\x90\x9b" => "\xf0\x90\x91\x83",
+ "\xf0\x90\x90\x9c" => "\xf0\x90\x91\x84",
+ "\xf0\x90\x90\x9d" => "\xf0\x90\x91\x85",
+ "\xf0\x90\x90\x9e" => "\xf0\x90\x91\x86",
+ "\xf0\x90\x90\x9f" => "\xf0\x90\x91\x87",
+ "\xf0\x90\x90\xa0" => "\xf0\x90\x91\x88",
+ "\xf0\x90\x90\xa1" => "\xf0\x90\x91\x89",
+ "\xf0\x90\x90\xa2" => "\xf0\x90\x91\x8a",
+ "\xf0\x90\x90\xa3" => "\xf0\x90\x91\x8b",
+ "\xf0\x90\x90\xa4" => "\xf0\x90\x91\x8c",
+ "\xf0\x90\x90\xa5" => "\xf0\x90\x91\x8d"
+);
+
+?>
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
new file mode 100644
index 00000000..3885bb98
--- /dev/null
+++ b/includes/WatchedItem.php
@@ -0,0 +1,190 @@
+<?php
+/**
+ *
+ * @package MediaWiki
+ */
+
+/**
+ *
+ * @package MediaWiki
+ */
+class WatchedItem {
+ var $mTitle, $mUser;
+
+ /**
+ * Create a WatchedItem object with the given user and title
+ * @todo document
+ * @access private
+ */
+ function &fromUserTitle( &$user, &$title ) {
+ $wl = new WatchedItem;
+ $wl->mUser =& $user;
+ $wl->mTitle =& $title;
+ $wl->id = $user->getId();
+# Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004
+# TG patch: here we do not consider pages and their talk pages equivalent - why should we ?
+# The change results in talk-pages not automatically included in watchlists, when their parent page is included
+# $wl->ns = $title->getNamespace() & ~1;
+ $wl->ns = $title->getNamespace();
+
+ $wl->ti = $title->getDBkey();
+ return $wl;
+ }
+
+ /**
+ * Returns the memcached key for this item
+ */
+ function watchKey() {
+ global $wgDBname;
+ return "$wgDBname:watchlist:user:$this->id:page:$this->ns:$this->ti";
+ }
+
+ /**
+ * Is mTitle being watched by mUser?
+ */
+ function isWatched() {
+ # Pages and their talk pages are considered equivalent for watching;
+ # remember that talk namespaces are numbered as page namespace+1.
+ global $wgMemc;
+ $fname = 'WatchedItem::isWatched';
+
+ $key = $this->watchKey();
+ $iswatched = $wgMemc->get( $key );
+ if( is_integer( $iswatched ) ) return $iswatched;
+
+ $dbr =& wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'watchlist', 1, array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns,
+ 'wl_title' => $this->ti ), $fname );
+ $iswatched = ($dbr->numRows( $res ) > 0) ? 1 : 0;
+ $wgMemc->set( $key, $iswatched );
+ return $iswatched;
+ }
+
+ /**
+ * @todo document
+ */
+ function addWatch() {
+ $fname = 'WatchedItem::addWatch';
+ wfProfileIn( $fname );
+
+ // Use INSERT IGNORE to avoid overwriting the notification timestamp
+ // if there's already an entry for this page
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->insert( 'watchlist',
+ array(
+ 'wl_user' => $this->id,
+ 'wl_namespace' => ($this->ns & ~1),
+ 'wl_title' => $this->ti,
+ 'wl_notificationtimestamp' => NULL
+ ), $fname, 'IGNORE' );
+
+ // Every single watched page needs now to be listed in watchlist;
+ // namespace:page and namespace_talk:page need separate entries:
+ $dbw->insert( 'watchlist',
+ array(
+ 'wl_user' => $this->id,
+ 'wl_namespace' => ($this->ns | 1 ),
+ 'wl_title' => $this->ti,
+ 'wl_notificationtimestamp' => NULL
+ ), $fname, 'IGNORE' );
+
+ global $wgMemc;
+ $wgMemc->set( $this->watchkey(), 1 );
+ wfProfileOut( $fname );
+ return true;
+ }
+
+ function removeWatch() {
+ global $wgMemc;
+ $fname = 'WatchedItem::removeWatch';
+
+ $success = false;
+ $dbw =& wfGetDB( DB_MASTER );
+ $dbw->delete( 'watchlist',
+ array(
+ 'wl_user' => $this->id,
+ 'wl_namespace' => ($this->ns & ~1),
+ 'wl_title' => $this->ti
+ ), $fname
+ );
+ if ( $dbw->affectedRows() ) {
+ $success = true;
+ }
+
+ # the following code compensates the new behaviour, introduced by the
+ # enotif patch, that every single watched page needs now to be listed
+ # in watchlist namespace:page and namespace_talk:page had separate
+ # entries: clear them
+ $dbw->delete( 'watchlist',
+ array(
+ 'wl_user' => $this->id,
+ 'wl_namespace' => ($this->ns | 1),
+ 'wl_title' => $this->ti
+ ), $fname
+ );
+
+ if ( $dbw->affectedRows() ) {
+ $success = true;
+ }
+ if ( $success ) {
+ $wgMemc->set( $this->watchkey(), 0 );
+ }
+ return $success;
+ }
+
+ /**
+ * Check if the given title already is watched by the user, and if so
+ * add watches on a new title. To be used for page renames and such.
+ *
+ * @param Title $ot Page title to duplicate entries from, if present
+ * @param Title $nt Page title to add watches on
+ * @static
+ */
+ function duplicateEntries( $ot, $nt ) {
+ WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() );
+ WatchedItem::doDuplicateEntries( $ot->getTalkPage(), $nt->getTalkPage() );
+ }
+
+ /**
+ * @static
+ * @access private
+ */
+ function doDuplicateEntries( $ot, $nt ) {
+ $fname = "WatchedItem::duplicateEntries";
+ $oldnamespace = $ot->getNamespace();
+ $newnamespace = $nt->getNamespace();
+ $oldtitle = $ot->getDBkey();
+ $newtitle = $nt->getDBkey();
+
+ $dbw =& wfGetDB( DB_MASTER );
+ $res = $dbw->select( 'watchlist', 'wl_user',
+ array( 'wl_namespace' => $oldnamespace, 'wl_title' => $oldtitle ),
+ $fname, 'FOR UPDATE'
+ );
+ # Construct array to replace into the watchlist
+ $values = array();
+ while ( $s = $dbw->fetchObject( $res ) ) {
+ $values[] = array(
+ 'wl_user' => $s->wl_user,
+ 'wl_namespace' => $newnamespace,
+ 'wl_title' => $newtitle
+ );
+ }
+ $dbw->freeResult( $res );
+
+ if( empty( $values ) ) {
+ // Nothing to do
+ return true;
+ }
+
+ # Perform replace
+ # Note that multi-row replace is very efficient for MySQL but may be inefficient for
+ # some other DBMSes, mostly due to poor simulation by us
+ $dbw->replace( 'watchlist', array(array( 'wl_user', 'wl_namespace', 'wl_title')), $values, $fname );
+ return true;
+ }
+
+
+}
+
+?>
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
new file mode 100644
index 00000000..4031e369
--- /dev/null
+++ b/includes/WebRequest.php
@@ -0,0 +1,491 @@
+<?php
+/**
+ * Deal with importing all those nasssty globals and things
+ * @package MediaWiki
+ */
+
+# Copyright (C) 2003 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
+
+/**
+ * The WebRequest class encapsulates getting at data passed in the
+ * URL or via a POSTed form, handling remove of "magic quotes" slashes,
+ * stripping illegal input characters and normalizing Unicode sequences.
+ *
+ * Usually this is used via a global singleton, $wgRequest. You should
+ * not create a second WebRequest object; make a FauxRequest object if
+ * you want to pass arbitrary data to some function in place of the web
+ * input.
+ *
+ * @package MediaWiki
+ */
+class WebRequest {
+ function WebRequest() {
+ $this->checkMagicQuotes();
+ global $wgUsePathInfo;
+ if( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') && $wgUsePathInfo ) {
+ # Stuff it!
+ $_GET['title'] = $_REQUEST['title'] =
+ substr( $_SERVER['PATH_INFO'], 1 );
+ }
+ }
+
+ /**
+ * Recursively strips slashes from the given array;
+ * used for undoing the evil that is magic_quotes_gpc.
+ * @param array &$arr will be modified
+ * @return array the original array
+ * @private
+ */
+ function &fix_magic_quotes( &$arr ) {
+ foreach( $arr as $key => $val ) {
+ if( is_array( $val ) ) {
+ $this->fix_magic_quotes( $arr[$key] );
+ } else {
+ $arr[$key] = stripslashes( $val );
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * If magic_quotes_gpc option is on, run the global arrays
+ * through fix_magic_quotes to strip out the stupid slashes.
+ * WARNING: This should only be done once! Running a second
+ * time could damage the values.
+ * @private
+ */
+ function checkMagicQuotes() {
+ if ( get_magic_quotes_gpc() ) {
+ $this->fix_magic_quotes( $_COOKIE );
+ $this->fix_magic_quotes( $_ENV );
+ $this->fix_magic_quotes( $_GET );
+ $this->fix_magic_quotes( $_POST );
+ $this->fix_magic_quotes( $_REQUEST );
+ $this->fix_magic_quotes( $_SERVER );
+ }
+ }
+
+ /**
+ * Recursively normalizes UTF-8 strings in the given array.
+ * @param array $data string or array
+ * @return cleaned-up version of the given
+ * @private
+ */
+ function normalizeUnicode( $data ) {
+ if( is_array( $data ) ) {
+ foreach( $data as $key => $val ) {
+ $data[$key] = $this->normalizeUnicode( $val );
+ }
+ } else {
+ $data = UtfNormal::cleanUp( $data );
+ }
+ return $data;
+ }
+
+ /**
+ * Fetch a value from the given array or return $default if it's not set.
+ *
+ * @param array $arr
+ * @param string $name
+ * @param mixed $default
+ * @return mixed
+ * @private
+ */
+ function getGPCVal( $arr, $name, $default ) {
+ if( isset( $arr[$name] ) ) {
+ global $wgContLang;
+ $data = $arr[$name];
+ if( isset( $_GET[$name] ) && !is_array( $data ) ) {
+ # Check for alternate/legacy character encoding.
+ if( isset( $wgContLang ) ) {
+ $data = $wgContLang->checkTitleEncoding( $data );
+ }
+ }
+ require_once( 'normal/UtfNormal.php' );
+ $data = $this->normalizeUnicode( $data );
+ return $data;
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * Fetch a scalar from the input or return $default if it's not set.
+ * Returns a string. Arrays are discarded.
+ *
+ * @param string $name
+ * @param string $default optional default (or NULL)
+ * @return string
+ */
+ function getVal( $name, $default = NULL ) {
+ $val = $this->getGPCVal( $_REQUEST, $name, $default );
+ if( is_array( $val ) ) {
+ $val = $default;
+ }
+ if( is_null( $val ) ) {
+ return null;
+ } else {
+ return (string)$val;
+ }
+ }
+
+ /**
+ * Fetch an array from the input or return $default if it's not set.
+ * If source was scalar, will return an array with a single element.
+ * If no source and no default, returns NULL.
+ *
+ * @param string $name
+ * @param array $default optional default (or NULL)
+ * @return array
+ */
+ function getArray( $name, $default = NULL ) {
+ $val = $this->getGPCVal( $_REQUEST, $name, $default );
+ if( is_null( $val ) ) {
+ return null;
+ } else {
+ return (array)$val;
+ }
+ }
+
+ /**
+ * Fetch an array of integers, or return $default if it's not set.
+ * If source was scalar, will return an array with a single element.
+ * If no source and no default, returns NULL.
+ * If an array is returned, contents are guaranteed to be integers.
+ *
+ * @param string $name
+ * @param array $default option default (or NULL)
+ * @return array of ints
+ */
+ function getIntArray( $name, $default = NULL ) {
+ $val = $this->getArray( $name, $default );
+ if( is_array( $val ) ) {
+ $val = array_map( 'intval', $val );
+ }
+ return $val;
+ }
+
+ /**
+ * Fetch an integer value from the input or return $default if not set.
+ * Guaranteed to return an integer; non-numeric input will typically
+ * return 0.
+ * @param string $name
+ * @param int $default
+ * @return int
+ */
+ function getInt( $name, $default = 0 ) {
+ return intval( $this->getVal( $name, $default ) );
+ }
+
+ /**
+ * Fetch an integer value from the input or return null if empty.
+ * Guaranteed to return an integer or null; non-numeric input will
+ * typically return null.
+ * @param string $name
+ * @return int
+ */
+ function getIntOrNull( $name ) {
+ $val = $this->getVal( $name );
+ return is_numeric( $val )
+ ? intval( $val )
+ : null;
+ }
+
+ /**
+ * Fetch a boolean value from the input or return $default if not set.
+ * Guaranteed to return true or false, with normal PHP semantics for
+ * boolean interpretation of strings.
+ * @param string $name
+ * @param bool $default
+ * @return bool
+ */
+ function getBool( $name, $default = false ) {
+ return $this->getVal( $name, $default ) ? true : false;
+ }
+
+ /**
+ * Return true if the named value is set in the input, whatever that
+ * value is (even "0"). Return false if the named value is not set.
+ * Example use is checking for the presence of check boxes in forms.
+ * @param string $name
+ * @return bool
+ */
+ function getCheck( $name ) {
+ # Checkboxes and buttons are only present when clicked
+ # Presence connotes truth, abscense false
+ $val = $this->getVal( $name, NULL );
+ return isset( $val );
+ }
+
+ /**
+ * Fetch a text string from the given array or return $default if it's not
+ * set. \r is stripped from the text, and with some language modules there
+ * is an input transliteration applied. This should generally be used for
+ * form <textarea> and <input> fields.
+ *
+ * @param string $name
+ * @param string $default optional
+ * @return string
+ */
+ function getText( $name, $default = '' ) {
+ global $wgContLang;
+ $val = $this->getVal( $name, $default );
+ return str_replace( "\r\n", "\n",
+ $wgContLang->recodeInput( $val ) );
+ }
+
+ /**
+ * Extracts the given named values into an array.
+ * If no arguments are given, returns all input values.
+ * No transformation is performed on the values.
+ */
+ function getValues() {
+ $names = func_get_args();
+ if ( count( $names ) == 0 ) {
+ $names = array_keys( $_REQUEST );
+ }
+
+ $retVal = array();
+ foreach ( $names as $name ) {
+ $value = $this->getVal( $name );
+ if ( !is_null( $value ) ) {
+ $retVal[$name] = $value;
+ }
+ }
+ return $retVal;
+ }
+
+ /**
+ * Returns true if the present request was reached by a POST operation,
+ * false otherwise (GET, HEAD, or command-line).
+ *
+ * Note that values retrieved by the object may come from the
+ * GET URL etc even on a POST request.
+ *
+ * @return bool
+ */
+ function wasPosted() {
+ return $_SERVER['REQUEST_METHOD'] == 'POST';
+ }
+
+ /**
+ * Returns true if there is a session cookie set.
+ * This does not necessarily mean that the user is logged in!
+ *
+ * @return bool
+ */
+ function checkSessionCookie() {
+ return isset( $_COOKIE[ini_get('session.name')] );
+ }
+
+ /**
+ * Return the path portion of the request URI.
+ * @return string
+ */
+ function getRequestURL() {
+ $base = $_SERVER['REQUEST_URI'];
+ if( $base{0} == '/' ) {
+ return $base;
+ } else {
+ // We may get paths with a host prepended; strip it.
+ return preg_replace( '!^[^:]+://[^/]+/!', '/', $base );
+ }
+ }
+
+ /**
+ * Return the request URI with the canonical service and hostname.
+ * @return string
+ */
+ function getFullRequestURL() {
+ global $wgServer;
+ return $wgServer . $this->getRequestURL();
+ }
+
+ /**
+ * Take an arbitrary query and rewrite the present URL to include it
+ * @param $query String: query string fragment; do not include initial '?'
+ * @return string
+ */
+ function appendQuery( $query ) {
+ global $wgTitle;
+ $basequery = '';
+ foreach( $_GET as $var => $val ) {
+ if ( $var == 'title' )
+ continue;
+ if ( is_array( $val ) )
+ /* This will happen given a request like
+ * http://en.wikipedia.org/w/index.php?title[]=Special:Userlogin&returnto[]=Main_Page
+ */
+ continue;
+ $basequery .= '&' . urlencode( $var ) . '=' . urlencode( $val );
+ }
+ $basequery .= '&' . $query;
+
+ # Trim the extra &
+ $basequery = substr( $basequery, 1 );
+ return $wgTitle->getLocalURL( $basequery );
+ }
+
+ /**
+ * HTML-safe version of appendQuery().
+ * @param $query String: query string fragment; do not include initial '?'
+ * @return string
+ */
+ function escapeAppendQuery( $query ) {
+ return htmlspecialchars( $this->appendQuery( $query ) );
+ }
+
+ /**
+ * Check for limit and offset parameters on the input, and return sensible
+ * defaults if not given. The limit must be positive and is capped at 5000.
+ * Offset must be positive but is not capped.
+ *
+ * @param $deflimit Integer: limit to use if no input and the user hasn't set the option.
+ * @param $optionname String: to specify an option other than rclimit to pull from.
+ * @return array first element is limit, second is offset
+ */
+ function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
+ global $wgUser;
+
+ $limit = $this->getInt( 'limit', 0 );
+ if( $limit < 0 ) $limit = 0;
+ if( ( $limit == 0 ) && ( $optionname != '' ) ) {
+ $limit = (int)$wgUser->getOption( $optionname );
+ }
+ if( $limit <= 0 ) $limit = $deflimit;
+ if( $limit > 5000 ) $limit = 5000; # We have *some* limits...
+
+ $offset = $this->getInt( 'offset', 0 );
+ if( $offset < 0 ) $offset = 0;
+
+ return array( $limit, $offset );
+ }
+
+ /**
+ * Return the path to the temporary file where PHP has stored the upload.
+ * @param $key String:
+ * @return string or NULL if no such file.
+ */
+ function getFileTempname( $key ) {
+ if( !isset( $_FILES[$key] ) ) {
+ return NULL;
+ }
+ return $_FILES[$key]['tmp_name'];
+ }
+
+ /**
+ * Return the size of the upload, or 0.
+ * @param $key String:
+ * @return integer
+ */
+ function getFileSize( $key ) {
+ if( !isset( $_FILES[$key] ) ) {
+ return 0;
+ }
+ return $_FILES[$key]['size'];
+ }
+
+ /**
+ * Return the upload error or 0
+ * @param $key String:
+ * @return integer
+ */
+ function getUploadError( $key ) {
+ if( !isset( $_FILES[$key] ) || !isset( $_FILES[$key]['error'] ) ) {
+ return 0/*UPLOAD_ERR_OK*/;
+ }
+ return $_FILES[$key]['error'];
+ }
+
+ /**
+ * Return the original filename of the uploaded file, as reported by
+ * the submitting user agent. HTML-style character entities are
+ * interpreted and normalized to Unicode normalization form C, in part
+ * to deal with weird input from Safari with non-ASCII filenames.
+ *
+ * Other than this the name is not verified for being a safe filename.
+ *
+ * @param $key String:
+ * @return string or NULL if no such file.
+ */
+ function getFileName( $key ) {
+ if( !isset( $_FILES[$key] ) ) {
+ return NULL;
+ }
+ $name = $_FILES[$key]['name'];
+
+ # Safari sends filenames in HTML-encoded Unicode form D...
+ # Horrid and evil! Let's try to make some kind of sense of it.
+ $name = Sanitizer::decodeCharReferences( $name );
+ $name = UtfNormal::cleanUp( $name );
+ wfDebug( "WebRequest::getFileName() '" . $_FILES[$key]['name'] . "' normalized to '$name'\n" );
+ return $name;
+ }
+}
+
+/**
+ * WebRequest clone which takes values from a provided array.
+ *
+ * @package MediaWiki
+ */
+class FauxRequest extends WebRequest {
+ var $data = null;
+ var $wasPosted = false;
+
+ function FauxRequest( $data, $wasPosted = false ) {
+ if( is_array( $data ) ) {
+ $this->data = $data;
+ } else {
+ throw new MWException( "FauxRequest() got bogus data" );
+ }
+ $this->wasPosted = $wasPosted;
+ }
+
+ function getVal( $name, $default = NULL ) {
+ return $this->getGPCVal( $this->data, $name, $default );
+ }
+
+ function getText( $name, $default = '' ) {
+ # Override; don't recode since we're using internal data
+ return $this->getVal( $name, $default );
+ }
+
+ function getValues() {
+ return $this->data;
+ }
+
+ function wasPosted() {
+ return $this->wasPosted;
+ }
+
+ function checkSessionCookie() {
+ return false;
+ }
+
+ function getRequestURL() {
+ throw new MWException( 'FauxRequest::getRequestURL() not implemented' );
+ }
+
+ function appendQuery( $query ) {
+ throw new MWException( 'FauxRequest::appendQuery() not implemented' );
+ }
+
+}
+
+?>
diff --git a/includes/Wiki.php b/includes/Wiki.php
new file mode 100644
index 00000000..6f010003
--- /dev/null
+++ b/includes/Wiki.php
@@ -0,0 +1,410 @@
+<?php
+/**
+ * MediaWiki is the to-be base class for this whole project
+ */
+
+class MediaWiki {
+
+ var $GET; /* Stores the $_GET variables at time of creation, can be changed */
+ var $params = array();
+
+ /**
+ * Constructor
+ */
+ function MediaWiki () {
+ $this->GET = $_GET;
+ }
+
+ /**
+ * Stores key/value pairs to circumvent global variables
+ * Note that keys are case-insensitive!
+ */
+ function setVal( $key, &$value ) {
+ $key = strtolower( $key );
+ $this->params[$key] =& $value;
+ }
+
+ /**
+ * Retrieves key/value pairs to circumvent global variables
+ * Note that keys are case-insensitive!
+ */
+ function getVal( $key, $default = '' ) {
+ $key = strtolower( $key );
+ if( isset( $this->params[$key] ) ) {
+ return $this->params[$key];
+ }
+ return $default;
+ }
+
+ /**
+ * Initialization of ... everything
+ @return Article either the object to become $wgArticle, or NULL
+ */
+ function initialize ( &$title, &$output, &$user, $request) {
+ wfProfileIn( 'MediaWiki::initialize' );
+ $this->preliminaryChecks ( $title, $output, $request ) ;
+ $article = NULL;
+ if ( !$this->initializeSpecialCases( $title, $output, $request ) ) {
+ $article = $this->initializeArticle( $title, $request );
+ if( is_object( $article ) ) {
+ $this->performAction( $output, $article, $title, $user, $request );
+ } elseif( is_string( $article ) ) {
+ $output->redirect( $article );
+ } else {
+ throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle() returned neither an object nor a URL" );
+ }
+ }
+ wfProfileOut( 'MediaWiki::initialize' );
+ return $article;
+ }
+
+ /**
+ * Checks some initial queries
+ * Note that $title here is *not* a Title object, but a string!
+ */
+ function checkInitialQueries( $title,$action,&$output,$request, $lang) {
+ if ($request->getVal( 'printable' ) == 'yes') {
+ $output->setPrintable();
+ }
+
+ $ret = NULL ;
+
+
+ if ( '' == $title && 'delete' != $action ) {
+ $ret = Title::newFromText( wfMsgForContent( 'mainpage' ) );
+ } elseif ( $curid = $request->getInt( 'curid' ) ) {
+ # URLs like this are generated by RC, because rc_title isn't always accurate
+ $ret = Title::newFromID( $curid );
+ } else {
+ $ret = Title::newFromURL( $title );
+ /* check variant links so that interwiki links don't have to worry about
+ the possible different language variants
+ */
+ if( count($lang->getVariants()) > 1 && !is_null($ret) && $ret->getArticleID() == 0 )
+ $lang->findVariantLink( $title, $ret );
+
+ }
+ return $ret ;
+ }
+
+ /**
+ * Checks for search query and anon-cannot-read case
+ */
+ function preliminaryChecks ( &$title, &$output, $request ) {
+
+ # Debug statement for user levels
+ // print_r($wgUser);
+
+ $search = $request->getText( 'search' );
+ if( !is_null( $search ) && $search !== '' ) {
+ // Compatibility with old search URLs which didn't use Special:Search
+ // Do this above the read whitelist check for security...
+ $title = Title::makeTitle( NS_SPECIAL, 'Search' );
+ }
+ $this->setVal( 'Search', $search );
+
+ # If the user is not logged in, the Namespace:title of the article must be in
+ # the Read array in order for the user to see it. (We have to check here to
+ # catch special pages etc. We check again in Article::view())
+ if ( !is_null( $title ) && !$title->userCanRead() ) {
+ $output->loginToUse();
+ $output->output();
+ exit;
+ }
+
+ }
+
+ /**
+ * Initialize the object to be known as $wgArticle for special cases
+ */
+ function initializeSpecialCases ( &$title, &$output, $request ) {
+
+ wfProfileIn( 'MediaWiki::initializeSpecialCases' );
+
+ $search = $this->getVal('Search');
+ $action = $this->getVal('Action');
+ if( !$this->getVal('DisableInternalSearch') && !is_null( $search ) && $search !== '' ) {
+ require_once( 'includes/SpecialSearch.php' );
+ $title = Title::makeTitle( NS_SPECIAL, 'Search' );
+ wfSpecialSearch();
+ } else if( !$title or $title->getDBkey() == '' ) {
+ $title = Title::makeTitle( NS_SPECIAL, 'Badtitle' );
+ # Die now before we mess up $wgArticle and the skin stops working
+ throw new ErrorPageError( 'badtitle', 'badtitletext' );
+ } else if ( $title->getInterwiki() != '' ) {
+ if( $rdfrom = $request->getVal( 'rdfrom' ) ) {
+ $url = $title->getFullURL( 'rdfrom=' . urlencode( $rdfrom ) );
+ } else {
+ $url = $title->getFullURL();
+ }
+ /* Check for a redirect loop */
+ if ( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) {
+ $output->redirect( $url );
+ } else {
+ $title = Title::makeTitle( NS_SPECIAL, 'Badtitle' );
+ throw new ErrorPageError( 'badtitle', 'badtitletext' );
+ }
+ } else if ( ( $action == 'view' ) &&
+ (!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) &&
+ !count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) )
+ {
+ /* Redirect to canonical url, make it a 301 to allow caching */
+ $output->setSquidMaxage( 1200 );
+ $output->redirect( $title->getFullURL(), '301');
+ } else if ( NS_SPECIAL == $title->getNamespace() ) {
+ /* actions that need to be made when we have a special pages */
+ SpecialPage::executePath( $title );
+ } else {
+ /* No match to special cases */
+ wfProfileOut( 'MediaWiki::initializeSpecialCases' );
+ return false;
+ }
+ /* Did match a special case */
+ wfProfileOut( 'MediaWiki::initializeSpecialCases' );
+ return true;
+ }
+
+ /**
+ * Create an Article object of the appropriate class for the given page.
+ * @param Title $title
+ * @return Article
+ */
+ function articleFromTitle( $title ) {
+ if( NS_MEDIA == $title->getNamespace() ) {
+ // FIXME: where should this go?
+ $title = Title::makeTitle( NS_IMAGE, $title->getDBkey() );
+ }
+
+ switch( $title->getNamespace() ) {
+ case NS_IMAGE:
+ return new ImagePage( $title );
+ case NS_CATEGORY:
+ return new CategoryPage( $title );
+ default:
+ return new Article( $title );
+ }
+ }
+
+ /**
+ * Initialize the object to be known as $wgArticle for "standard" actions
+ * Create an Article object for the page, following redirects if needed.
+ * @param Title $title
+ * @param Request $request
+ * @param string $action
+ * @return mixed an Article, or a string to redirect to another URL
+ */
+ function initializeArticle( $title, $request ) {
+ global $wgTitle;
+ wfProfileIn( 'MediaWiki::initializeArticle' );
+
+ $action = $this->getVal('Action');
+ $article = $this->articleFromTitle( $title );
+
+ // Namespace might change when using redirects
+ if( $action == 'view' && !$request->getVal( 'oldid' ) &&
+ $request->getVal( 'redirect' ) != 'no' ) {
+
+ $dbr =& wfGetDB(DB_SLAVE);
+ $article->loadPageData($article->pageDataFromTitle($dbr, $title));
+
+ /* Follow redirects only for... redirects */
+ if ($article->mIsRedirect) {
+ $target = $article->followRedirect();
+ if( is_string( $target ) ) {
+ global $wgDisableHardRedirects;
+ if( !$wgDisableHardRedirects ) {
+ // we'll need to redirect
+ return $target;
+ }
+ }
+ if( is_object( $target ) ) {
+ /* Rewrite environment to redirected article */
+ $rarticle = $this->articleFromTitle($target);
+ $rarticle->loadPageData($rarticle->pageDataFromTitle($dbr,$target));
+ if ($rarticle->mTitle->mArticleID) {
+ $article = $rarticle;
+ $wgTitle = $target;
+ $article->setRedirectedFrom( $title );
+ } else {
+ $wgTitle = $title;
+ }
+ }
+ } else {
+ $wgTitle = $article->mTitle;
+ }
+ }
+ wfProfileOut( 'MediaWiki::initializeArticle' );
+ return $article;
+ }
+
+ /**
+ * Cleaning up by doing deferred updates, calling loadbalancer and doing the output
+ */
+ function finalCleanup ( &$deferredUpdates, &$loadBalancer, &$output ) {
+ wfProfileIn( 'MediaWiki::finalCleanup' );
+ $this->doUpdates( $deferredUpdates );
+ $this->doJobs();
+ $loadBalancer->saveMasterPos();
+ # Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
+ $loadBalancer->commitAll();
+ $output->output();
+ wfProfileOut( 'MediaWiki::finalCleanup' );
+ }
+
+ /**
+ * Deferred updates aren't really deferred anymore. It's important to report errors to the
+ * user, and that means doing this before OutputPage::output(). Note that for page saves,
+ * the client will wait until the script exits anyway before following the redirect.
+ */
+ function doUpdates ( &$updates ) {
+ wfProfileIn( 'MediaWiki::doUpdates' );
+ foreach( $updates as $up ) {
+ $up->doUpdate();
+ }
+ wfProfileOut( 'MediaWiki::doUpdates' );
+ }
+
+ /**
+ * Do a job from the job queue
+ */
+ function doJobs() {
+ global $wgJobRunRate;
+
+ if ( $wgJobRunRate <= 0 ) {
+ return;
+ }
+ if ( $wgJobRunRate < 1 ) {
+ $max = mt_getrandmax();
+ if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
+ return;
+ }
+ $n = 1;
+ } else {
+ $n = intval( $wgJobRunRate );
+ }
+
+ while ( $n-- && false != ($job = Job::pop())) {
+ $output = $job->toString() . "\n";
+ $t = -wfTime();
+ $success = $job->run();
+ $t += wfTime();
+ $t = round( $t*1000 );
+ if ( !$success ) {
+ $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
+ } else {
+ $output .= "Success, Time: $t ms\n";
+ }
+ wfDebugLog( 'jobqueue', $output );
+ }
+ }
+
+ /**
+ * Ends this task peacefully
+ */
+ function restInPeace ( &$loadBalancer ) {
+ wfProfileClose();
+ logProfilingData();
+ $loadBalancer->closeAll();
+ wfDebug( "Request ended normally\n" );
+ }
+
+ /**
+ * Perform one of the "standard" actions
+ */
+ function performAction( &$output, &$article, &$title, &$user, &$request ) {
+
+ wfProfileIn( 'MediaWiki::performAction' );
+
+ $action = $this->getVal('Action');
+ if( in_array( $action, $this->getVal('DisabledActions',array()) ) ) {
+ /* No such action; this will switch to the default case */
+ $action = 'nosuchaction';
+ }
+
+ switch( $action ) {
+ case 'view':
+ $output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
+ $article->view();
+ break;
+ case 'watch':
+ case 'unwatch':
+ case 'delete':
+ case 'revert':
+ case 'rollback':
+ case 'protect':
+ case 'unprotect':
+ case 'info':
+ case 'markpatrolled':
+ case 'render':
+ case 'deletetrackback':
+ case 'purge':
+ $article->$action();
+ break;
+ case 'print':
+ $article->view();
+ break;
+ case 'dublincore':
+ if( !$this->getVal( 'EnableDublinCoreRdf' ) ) {
+ wfHttpError( 403, 'Forbidden', wfMsg( 'nodublincore' ) );
+ } else {
+ require_once( 'includes/Metadata.php' );
+ wfDublinCoreRdf( $article );
+ }
+ break;
+ case 'creativecommons':
+ if( !$this->getVal( 'EnableCreativeCommonsRdf' ) ) {
+ wfHttpError( 403, 'Forbidden', wfMsg( 'nocreativecommons' ) );
+ } else {
+ require_once( 'includes/Metadata.php' );
+ wfCreativeCommonsRdf( $article );
+ }
+ break;
+ case 'credits':
+ require_once( 'includes/Credits.php' );
+ showCreditsPage( $article );
+ break;
+ case 'submit':
+ if( !$this->getVal( 'CommandLineMode' ) && !$request->checkSessionCookie() ) {
+ /* Send a cookie so anons get talk message notifications */
+ User::SetupSession();
+ }
+ /* Continue... */
+ case 'edit':
+ $internal = $request->getVal( 'internaledit' );
+ $external = $request->getVal( 'externaledit' );
+ $section = $request->getVal( 'section' );
+ $oldid = $request->getVal( 'oldid' );
+ if( !$this->getVal( 'UseExternalEditor' ) || $action=='submit' || $internal ||
+ $section || $oldid || ( !$user->getOption( 'externaleditor' ) && !$external ) ) {
+ $editor = new EditPage( $article );
+ $editor->submit();
+ } elseif( $this->getVal( 'UseExternalEditor' ) && ( $external || $user->getOption( 'externaleditor' ) ) ) {
+ $mode = $request->getVal( 'mode' );
+ $extedit = new ExternalEdit( $article, $mode );
+ $extedit->edit();
+ }
+ break;
+ case 'history':
+ if( $_SERVER['REQUEST_URI'] == $title->getInternalURL( 'action=history' ) ) {
+ $output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
+ }
+ $history = new PageHistory( $article );
+ $history->history();
+ break;
+ case 'raw':
+ $raw = new RawPage( $article );
+ $raw->view();
+ break;
+ default:
+ if( wfRunHooks( 'UnknownAction', array( $action, $article ) ) ) {
+ $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
+ }
+ }
+ wfProfileOut( 'MediaWiki::performAction' );
+
+
+ }
+
+}; /* End of class MediaWiki */
+
+?>
diff --git a/includes/WikiError.php b/includes/WikiError.php
new file mode 100644
index 00000000..1b2c03bf
--- /dev/null
+++ b/includes/WikiError.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * MediaWiki error classes
+ * Copyright (C) 2005 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
+ */
+
+/**
+ * Since PHP4 doesn't have exceptions, here's some error objects
+ * loosely modeled on the standard PEAR_Error model...
+ * @package MediaWiki
+ */
+class WikiError {
+ /**
+ * @param string $message
+ */
+ function WikiError( $message ) {
+ $this->mMessage = $message;
+ }
+
+ /**
+ * @return string Plaintext error message to display
+ */
+ function getMessage() {
+ return $this->mMessage;
+ }
+
+ /**
+ * In following PEAR_Error model this could be formatted differently,
+ * but so far it's not.
+ * @return string
+ */
+ function toString() {
+ return $this->getMessage();
+ }
+
+ /**
+ * Returns true if the given object is a WikiError-descended
+ * error object, false otherwise.
+ *
+ * @param mixed $object
+ * @return bool
+ * @static
+ */
+ function isError( &$object ) {
+ return is_a( $object, 'WikiError' );
+ }
+}
+
+/**
+ * Localized error message object
+ * @package MediaWiki
+ */
+class WikiErrorMsg extends WikiError {
+ /**
+ * @param string $message Wiki message name
+ * @param ... parameters to pass to wfMsg()
+ */
+ function WikiErrorMsg( $message/*, ... */ ) {
+ $args = func_get_args();
+ array_shift( $args );
+ $this->mMessage = wfMsgReal( $message, $args, true );
+ }
+}
+
+/**
+ * @package MediaWiki
+ * @todo document
+ */
+class WikiXmlError extends WikiError {
+ /**
+ * @param resource $parser
+ * @param string $message
+ */
+ function WikiXmlError( $parser, $message = 'XML parsing error', $context = null, $offset = 0 ) {
+ $this->mXmlError = xml_get_error_code( $parser );
+ $this->mColumn = xml_get_current_column_number( $parser );
+ $this->mLine = xml_get_current_line_number( $parser );
+ $this->mByte = xml_get_current_byte_index( $parser );
+ $this->mContext = $this->_extractContext( $context, $offset );
+ $this->mMessage = $message;
+ xml_parser_free( $parser );
+ wfDebug( "WikiXmlError: " . $this->getMessage() . "\n" );
+ }
+
+ /** @return string */
+ function getMessage() {
+ return sprintf( '%s at line %d, col %d (byte %d%s): %s',
+ $this->mMessage,
+ $this->mLine,
+ $this->mColumn,
+ $this->mByte,
+ $this->mContext,
+ xml_error_string( $this->mXmlError ) );
+ }
+
+ function _extractContext( $context, $offset ) {
+ if( is_null( $context ) ) {
+ return null;
+ } else {
+ // Hopefully integer overflow will be handled transparently here
+ $inlineOffset = $this->mByte - $offset;
+ return '; "' . substr( $context, $inlineOffset, 16 ) . '"';
+ }
+ }
+}
+
+?>
diff --git a/includes/Xml.php b/includes/Xml.php
new file mode 100644
index 00000000..52993367
--- /dev/null
+++ b/includes/Xml.php
@@ -0,0 +1,279 @@
+<?php
+
+/**
+ * Module of static functions for generating XML
+ */
+
+class Xml {
+ /**
+ * Format an XML element with given attributes and, optionally, text content.
+ * Element and attribute names are assumed to be ready for literal inclusion.
+ * Strings are assumed to not contain XML-illegal characters; special
+ * characters (<, >, &) are escaped but illegals are not touched.
+ *
+ * @param $element String:
+ * @param $attribs Array: Name=>value pairs. Values will be escaped.
+ * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @return string
+ */
+ function element( $element, $attribs = null, $contents = '') {
+ $out = '<' . $element;
+ if( !is_null( $attribs ) ) {
+ foreach( $attribs as $name => $val ) {
+ $out .= ' ' . $name . '="' . Sanitizer::encodeAttribute( $val ) . '"';
+ }
+ }
+ if( is_null( $contents ) ) {
+ $out .= '>';
+ } else {
+ if( $contents === '' ) {
+ $out .= ' />';
+ } else {
+ $out .= '>' . htmlspecialchars( $contents ) . "</$element>";
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Format an XML element as with self::element(), but run text through the
+ * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
+ * is passed.
+ *
+ * @param $element String:
+ * @param $attribs Array: Name=>value pairs. Values will be escaped.
+ * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @return string
+ */
+ function elementClean( $element, $attribs = array(), $contents = '') {
+ if( $attribs ) {
+ $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
+ }
+ if( $contents ) {
+ $contents = UtfNormal::cleanUp( $contents );
+ }
+ return self::element( $element, $attribs, $contents );
+ }
+
+ // Shortcuts
+ function openElement( $element, $attribs = null ) { return self::element( $element, $attribs, null ); }
+ function closeElement( $element ) { return "</$element>"; }
+
+ /**
+ * Create a namespace selector
+ *
+ * @param $selected Mixed: the namespace which should be selected, default ''
+ * @param $allnamespaces String: value of a special item denoting all namespaces. Null to not include (default)
+ * @param $includehidden Bool: include hidden namespaces?
+ * @return String: Html string containing the namespace selector
+ */
+ function &namespaceSelector($selected = '', $allnamespaces = null, $includehidden=false) {
+ global $wgContLang;
+ if( $selected !== '' ) {
+ if( is_null( $selected ) ) {
+ // No namespace selected; let exact match work without hitting Main
+ $selected = '';
+ } else {
+ // Let input be numeric strings without breaking the empty match.
+ $selected = intval( $selected );
+ }
+ }
+ $s = "<select id='namespace' name='namespace' class='namespaceselector'>\n\t";
+ $arr = $wgContLang->getFormattedNamespaces();
+ if( !is_null($allnamespaces) ) {
+ $arr = array($allnamespaces => wfMsg('namespacesall')) + $arr;
+ }
+ foreach ($arr as $index => $name) {
+ if ($index < NS_MAIN) continue;
+
+ $name = $index !== 0 ? $name : wfMsg('blanknamespace');
+
+ if ($index === $selected) {
+ $s .= self::element("option",
+ array("value" => $index, "selected" => "selected"),
+ $name);
+ } else {
+ $s .= self::element("option", array("value" => $index), $name);
+ }
+ }
+ $s .= "\n</select>\n";
+ return $s;
+ }
+
+ function span( $text, $class, $attribs=array() ) {
+ return self::element( 'span', array( 'class' => $class ) + $attribs, $text );
+ }
+
+ /**
+ * Convenience function to build an HTML text input field
+ * @return string HTML
+ */
+ function input( $name, $size=false, $value=false, $attribs=array() ) {
+ return self::element( 'input', array(
+ 'name' => $name,
+ 'size' => $size,
+ 'value' => $value ) + $attribs );
+ }
+
+ /**
+ * Internal function for use in checkboxes and radio buttons and such.
+ * @return array
+ */
+ function attrib( $name, $present = true ) {
+ return $present ? array( $name => $name ) : array();
+ }
+
+ /**
+ * Convenience function to build an HTML checkbox
+ * @return string HTML
+ */
+ function check( $name, $checked=false, $attribs=array() ) {
+ return self::element( 'input', array(
+ 'name' => $name,
+ 'type' => 'checkbox',
+ 'value' => 1 ) + self::attrib( 'checked', $checked ) + $attribs );
+ }
+
+ /**
+ * Convenience function to build an HTML radio button
+ * @return string HTML
+ */
+ function radio( $name, $value, $checked=false, $attribs=array() ) {
+ return self::element( 'input', array(
+ 'name' => $name,
+ 'type' => 'radio',
+ 'value' => $value ) + self::attrib( 'checked', $checked ) + $attribs );
+ }
+
+ /**
+ * Convenience function to build an HTML form label
+ * @return string HTML
+ */
+ function label( $label, $id ) {
+ return self::element( 'label', array( 'for' => $id ), $label );
+ }
+
+ /**
+ * Convenience function to build an HTML text input field with a label
+ * @return string HTML
+ */
+ function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+ return Xml::label( $label, $id ) .
+ '&nbsp;' .
+ self::input( $name, $size, $value, array( 'id' => $id ) + $attribs );
+ }
+
+ /**
+ * Convenience function to build an HTML checkbox with a label
+ * @return string HTML
+ */
+ function checkLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
+ return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) .
+ '&nbsp;' .
+ self::label( $label, $id );
+ }
+
+ /**
+ * Convenience function to build an HTML radio button with a label
+ * @return string HTML
+ */
+ function radioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
+ return self::radio( $name, $value, $checked, array( 'id' => $id ) + $attribs ) .
+ '&nbsp;' .
+ self::label( $label, $id );
+ }
+
+ /**
+ * Convenience function to build an HTML submit button
+ * @param $value String: label text for the button
+ * @param $attribs Array: optional custom attributes
+ * @return string HTML
+ */
+ function submitButton( $value, $attribs=array() ) {
+ return self::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
+ }
+
+ /**
+ * Convenience function to build an HTML hidden form field.
+ * @todo Document $name parameter.
+ * @param $name FIXME
+ * @param $value String: label text for the button
+ * @param $attribs Array: optional custom attributes
+ * @return string HTML
+ */
+ function hidden( $name, $value, $attribs=array() ) {
+ return self::element( 'input', array(
+ 'name' => $name,
+ 'type' => 'hidden',
+ 'value' => $value ) + $attribs );
+ }
+
+ /**
+ * Returns an escaped string suitable for inclusion in a string literal
+ * for JavaScript source code.
+ * Illegal control characters are assumed not to be present.
+ *
+ * @param string $string
+ * @return string
+ */
+ function escapeJsString( $string ) {
+ // See ECMA 262 section 7.8.4 for string literal format
+ $pairs = array(
+ "\\" => "\\\\",
+ "\"" => "\\\"",
+ '\'' => '\\\'',
+ "\n" => "\\n",
+ "\r" => "\\r",
+
+ # To avoid closing the element or CDATA section
+ "<" => "\\x3c",
+ ">" => "\\x3e",
+ );
+ return strtr( $string, $pairs );
+ }
+
+ /**
+ * Check if a string is well-formed XML.
+ * Must include the surrounding tag.
+ *
+ * @param $text String: string to test.
+ * @return bool
+ *
+ * @todo Error position reporting return
+ */
+ function isWellFormed( $text ) {
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ if( !xml_parse( $parser, $text, true ) ) {
+ $err = xml_error_string( xml_get_error_code( $parser ) );
+ $position = xml_get_current_byte_index( $parser );
+ //$fragment = $this->extractFragment( $html, $position );
+ //$this->mXmlError = "$err at byte $position:\n$fragment";
+ xml_parser_free( $parser );
+ return false;
+ }
+ xml_parser_free( $parser );
+ return true;
+ }
+
+ /**
+ * Check if a string is a well-formed XML fragment.
+ * Wraps fragment in an \<html\> bit and doctype, so it can be a fragment
+ * and can use HTML named entities.
+ *
+ * @param $text String:
+ * @return bool
+ */
+ function isWellFormedXmlFragment( $text ) {
+ $html =
+ Sanitizer::hackDocType() .
+ '<html>' .
+ $text .
+ '</html>';
+ return Xml::isWellFormed( $html );
+ }
+}
+?>
diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php
new file mode 100644
index 00000000..64e349f2
--- /dev/null
+++ b/includes/XmlFunctions.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Aliases for functions in the Xml module
+ */
+function wfElement( $element, $attribs = null, $contents = '') {
+ return Xml::element( $element, $attribs, $contents );
+}
+function wfElementClean( $element, $attribs = array(), $contents = '') {
+ return Xml::elementClean( $element, $attribs, $contents );
+}
+function wfOpenElement( $element, $attribs = null ) {
+ return Xml::openElement( $element, $attribs );
+}
+function wfCloseElement( $element ) {
+ return "</$element>";
+}
+function &HTMLnamespaceselector($selected = '', $allnamespaces = null, $includehidden=false) {
+ return Xml::namespaceSelector( $selected, $allnamespaces, $includehidden );
+}
+function wfSpan( $text, $class, $attribs=array() ) {
+ return Xml::span( $text, $class, $attribs );
+}
+function wfInput( $name, $size=false, $value=false, $attribs=array() ) {
+ return Xml::input( $name, $size, $value, $attribs );
+}
+function wfAttrib( $name, $present = true ) {
+ return Xml::attrib( $name, $present );
+}
+function wfCheck( $name, $checked=false, $attribs=array() ) {
+ return Xml::check( $name, $checked, $attribs );
+}
+function wfRadio( $name, $value, $checked=false, $attribs=array() ) {
+ return Xml::radio( $name, $value, $checked, $attribs );
+}
+function wfLabel( $label, $id ) {
+ return Xml::label( $label, $id );
+}
+function wfInputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+ return Xml::inputLabel( $label, $name, $id, $size, $value, $attribs );
+}
+function wfCheckLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
+ return Xml::checkLabel( $label, $name, $id, $checked, $attribs );
+}
+function wfRadioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
+ return Xml::radioLabel( $label, $name, $value, $id, $checked, $attribs );
+}
+function wfSubmitButton( $value, $attribs=array() ) {
+ return Xml::submitButton( $value, $attribs );
+}
+function wfHidden( $name, $value, $attribs=array() ) {
+ return Xml::hidden( $name, $value, $attribs );
+}
+function wfEscapeJsString( $string ) {
+ return Xml::escapeJsString( $string );
+}
+function wfIsWellFormedXml( $text ) {
+ return Xml::isWellFormed( $text );
+}
+function wfIsWellFormedXmlFragment( $text ) {
+ return Xml::isWellFormedXmlFragment( $text );
+}
+
+
+?>
diff --git a/includes/ZhClient.php b/includes/ZhClient.php
new file mode 100644
index 00000000..0451ce81
--- /dev/null
+++ b/includes/ZhClient.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * @package MediaWiki
+ */
+
+/**
+ * Client for querying zhdaemon
+ *
+ * @package MediaWiki
+ */
+class ZhClient {
+ var $mHost, $mPort, $mFP, $mConnected;
+
+ /**
+ * Constructor
+ *
+ * @access private
+ */
+ function ZhClient($host, $port) {
+ $this->mHost = $host;
+ $this->mPort = $port;
+ $this->mConnected = $this->connect();
+ }
+
+ /**
+ * Check if connection to zhdaemon is successful
+ *
+ * @access public
+ */
+ function isconnected() {
+ return $this->mConnected;
+ }
+
+ /**
+ * Establish conncetion
+ *
+ * @access private
+ */
+ function connect() {
+ wfSuppressWarnings();
+ $this->mFP = fsockopen($this->mHost, $this->mPort, $errno, $errstr, 30);
+ wfRestoreWarnings();
+ if(!$this->mFP) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Query the daemon and return the result
+ *
+ * @access private
+ */
+ function query($request) {
+ if(!$this->mConnected)
+ return false;
+
+ fwrite($this->mFP, $request);
+
+ $result=fgets($this->mFP, 1024);
+
+ list($status, $len) = explode(" ", $result);
+ if($status == 'ERROR') {
+ //$len is actually the error code...
+ print "zhdaemon error $len<br />\n";
+ return false;
+ }
+ $bytesread=0;
+ $data='';
+ while(!feof($this->mFP) && $bytesread<$len) {
+ $str= fread($this->mFP, $len-$bytesread);
+ $bytesread += strlen($str);
+ $data .= $str;
+ }
+ //data should be of length $len. otherwise something is wrong
+ if(strlen($data) != $len)
+ return false;
+ return $data;
+ }
+
+ /**
+ * Convert the input to a different language variant
+ *
+ * @param string $text input text
+ * @param string $tolang language variant
+ * @return string the converted text
+ * @access public
+ */
+ function convert($text, $tolang) {
+ $len = strlen($text);
+ $q = "CONV $tolang $len\n$text";
+ $result = $this->query($q);
+ if(!$result)
+ $result = $text;
+ return $result;
+ }
+
+ /**
+ * Convert the input to all possible variants
+ *
+ * @param string $text input text
+ * @return array langcode => converted_string
+ * @access public
+ */
+ function convertToAllVariants($text) {
+ $len = strlen($text);
+ $q = "CONV ALL $len\n$text";
+ $result = $this->query($q);
+ if(!$result)
+ return false;
+ list($infoline, $data) = explode('|', $result, 2);
+ $info = explode(";", $infoline);
+ $ret = array();
+ $i=0;
+ foreach($info as $variant) {
+ list($code, $len) = explode(' ', $variant);
+ $ret[strtolower($code)] = substr($data, $i, $len);
+ $r = $ret[strtolower($code)];
+ $i+=$len;
+ }
+ return $ret;
+ }
+ /**
+ * Perform word segmentation
+ *
+ * @param string $text input text
+ * @return string segmented text
+ * @access public
+ */
+ function segment($text) {
+ $len = strlen($text);
+ $q = "SEG $len\n$text";
+ $result = $this->query($q);
+ if(!$result) {// fallback to character based segmentation
+ $result = ZhClientFake::segment($text);
+ }
+ return $result;
+ }
+
+ /**
+ * Close the connection
+ *
+ * @access public
+ */
+ function close() {
+ fclose($this->mFP);
+ }
+}
+?> \ No newline at end of file
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
new file mode 100644
index 00000000..e63281eb
--- /dev/null
+++ b/includes/ZhConversion.php
@@ -0,0 +1,8457 @@
+<?php
+/**
+ * Simplified/Traditional Chinese conversion tables
+ *
+ * Automatically generated using code and data in includes/zhtable/
+ * Do not modify directly!
+ *
+ * @package MediaWiki
+*/
+
+$zh2TW=array(
+"画"=>"畫",
+"板"=>"板",
+"表"=>"表",
+"才"=>"才",
+"丑"=>"醜",
+"出"=>"出",
+"淀"=>"澱",
+"冬"=>"冬",
+"范"=>"範",
+"丰"=>"豐",
+"刮"=>"刮",
+"后"=>"後",
+"胡"=>"胡",
+"回"=>"回",
+"伙"=>"夥",
+"姜"=>"薑",
+"借"=>"借",
+"克"=>"克",
+"困"=>"困",
+"漓"=>"漓",
+"里"=>"里",
+"帘"=>"簾",
+"霉"=>"霉",
+"面"=>"面",
+"蔑"=>"蔑",
+"千"=>"千",
+"秋"=>"秋",
+"松"=>"松",
+"咸"=>"咸",
+"向"=>"向",
+"余"=>"餘",
+"郁"=>"鬱",
+"御"=>"御",
+"愿"=>"願",
+"云"=>"雲",
+"芸"=>"芸",
+"沄"=>"沄",
+"致"=>"致",
+"制"=>"制",
+"朱"=>"朱",
+"筑"=>"築",
+"准"=>"準",
+"厂"=>"廠",
+"广"=>"廣",
+"辟"=>"闢",
+"别"=>"別",
+"卜"=>"卜",
+"沈"=>"沈",
+"冲"=>"沖",
+"种"=>"種",
+"虫"=>"蟲",
+"担"=>"擔",
+"党"=>"黨",
+"斗"=>"鬥",
+"儿"=>"兒",
+"干"=>"乾",
+"谷"=>"谷",
+"柜"=>"櫃",
+"合"=>"合",
+"划"=>"劃",
+"坏"=>"壞",
+"几"=>"幾",
+"系"=>"系",
+"家"=>"家",
+"价"=>"價",
+"据"=>"據",
+"卷"=>"捲",
+"适"=>"適",
+"蜡"=>"蠟",
+"腊"=>"臘",
+"了"=>"了",
+"累"=>"累",
+"么"=>"麽",
+"蒙"=>"蒙",
+"万"=>"萬",
+"宁"=>"寧",
+"朴"=>"樸",
+"苹"=>"蘋",
+"仆"=>"僕",
+"曲"=>"曲",
+"确"=>"確",
+"舍"=>"舍",
+"胜"=>"勝",
+"术"=>"術",
+"台"=>"台",
+"体"=>"體",
+"涂"=>"塗",
+"叶"=>"葉",
+"吁"=>"吁",
+"旋"=>"旋",
+"佣"=>"傭",
+"与"=>"與",
+"折"=>"折",
+"征"=>"徵",
+"症"=>"症",
+"恶"=>"惡",
+"发"=>"發",
+"复"=>"復",
+"汇"=>"匯",
+"获"=>"獲",
+"饥"=>"飢",
+"尽"=>"盡",
+"历"=>"歷",
+"卤"=>"滷",
+"弥"=>"彌",
+"签"=>"簽",
+"纤"=>"纖",
+"苏"=>"蘇",
+"坛"=>"壇",
+"团"=>"團",
+"须"=>"須",
+"脏"=>"臟",
+"只"=>"只",
+"钟"=>"鐘",
+"药"=>"藥",
+"同"=>"同",
+"志"=>"志",
+"杯"=>"杯",
+"岳"=>"岳",
+"布"=>"布",
+"当"=>"當",
+"吊"=>"弔",
+"仇"=>"仇",
+"蕴"=>"蘊",
+"线"=>"線",
+"为"=>"為",
+"产"=>"產",
+"众"=>"眾",
+"伪"=>"偽",
+"凫"=>"鳧",
+"厕"=>"廁",
+"启"=>"啟",
+"墙"=>"牆",
+"壳"=>"殼",
+"奖"=>"獎",
+"妫"=>"媯",
+"并"=>"並",
+"录"=>"錄",
+"悫"=>"愨",
+"极"=>"極",
+"沩"=>"溈",
+"瘘"=>"瘺",
+"硷"=>"鹼",
+"竖"=>"豎",
+"绝"=>"絕",
+"绣"=>"繡",
+"绦"=>"絛",
+"绱"=>"緔",
+"绷"=>"綳",
+"绿"=>"綠",
+"缰"=>"韁",
+"苧"=>"苎",
+"莼"=>"蒓",
+"说"=>"說",
+"谣"=>"謠",
+"谫"=>"譾",
+"赃"=>"贓",
+"赍"=>"齎",
+"赝"=>"贗",
+"酝"=>"醞",
+"采"=>"採",
+"钩"=>"鉤",
+"钵"=>"缽",
+"锈"=>"銹",
+"锐"=>"銳",
+"锨"=>"杴",
+"镌"=>"鐫",
+"镢"=>"钁",
+"阅"=>"閱",
+"颓"=>"頹",
+"颜"=>"顏",
+"骂"=>"罵",
+"鲇"=>"鯰",
+"鲞"=>"鯗",
+"鳄"=>"鱷",
+"鸡"=>"雞",
+"鹚"=>"鶿",
+"䌶"=>"䊷",
+"䜥"=>"𧩙",
+"专"=>"專",
+"业"=>"業",
+"丛"=>"叢",
+"东"=>"東",
+"丝"=>"絲",
+"丢"=>"丟",
+"两"=>"兩",
+"严"=>"嚴",
+"丧"=>"喪",
+"个"=>"個",
+"临"=>"臨",
+"丽"=>"麗",
+"举"=>"舉",
+"义"=>"義",
+"乌"=>"烏",
+"乐"=>"樂",
+"乔"=>"喬",
+"习"=>"習",
+"乡"=>"鄉",
+"书"=>"書",
+"买"=>"買",
+"乱"=>"亂",
+"争"=>"爭",
+"于"=>"於",
+"亏"=>"虧",
+"亚"=>"亞",
+"亩"=>"畝",
+"亲"=>"親",
+"亵"=>"褻",
+"亸"=>"嚲",
+"亿"=>"億",
+"仅"=>"僅",
+"从"=>"從",
+"仑"=>"侖",
+"仓"=>"倉",
+"仪"=>"儀",
+"们"=>"們",
+"优"=>"優",
+"会"=>"會",
+"伛"=>"傴",
+"伞"=>"傘",
+"伟"=>"偉",
+"传"=>"傳",
+"伣"=>"俔",
+"伤"=>"傷",
+"伥"=>"倀",
+"伦"=>"倫",
+"伧"=>"傖",
+"伫"=>"佇",
+"佥"=>"僉",
+"侠"=>"俠",
+"侣"=>"侶",
+"侥"=>"僥",
+"侦"=>"偵",
+"侧"=>"側",
+"侨"=>"僑",
+"侩"=>"儈",
+"侪"=>"儕",
+"侬"=>"儂",
+"俣"=>"俁",
+"俦"=>"儔",
+"俨"=>"儼",
+"俩"=>"倆",
+"俪"=>"儷",
+"俫"=>"倈",
+"俭"=>"儉",
+"债"=>"債",
+"倾"=>"傾",
+"偬"=>"傯",
+"偻"=>"僂",
+"偾"=>"僨",
+"偿"=>"償",
+"傥"=>"儻",
+"傧"=>"儐",
+"储"=>"儲",
+"傩"=>"儺",
+"兑"=>"兌",
+"兖"=>"兗",
+"兰"=>"蘭",
+"关"=>"關",
+"兴"=>"興",
+"兹"=>"茲",
+"养"=>"養",
+"兽"=>"獸",
+"冁"=>"囅",
+"内"=>"內",
+"冈"=>"岡",
+"册"=>"冊",
+"写"=>"寫",
+"军"=>"軍",
+"农"=>"農",
+"冯"=>"馮",
+"决"=>"決",
+"况"=>"況",
+"冻"=>"凍",
+"净"=>"凈",
+"凉"=>"涼",
+"减"=>"減",
+"凑"=>"湊",
+"凛"=>"凜",
+"凤"=>"鳳",
+"凭"=>"憑",
+"凯"=>"凱",
+"击"=>"擊",
+"凿"=>"鑿",
+"刍"=>"芻",
+"刘"=>"劉",
+"则"=>"則",
+"刚"=>"剛",
+"创"=>"創",
+"删"=>"刪",
+"刬"=>"剗",
+"刭"=>"剄",
+"刹"=>"剎",
+"刽"=>"劊",
+"刿"=>"劌",
+"剀"=>"剴",
+"剂"=>"劑",
+"剐"=>"剮",
+"剑"=>"劍",
+"剥"=>"剝",
+"剧"=>"劇",
+"劝"=>"勸",
+"办"=>"辦",
+"务"=>"務",
+"劢"=>"勱",
+"动"=>"動",
+"励"=>"勵",
+"劲"=>"勁",
+"劳"=>"勞",
+"势"=>"勢",
+"勋"=>"勛",
+"勚"=>"勩",
+"匀"=>"勻",
+"匦"=>"匭",
+"匮"=>"匱",
+"区"=>"區",
+"医"=>"醫",
+"华"=>"華",
+"协"=>"協",
+"单"=>"單",
+"卖"=>"賣",
+"卢"=>"盧",
+"卫"=>"衛",
+"却"=>"卻",
+"厅"=>"廳",
+"厉"=>"厲",
+"压"=>"壓",
+"厌"=>"厭",
+"厍"=>"厙",
+"厐"=>"龎",
+"厘"=>"釐",
+"厢"=>"廂",
+"厣"=>"厴",
+"厦"=>"廈",
+"厨"=>"廚",
+"厩"=>"廄",
+"厮"=>"廝",
+"县"=>"縣",
+"叁"=>"叄",
+"参"=>"參",
+"双"=>"雙",
+"变"=>"變",
+"叙"=>"敘",
+"叠"=>"疊",
+"号"=>"號",
+"叹"=>"嘆",
+"叽"=>"嘰",
+"吓"=>"嚇",
+"吕"=>"呂",
+"吗"=>"嗎",
+"吣"=>"唚",
+"吨"=>"噸",
+"听"=>"聽",
+"吴"=>"吳",
+"呐"=>"吶",
+"呒"=>"嘸",
+"呓"=>"囈",
+"呕"=>"嘔",
+"呖"=>"嚦",
+"呗"=>"唄",
+"员"=>"員",
+"呙"=>"咼",
+"呛"=>"嗆",
+"呜"=>"嗚",
+"咏"=>"詠",
+"咙"=>"嚨",
+"咛"=>"嚀",
+"咝"=>"噝",
+"咤"=>"吒",
+"响"=>"響",
+"哑"=>"啞",
+"哒"=>"噠",
+"哓"=>"嘵",
+"哔"=>"嗶",
+"哕"=>"噦",
+"哗"=>"嘩",
+"哙"=>"噲",
+"哜"=>"嚌",
+"哝"=>"噥",
+"哟"=>"喲",
+"唛"=>"嘜",
+"唝"=>"嗊",
+"唠"=>"嘮",
+"唡"=>"啢",
+"唢"=>"嗩",
+"唤"=>"喚",
+"啧"=>"嘖",
+"啬"=>"嗇",
+"啭"=>"囀",
+"啮"=>"嚙",
+"啴"=>"嘽",
+"啸"=>"嘯",
+"㖞"=>"喎",
+"喷"=>"噴",
+"喽"=>"嘍",
+"喾"=>"嚳",
+"嗫"=>"囁",
+"嗳"=>"噯",
+"嘘"=>"噓",
+"嘤"=>"嚶",
+"嘱"=>"囑",
+"噜"=>"嚕",
+"嚣"=>"囂",
+"园"=>"園",
+"囱"=>"囪",
+"围"=>"圍",
+"囵"=>"圇",
+"国"=>"國",
+"图"=>"圖",
+"圆"=>"圓",
+"圣"=>"聖",
+"圹"=>"壙",
+"场"=>"場",
+"坂"=>"阪",
+"块"=>"塊",
+"坚"=>"堅",
+"坜"=>"壢",
+"坝"=>"壩",
+"坞"=>"塢",
+"坟"=>"墳",
+"坠"=>"墜",
+"垄"=>"壟",
+"垅"=>"壠",
+"垆"=>"壚",
+"垒"=>"壘",
+"垦"=>"墾",
+"垩"=>"堊",
+"垫"=>"墊",
+"垭"=>"埡",
+"垱"=>"壋",
+"垲"=>"塏",
+"垴"=>"堖",
+"埘"=>"塒",
+"埙"=>"塤",
+"埚"=>"堝",
+"埯"=>"垵",
+"堑"=>"塹",
+"堕"=>"墮",
+"𡒄"=>"壈",
+"壮"=>"壯",
+"声"=>"聲",
+"壶"=>"壺",
+"壸"=>"壼",
+"处"=>"處",
+"备"=>"備",
+"够"=>"夠",
+"头"=>"頭",
+"夸"=>"誇",
+"夹"=>"夾",
+"夺"=>"奪",
+"奁"=>"奩",
+"奂"=>"奐",
+"奋"=>"奮",
+"奥"=>"奧",
+"奸"=>"姦",
+"妆"=>"妝",
+"妇"=>"婦",
+"妈"=>"媽",
+"妩"=>"嫵",
+"妪"=>"嫗",
+"姗"=>"姍",
+"姹"=>"奼",
+"娄"=>"婁",
+"娅"=>"婭",
+"娆"=>"嬈",
+"娇"=>"嬌",
+"娈"=>"孌",
+"娱"=>"娛",
+"娲"=>"媧",
+"娴"=>"嫻",
+"婳"=>"嫿",
+"婴"=>"嬰",
+"婵"=>"嬋",
+"婶"=>"嬸",
+"媪"=>"媼",
+"嫒"=>"嬡",
+"嫔"=>"嬪",
+"嫱"=>"嬙",
+"嬷"=>"嬤",
+"孙"=>"孫",
+"学"=>"學",
+"孪"=>"孿",
+"宝"=>"寶",
+"实"=>"實",
+"宠"=>"寵",
+"审"=>"審",
+"宪"=>"憲",
+"宫"=>"宮",
+"宽"=>"寬",
+"宾"=>"賓",
+"寝"=>"寢",
+"对"=>"對",
+"寻"=>"尋",
+"导"=>"導",
+"寿"=>"壽",
+"将"=>"將",
+"尔"=>"爾",
+"尘"=>"塵",
+"尝"=>"嘗",
+"尧"=>"堯",
+"尴"=>"尷",
+"尸"=>"屍",
+"层"=>"層",
+"屃"=>"屓",
+"屉"=>"屜",
+"届"=>"屆",
+"属"=>"屬",
+"屡"=>"屢",
+"屦"=>"屨",
+"屿"=>"嶼",
+"岁"=>"歲",
+"岂"=>"豈",
+"岖"=>"嶇",
+"岗"=>"崗",
+"岘"=>"峴",
+"岙"=>"嶴",
+"岚"=>"嵐",
+"岛"=>"島",
+"岭"=>"嶺",
+"岽"=>"崬",
+"岿"=>"巋",
+"峄"=>"嶧",
+"峡"=>"峽",
+"峣"=>"嶢",
+"峤"=>"嶠",
+"峥"=>"崢",
+"峦"=>"巒",
+"崂"=>"嶗",
+"崃"=>"崍",
+"崄"=>"嶮",
+"崭"=>"嶄",
+"嵘"=>"嶸",
+"嵚"=>"嶔",
+"嵝"=>"嶁",
+"巅"=>"巔",
+"巩"=>"鞏",
+"巯"=>"巰",
+"币"=>"幣",
+"帅"=>"帥",
+"师"=>"師",
+"帏"=>"幃",
+"帐"=>"帳",
+"帜"=>"幟",
+"带"=>"帶",
+"帧"=>"幀",
+"帮"=>"幫",
+"帱"=>"幬",
+"帻"=>"幘",
+"帼"=>"幗",
+"幂"=>"冪",
+"幺"=>"么",
+"庄"=>"莊",
+"庆"=>"慶",
+"庐"=>"廬",
+"庑"=>"廡",
+"库"=>"庫",
+"应"=>"應",
+"庙"=>"廟",
+"庞"=>"龐",
+"废"=>"廢",
+"廪"=>"廩",
+"开"=>"開",
+"异"=>"異",
+"弃"=>"棄",
+"弑"=>"弒",
+"张"=>"張",
+"弪"=>"弳",
+"弯"=>"彎",
+"弹"=>"彈",
+"强"=>"強",
+"归"=>"歸",
+"彝"=>"彞",
+"彦"=>"彥",
+"彻"=>"徹",
+"径"=>"徑",
+"徕"=>"徠",
+"忆"=>"憶",
+"忏"=>"懺",
+"忧"=>"憂",
+"忾"=>"愾",
+"怀"=>"懷",
+"态"=>"態",
+"怂"=>"慫",
+"怃"=>"憮",
+"怄"=>"慪",
+"怅"=>"悵",
+"怆"=>"愴",
+"怜"=>"憐",
+"总"=>"總",
+"怼"=>"懟",
+"怿"=>"懌",
+"恋"=>"戀",
+"恒"=>"恆",
+"恳"=>"懇",
+"恸"=>"慟",
+"恹"=>"懨",
+"恺"=>"愷",
+"恻"=>"惻",
+"恼"=>"惱",
+"恽"=>"惲",
+"悦"=>"悅",
+"悬"=>"懸",
+"悭"=>"慳",
+"悮"=>"悞",
+"悯"=>"憫",
+"惊"=>"驚",
+"惧"=>"懼",
+"惨"=>"慘",
+"惩"=>"懲",
+"惫"=>"憊",
+"惬"=>"愜",
+"惭"=>"慚",
+"惮"=>"憚",
+"惯"=>"慣",
+"愠"=>"慍",
+"愤"=>"憤",
+"愦"=>"憒",
+"慑"=>"懾",
+"懑"=>"懣",
+"懒"=>"懶",
+"懔"=>"懍",
+"戆"=>"戇",
+"戋"=>"戔",
+"戏"=>"戲",
+"戗"=>"戧",
+"战"=>"戰",
+"戬"=>"戩",
+"戯"=>"戱",
+"户"=>"戶",
+"扑"=>"撲",
+"执"=>"執",
+"扩"=>"擴",
+"扪"=>"捫",
+"扫"=>"掃",
+"扬"=>"揚",
+"扰"=>"擾",
+"抚"=>"撫",
+"抛"=>"拋",
+"抟"=>"摶",
+"抠"=>"摳",
+"抡"=>"掄",
+"抢"=>"搶",
+"护"=>"護",
+"报"=>"報",
+"拟"=>"擬",
+"拢"=>"攏",
+"拣"=>"揀",
+"拥"=>"擁",
+"拦"=>"攔",
+"拧"=>"擰",
+"拨"=>"撥",
+"择"=>"擇",
+"挂"=>"掛",
+"挚"=>"摯",
+"挛"=>"攣",
+"挜"=>"掗",
+"挝"=>"撾",
+"挞"=>"撻",
+"挟"=>"挾",
+"挠"=>"撓",
+"挡"=>"擋",
+"挢"=>"撟",
+"挣"=>"掙",
+"挤"=>"擠",
+"挥"=>"揮",
+"挦"=>"撏",
+"挽"=>"輓",
+"捝"=>"挩",
+"捞"=>"撈",
+"损"=>"損",
+"捡"=>"撿",
+"换"=>"換",
+"捣"=>"搗",
+"掳"=>"擄",
+"掴"=>"摑",
+"掷"=>"擲",
+"掸"=>"撣",
+"掺"=>"摻",
+"掼"=>"摜",
+"揽"=>"攬",
+"揾"=>"搵",
+"揿"=>"撳",
+"搀"=>"攙",
+"搁"=>"擱",
+"搂"=>"摟",
+"搅"=>"攪",
+"携"=>"攜",
+"摄"=>"攝",
+"摅"=>"攄",
+"摆"=>"擺",
+"摇"=>"搖",
+"摈"=>"擯",
+"摊"=>"攤",
+"撄"=>"攖",
+"撑"=>"撐",
+"㧑"=>"撝",
+"撵"=>"攆",
+"撷"=>"擷",
+"撸"=>"擼",
+"撺"=>"攛",
+"㧟"=>"擓",
+"擞"=>"擻",
+"攒"=>"攢",
+"敌"=>"敵",
+"敛"=>"斂",
+"数"=>"數",
+"斋"=>"齋",
+"斓"=>"斕",
+"斩"=>"斬",
+"断"=>"斷",
+"无"=>"無",
+"旧"=>"舊",
+"时"=>"時",
+"旷"=>"曠",
+"旸"=>"暘",
+"昙"=>"曇",
+"昼"=>"晝",
+"昽"=>"曨",
+"显"=>"顯",
+"晋"=>"晉",
+"晒"=>"曬",
+"晓"=>"曉",
+"晔"=>"曄",
+"晕"=>"暈",
+"晖"=>"暉",
+"暂"=>"暫",
+"暧"=>"曖",
+"机"=>"機",
+"杀"=>"殺",
+"杂"=>"雜",
+"权"=>"權",
+"杆"=>"桿",
+"条"=>"條",
+"来"=>"來",
+"杨"=>"楊",
+"杩"=>"榪",
+"杰"=>"傑",
+"构"=>"構",
+"枞"=>"樅",
+"枢"=>"樞",
+"枣"=>"棗",
+"枥"=>"櫪",
+"枧"=>"梘",
+"枨"=>"棖",
+"枪"=>"槍",
+"枫"=>"楓",
+"枭"=>"梟",
+"柠"=>"檸",
+"柽"=>"檉",
+"栀"=>"梔",
+"栅"=>"柵",
+"标"=>"標",
+"栈"=>"棧",
+"栉"=>"櫛",
+"栊"=>"櫳",
+"栋"=>"棟",
+"栌"=>"櫨",
+"栎"=>"櫟",
+"栏"=>"欄",
+"树"=>"樹",
+"栖"=>"棲",
+"栗"=>"慄",
+"样"=>"樣",
+"栾"=>"欒",
+"桠"=>"椏",
+"桡"=>"橈",
+"桢"=>"楨",
+"档"=>"檔",
+"桤"=>"榿",
+"桥"=>"橋",
+"桦"=>"樺",
+"桧"=>"檜",
+"桨"=>"槳",
+"桩"=>"樁",
+"梦"=>"夢",
+"梼"=>"檮",
+"梾"=>"棶",
+"梿"=>"槤",
+"检"=>"檢",
+"棁"=>"梲",
+"棂"=>"欞",
+"椁"=>"槨",
+"椟"=>"櫝",
+"椠"=>"槧",
+"椤"=>"欏",
+"椭"=>"橢",
+"楼"=>"樓",
+"榄"=>"欖",
+"榅"=>"榲",
+"榇"=>"櫬",
+"榈"=>"櫚",
+"榉"=>"櫸",
+"槚"=>"檟",
+"槛"=>"檻",
+"槟"=>"檳",
+"槠"=>"櫧",
+"横"=>"橫",
+"樯"=>"檣",
+"樱"=>"櫻",
+"橥"=>"櫫",
+"橱"=>"櫥",
+"橹"=>"櫓",
+"橼"=>"櫞",
+"檩"=>"檁",
+"欢"=>"歡",
+"欤"=>"歟",
+"欧"=>"歐",
+"歼"=>"殲",
+"殁"=>"歿",
+"殇"=>"殤",
+"残"=>"殘",
+"殒"=>"殞",
+"殓"=>"殮",
+"殚"=>"殫",
+"殡"=>"殯",
+"㱮"=>"殨",
+"殴"=>"毆",
+"毁"=>"毀",
+"毂"=>"轂",
+"毕"=>"畢",
+"毙"=>"斃",
+"毡"=>"氈",
+"毵"=>"毿",
+"氇"=>"氌",
+"气"=>"氣",
+"氢"=>"氫",
+"氩"=>"氬",
+"氲"=>"氳",
+"汉"=>"漢",
+"汤"=>"湯",
+"汹"=>"洶",
+"沟"=>"溝",
+"没"=>"沒",
+"沣"=>"灃",
+"沤"=>"漚",
+"沥"=>"瀝",
+"沦"=>"淪",
+"沧"=>"滄",
+"沪"=>"滬",
+"泞"=>"濘",
+"注"=>"註",
+"泪"=>"淚",
+"泶"=>"澩",
+"泷"=>"瀧",
+"泸"=>"瀘",
+"泺"=>"濼",
+"泻"=>"瀉",
+"泼"=>"潑",
+"泽"=>"澤",
+"泾"=>"涇",
+"洁"=>"潔",
+"洒"=>"灑",
+"洼"=>"窪",
+"浃"=>"浹",
+"浅"=>"淺",
+"浆"=>"漿",
+"浇"=>"澆",
+"浈"=>"湞",
+"浊"=>"濁",
+"测"=>"測",
+"浍"=>"澮",
+"济"=>"濟",
+"浏"=>"瀏",
+"浐"=>"滻",
+"浑"=>"渾",
+"浒"=>"滸",
+"浓"=>"濃",
+"浔"=>"潯",
+"涛"=>"濤",
+"涝"=>"澇",
+"涞"=>"淶",
+"涟"=>"漣",
+"涠"=>"潿",
+"涡"=>"渦",
+"涣"=>"渙",
+"涤"=>"滌",
+"润"=>"潤",
+"涧"=>"澗",
+"涨"=>"漲",
+"涩"=>"澀",
+"渊"=>"淵",
+"渌"=>"淥",
+"渍"=>"漬",
+"渎"=>"瀆",
+"渐"=>"漸",
+"渑"=>"澠",
+"渔"=>"漁",
+"渖"=>"瀋",
+"渗"=>"滲",
+"温"=>"溫",
+"湾"=>"灣",
+"湿"=>"濕",
+"溃"=>"潰",
+"溅"=>"濺",
+"溆"=>"漵",
+"滗"=>"潷",
+"滚"=>"滾",
+"滞"=>"滯",
+"滟"=>"灧",
+"滠"=>"灄",
+"满"=>"滿",
+"滢"=>"瀅",
+"滤"=>"濾",
+"滥"=>"濫",
+"滦"=>"灤",
+"滨"=>"濱",
+"滩"=>"灘",
+"滪"=>"澦",
+"漤"=>"灠",
+"潆"=>"瀠",
+"潇"=>"瀟",
+"潋"=>"瀲",
+"潍"=>"濰",
+"潜"=>"潛",
+"潴"=>"瀦",
+"澜"=>"瀾",
+"濑"=>"瀨",
+"濒"=>"瀕",
+"灏"=>"灝",
+"灭"=>"滅",
+"灯"=>"燈",
+"灵"=>"靈",
+"灶"=>"竈",
+"灾"=>"災",
+"灿"=>"燦",
+"炀"=>"煬",
+"炉"=>"爐",
+"炖"=>"燉",
+"炜"=>"煒",
+"炝"=>"熗",
+"点"=>"點",
+"炼"=>"煉",
+"炽"=>"熾",
+"烁"=>"爍",
+"烂"=>"爛",
+"烃"=>"烴",
+"烛"=>"燭",
+"烟"=>"煙",
+"烦"=>"煩",
+"烧"=>"燒",
+"烨"=>"燁",
+"烩"=>"燴",
+"烫"=>"燙",
+"烬"=>"燼",
+"热"=>"熱",
+"焕"=>"煥",
+"焖"=>"燜",
+"焘"=>"燾",
+"煴"=>"熅",
+"爱"=>"愛",
+"爷"=>"爺",
+"牍"=>"牘",
+"牦"=>"氂",
+"牵"=>"牽",
+"牺"=>"犧",
+"犊"=>"犢",
+"状"=>"狀",
+"犷"=>"獷",
+"犸"=>"獁",
+"犹"=>"猶",
+"狈"=>"狽",
+"狝"=>"獮",
+"狞"=>"獰",
+"独"=>"獨",
+"狭"=>"狹",
+"狮"=>"獅",
+"狯"=>"獪",
+"狰"=>"猙",
+"狱"=>"獄",
+"狲"=>"猻",
+"猃"=>"獫",
+"猎"=>"獵",
+"猕"=>"獼",
+"猡"=>"玀",
+"猪"=>"豬",
+"猫"=>"貓",
+"猬"=>"蝟",
+"献"=>"獻",
+"獭"=>"獺",
+"玑"=>"璣",
+"玚"=>"瑒",
+"玛"=>"瑪",
+"玮"=>"瑋",
+"环"=>"環",
+"现"=>"現",
+"玱"=>"瑲",
+"玺"=>"璽",
+"珐"=>"琺",
+"珑"=>"瓏",
+"珰"=>"璫",
+"珲"=>"琿",
+"琏"=>"璉",
+"琐"=>"瑣",
+"琼"=>"瓊",
+"瑶"=>"瑤",
+"瑷"=>"璦",
+"璎"=>"瓔",
+"瓒"=>"瓚",
+"瓯"=>"甌",
+"电"=>"電",
+"画"=>"畫",
+"畅"=>"暢",
+"畴"=>"疇",
+"疖"=>"癤",
+"疗"=>"療",
+"疟"=>"瘧",
+"疠"=>"癘",
+"疡"=>"瘍",
+"疬"=>"癧",
+"疭"=>"瘲",
+"疮"=>"瘡",
+"疯"=>"瘋",
+"疱"=>"皰",
+"疴"=>"痾",
+"痈"=>"癰",
+"痉"=>"痙",
+"痒"=>"癢",
+"痖"=>"瘂",
+"痨"=>"癆",
+"痪"=>"瘓",
+"痫"=>"癇",
+"瘅"=>"癉",
+"瘆"=>"瘮",
+"瘗"=>"瘞",
+"瘪"=>"癟",
+"瘫"=>"癱",
+"瘾"=>"癮",
+"瘿"=>"癭",
+"癞"=>"癩",
+"癣"=>"癬",
+"癫"=>"癲",
+"皑"=>"皚",
+"皱"=>"皺",
+"皲"=>"皸",
+"盏"=>"盞",
+"盐"=>"鹽",
+"监"=>"監",
+"盖"=>"蓋",
+"盗"=>"盜",
+"盘"=>"盤",
+"眍"=>"瞘",
+"眦"=>"眥",
+"眬"=>"矓",
+"着"=>"著",
+"睁"=>"睜",
+"睐"=>"睞",
+"睑"=>"瞼",
+"瞆"=>"瞶",
+"瞒"=>"瞞",
+"䁖"=>"瞜",
+"瞩"=>"矚",
+"矫"=>"矯",
+"矶"=>"磯",
+"矾"=>"礬",
+"矿"=>"礦",
+"砀"=>"碭",
+"码"=>"碼",
+"砖"=>"磚",
+"砗"=>"硨",
+"砚"=>"硯",
+"砜"=>"碸",
+"砺"=>"礪",
+"砻"=>"礱",
+"砾"=>"礫",
+"础"=>"礎",
+"硁"=>"硜",
+"硕"=>"碩",
+"硖"=>"硤",
+"硗"=>"磽",
+"硙"=>"磑",
+"碍"=>"礙",
+"碛"=>"磧",
+"碜"=>"磣",
+"碱"=>"鹼",
+"礼"=>"禮",
+"祃"=>"禡",
+"祎"=>"禕",
+"祢"=>"禰",
+"祯"=>"禎",
+"祷"=>"禱",
+"祸"=>"禍",
+"禀"=>"稟",
+"禄"=>"祿",
+"禅"=>"禪",
+"离"=>"離",
+"秃"=>"禿",
+"秆"=>"稈",
+"积"=>"積",
+"称"=>"稱",
+"秽"=>"穢",
+"秾"=>"穠",
+"稆"=>"穭",
+"税"=>"稅",
+"稣"=>"穌",
+"稳"=>"穩",
+"穑"=>"穡",
+"穷"=>"窮",
+"窃"=>"竊",
+"窍"=>"竅",
+"窎"=>"窵",
+"窑"=>"窯",
+"窜"=>"竄",
+"窝"=>"窩",
+"窥"=>"窺",
+"窦"=>"竇",
+"窭"=>"窶",
+"竞"=>"競",
+"笃"=>"篤",
+"笋"=>"筍",
+"笔"=>"筆",
+"笕"=>"筧",
+"笺"=>"箋",
+"笼"=>"籠",
+"笾"=>"籩",
+"筚"=>"篳",
+"筛"=>"篩",
+"筜"=>"簹",
+"筝"=>"箏",
+"䇲"=>"筴",
+"筹"=>"籌",
+"筼"=>"篔",
+"简"=>"簡",
+"箓"=>"籙",
+"箦"=>"簀",
+"箧"=>"篋",
+"箨"=>"籜",
+"箩"=>"籮",
+"箪"=>"簞",
+"箫"=>"簫",
+"篑"=>"簣",
+"篓"=>"簍",
+"篮"=>"籃",
+"篱"=>"籬",
+"簖"=>"籪",
+"籁"=>"籟",
+"籴"=>"糴",
+"类"=>"類",
+"籼"=>"秈",
+"粜"=>"糶",
+"粝"=>"糲",
+"粤"=>"粵",
+"粪"=>"糞",
+"粮"=>"糧",
+"糁"=>"糝",
+"糇"=>"餱",
+"紧"=>"緊",
+"䌷"=>"紬",
+"䌹"=>"絅",
+"絷"=>"縶",
+"䌸"=>"縳",
+"䍁"=>"繸",
+"纟"=>"糹",
+"纠"=>"糾",
+"纡"=>"紆",
+"红"=>"紅",
+"纣"=>"紂",
+"纥"=>"紇",
+"约"=>"約",
+"级"=>"級",
+"纨"=>"紈",
+"纩"=>"纊",
+"纪"=>"紀",
+"纫"=>"紉",
+"纬"=>"緯",
+"纭"=>"紜",
+"纮"=>"紘",
+"纯"=>"純",
+"纰"=>"紕",
+"纱"=>"紗",
+"纲"=>"綱",
+"纳"=>"納",
+"纴"=>"紝",
+"纵"=>"縱",
+"纶"=>"綸",
+"纷"=>"紛",
+"纸"=>"紙",
+"纹"=>"紋",
+"纺"=>"紡",
+"纻"=>"紵",
+"纼"=>"紖",
+"纽"=>"紐",
+"纾"=>"紓",
+"绀"=>"紺",
+"绁"=>"紲",
+"绂"=>"紱",
+"练"=>"練",
+"组"=>"組",
+"绅"=>"紳",
+"细"=>"細",
+"织"=>"織",
+"终"=>"終",
+"绉"=>"縐",
+"绊"=>"絆",
+"绋"=>"紼",
+"绌"=>"絀",
+"绍"=>"紹",
+"绎"=>"繹",
+"经"=>"經",
+"绐"=>"紿",
+"绑"=>"綁",
+"绒"=>"絨",
+"结"=>"結",
+"绔"=>"絝",
+"绕"=>"繞",
+"绖"=>"絰",
+"绗"=>"絎",
+"绘"=>"繪",
+"给"=>"給",
+"绚"=>"絢",
+"绛"=>"絳",
+"络"=>"絡",
+"绞"=>"絞",
+"统"=>"統",
+"绠"=>"綆",
+"绡"=>"綃",
+"绢"=>"絹",
+"绤"=>"綌",
+"绥"=>"綏",
+"继"=>"繼",
+"绨"=>"綈",
+"绩"=>"績",
+"绪"=>"緒",
+"绫"=>"綾",
+"绬"=>"緓",
+"续"=>"續",
+"绮"=>"綺",
+"绯"=>"緋",
+"绰"=>"綽",
+"绲"=>"緄",
+"绳"=>"繩",
+"维"=>"維",
+"绵"=>"綿",
+"绶"=>"綬",
+"绸"=>"綢",
+"绹"=>"綯",
+"绺"=>"綹",
+"绻"=>"綣",
+"综"=>"綜",
+"绽"=>"綻",
+"绾"=>"綰",
+"缀"=>"綴",
+"缁"=>"緇",
+"缂"=>"緙",
+"缃"=>"緗",
+"缄"=>"緘",
+"缅"=>"緬",
+"缆"=>"纜",
+"缇"=>"緹",
+"缈"=>"緲",
+"缉"=>"緝",
+"缊"=>"縕",
+"缋"=>"繢",
+"缌"=>"緦",
+"缍"=>"綞",
+"缎"=>"緞",
+"缏"=>"緶",
+"缑"=>"緱",
+"缒"=>"縋",
+"缓"=>"緩",
+"缔"=>"締",
+"缕"=>"縷",
+"编"=>"編",
+"缗"=>"緡",
+"缘"=>"緣",
+"缙"=>"縉",
+"缚"=>"縛",
+"缛"=>"縟",
+"缜"=>"縝",
+"缝"=>"縫",
+"缞"=>"縗",
+"缟"=>"縞",
+"缠"=>"纏",
+"缡"=>"縭",
+"缢"=>"縊",
+"缣"=>"縑",
+"缤"=>"繽",
+"缥"=>"縹",
+"缦"=>"縵",
+"缧"=>"縲",
+"缨"=>"纓",
+"缩"=>"縮",
+"缪"=>"繆",
+"缫"=>"繅",
+"缬"=>"纈",
+"缭"=>"繚",
+"缮"=>"繕",
+"缯"=>"繒",
+"缱"=>"繾",
+"缲"=>"繰",
+"缳"=>"繯",
+"缴"=>"繳",
+"缵"=>"纘",
+"罂"=>"罌",
+"网"=>"網",
+"罗"=>"羅",
+"罚"=>"罰",
+"罢"=>"罷",
+"罴"=>"羆",
+"羁"=>"羈",
+"羟"=>"羥",
+"翘"=>"翹",
+"耢"=>"耮",
+"耧"=>"耬",
+"耸"=>"聳",
+"耻"=>"恥",
+"聂"=>"聶",
+"聋"=>"聾",
+"职"=>"職",
+"聍"=>"聹",
+"联"=>"聯",
+"聩"=>"聵",
+"聪"=>"聰",
+"肃"=>"肅",
+"肠"=>"腸",
+"肤"=>"膚",
+"肮"=>"骯",
+"肴"=>"餚",
+"肾"=>"腎",
+"肿"=>"腫",
+"胀"=>"脹",
+"胁"=>"脅",
+"胆"=>"膽",
+"胧"=>"朧",
+"胨"=>"腖",
+"胪"=>"臚",
+"胫"=>"脛",
+"胶"=>"膠",
+"脉"=>"脈",
+"脍"=>"膾",
+"脐"=>"臍",
+"脑"=>"腦",
+"脓"=>"膿",
+"脔"=>"臠",
+"脚"=>"腳",
+"脱"=>"脫",
+"脶"=>"腡",
+"脸"=>"臉",
+"腭"=>"齶",
+"腻"=>"膩",
+"腼"=>"靦",
+"腽"=>"膃",
+"腾"=>"騰",
+"膑"=>"臏",
+"臜"=>"臢",
+"舆"=>"輿",
+"舣"=>"艤",
+"舰"=>"艦",
+"舱"=>"艙",
+"舻"=>"艫",
+"艰"=>"艱",
+"艳"=>"艷",
+"艺"=>"藝",
+"节"=>"節",
+"芈"=>"羋",
+"芗"=>"薌",
+"芜"=>"蕪",
+"芦"=>"蘆",
+"苁"=>"蓯",
+"苇"=>"葦",
+"苈"=>"藶",
+"苋"=>"莧",
+"苌"=>"萇",
+"苍"=>"蒼",
+"苎"=>"苧",
+"茎"=>"莖",
+"茏"=>"蘢",
+"茑"=>"蔦",
+"茔"=>"塋",
+"茕"=>"煢",
+"茧"=>"繭",
+"荆"=>"荊",
+"荐"=>"薦",
+"荙"=>"薘",
+"荚"=>"莢",
+"荛"=>"蕘",
+"荜"=>"蓽",
+"荞"=>"蕎",
+"荟"=>"薈",
+"荠"=>"薺",
+"荡"=>"蕩",
+"荣"=>"榮",
+"荤"=>"葷",
+"荥"=>"滎",
+"荦"=>"犖",
+"荧"=>"熒",
+"荨"=>"蕁",
+"荩"=>"藎",
+"荪"=>"蓀",
+"荫"=>"蔭",
+"荬"=>"蕒",
+"荭"=>"葒",
+"荮"=>"葤",
+"莅"=>"蒞",
+"莱"=>"萊",
+"莲"=>"蓮",
+"莳"=>"蒔",
+"莴"=>"萵",
+"莶"=>"薟",
+"莸"=>"蕕",
+"莹"=>"瑩",
+"莺"=>"鶯",
+"萝"=>"蘿",
+"萤"=>"螢",
+"营"=>"營",
+"萦"=>"縈",
+"萧"=>"蕭",
+"萨"=>"薩",
+"葱"=>"蔥",
+"蒇"=>"蕆",
+"蒉"=>"蕢",
+"蒋"=>"蔣",
+"蒌"=>"蔞",
+"蓝"=>"藍",
+"蓟"=>"薊",
+"蓠"=>"蘺",
+"蓣"=>"蕷",
+"蓥"=>"鎣",
+"蓦"=>"驀",
+"蔂"=>"虆",
+"蔷"=>"薔",
+"蔹"=>"蘞",
+"蔺"=>"藺",
+"蔼"=>"藹",
+"蕰"=>"薀",
+"蕲"=>"蘄",
+"薮"=>"藪",
+"藓"=>"蘚",
+"蘖"=>"櫱",
+"虏"=>"虜",
+"虑"=>"慮",
+"虚"=>"虛",
+"虬"=>"虯",
+"虮"=>"蟣",
+"虽"=>"雖",
+"虾"=>"蝦",
+"虿"=>"蠆",
+"蚀"=>"蝕",
+"蚁"=>"蟻",
+"蚂"=>"螞",
+"蚕"=>"蠶",
+"蚬"=>"蜆",
+"蛊"=>"蠱",
+"蛎"=>"蠣",
+"蛏"=>"蟶",
+"蛮"=>"蠻",
+"蛰"=>"蟄",
+"蛱"=>"蛺",
+"蛲"=>"蟯",
+"蛳"=>"螄",
+"蛴"=>"蠐",
+"蜕"=>"蛻",
+"蜗"=>"蝸",
+"蝇"=>"蠅",
+"蝈"=>"蟈",
+"蝉"=>"蟬",
+"蝼"=>"螻",
+"蝾"=>"蠑",
+"螀"=>"螿",
+"螨"=>"蟎",
+"蟏"=>"蠨",
+"衅"=>"釁",
+"衔"=>"銜",
+"补"=>"補",
+"衬"=>"襯",
+"衮"=>"袞",
+"袄"=>"襖",
+"袅"=>"裊",
+"袆"=>"褘",
+"袜"=>"襪",
+"袭"=>"襲",
+"袯"=>"襏",
+"装"=>"裝",
+"裆"=>"襠",
+"裈"=>"褌",
+"裢"=>"褳",
+"裣"=>"襝",
+"裤"=>"褲",
+"裥"=>"襇",
+"褛"=>"褸",
+"褴"=>"襤",
+"见"=>"見",
+"观"=>"觀",
+"觃"=>"覎",
+"规"=>"規",
+"觅"=>"覓",
+"视"=>"視",
+"觇"=>"覘",
+"览"=>"覽",
+"觉"=>"覺",
+"觊"=>"覬",
+"觋"=>"覡",
+"觌"=>"覿",
+"觍"=>"覥",
+"觎"=>"覦",
+"觏"=>"覯",
+"觐"=>"覲",
+"觑"=>"覷",
+"觞"=>"觴",
+"触"=>"觸",
+"觯"=>"觶",
+"訚"=>"誾",
+"䜣"=>"訢",
+"誉"=>"譽",
+"誊"=>"謄",
+"讠"=>"訁",
+"计"=>"計",
+"订"=>"訂",
+"讣"=>"訃",
+"认"=>"認",
+"讥"=>"譏",
+"讦"=>"訐",
+"讧"=>"訌",
+"讨"=>"討",
+"让"=>"讓",
+"讪"=>"訕",
+"讫"=>"訖",
+"讬"=>"託",
+"训"=>"訓",
+"议"=>"議",
+"讯"=>"訊",
+"记"=>"記",
+"讱"=>"訒",
+"讲"=>"講",
+"讳"=>"諱",
+"讴"=>"謳",
+"讵"=>"詎",
+"讶"=>"訝",
+"讷"=>"訥",
+"许"=>"許",
+"讹"=>"訛",
+"论"=>"論",
+"讻"=>"訩",
+"讼"=>"訟",
+"讽"=>"諷",
+"设"=>"設",
+"访"=>"訪",
+"诀"=>"訣",
+"证"=>"證",
+"诂"=>"詁",
+"诃"=>"訶",
+"评"=>"評",
+"诅"=>"詛",
+"识"=>"識",
+"诇"=>"詗",
+"诈"=>"詐",
+"诉"=>"訴",
+"诊"=>"診",
+"诋"=>"詆",
+"诌"=>"謅",
+"词"=>"詞",
+"诎"=>"詘",
+"诏"=>"詔",
+"诐"=>"詖",
+"译"=>"譯",
+"诒"=>"詒",
+"诓"=>"誆",
+"诔"=>"誄",
+"试"=>"試",
+"诖"=>"詿",
+"诗"=>"詩",
+"诘"=>"詰",
+"诙"=>"詼",
+"诚"=>"誠",
+"诛"=>"誅",
+"诜"=>"詵",
+"话"=>"話",
+"诞"=>"誕",
+"诟"=>"詬",
+"诠"=>"詮",
+"诡"=>"詭",
+"询"=>"詢",
+"诣"=>"詣",
+"诤"=>"諍",
+"该"=>"該",
+"详"=>"詳",
+"诧"=>"詫",
+"诨"=>"諢",
+"诩"=>"詡",
+"诪"=>"譸",
+"诫"=>"誡",
+"诬"=>"誣",
+"语"=>"語",
+"诮"=>"誚",
+"误"=>"誤",
+"诰"=>"誥",
+"诱"=>"誘",
+"诲"=>"誨",
+"诳"=>"誑",
+"诵"=>"誦",
+"诶"=>"誒",
+"请"=>"請",
+"诸"=>"諸",
+"诹"=>"諏",
+"诺"=>"諾",
+"读"=>"讀",
+"诼"=>"諑",
+"诽"=>"誹",
+"课"=>"課",
+"诿"=>"諉",
+"谀"=>"諛",
+"谁"=>"誰",
+"谂"=>"諗",
+"调"=>"調",
+"谄"=>"諂",
+"谅"=>"諒",
+"谆"=>"諄",
+"谇"=>"誶",
+"谈"=>"談",
+"谊"=>"誼",
+"谋"=>"謀",
+"谌"=>"諶",
+"谍"=>"諜",
+"谎"=>"謊",
+"谏"=>"諫",
+"谐"=>"諧",
+"谑"=>"謔",
+"谒"=>"謁",
+"谓"=>"謂",
+"谔"=>"諤",
+"谕"=>"諭",
+"谖"=>"諼",
+"谗"=>"讒",
+"谘"=>"諮",
+"谙"=>"諳",
+"谚"=>"諺",
+"谛"=>"諦",
+"谜"=>"謎",
+"谝"=>"諞",
+"谞"=>"諝",
+"谟"=>"謨",
+"谠"=>"讜",
+"谡"=>"謖",
+"谢"=>"謝",
+"谤"=>"謗",
+"谥"=>"謚",
+"谦"=>"謙",
+"谧"=>"謐",
+"谨"=>"謹",
+"谩"=>"謾",
+"谪"=>"謫",
+"谬"=>"謬",
+"谭"=>"譚",
+"谮"=>"譖",
+"谯"=>"譙",
+"谰"=>"讕",
+"谱"=>"譜",
+"谲"=>"譎",
+"谳"=>"讞",
+"谴"=>"譴",
+"谵"=>"譫",
+"谶"=>"讖",
+"豮"=>"豶",
+"贝"=>"貝",
+"贞"=>"貞",
+"负"=>"負",
+"贠"=>"貟",
+"贡"=>"貢",
+"财"=>"財",
+"责"=>"責",
+"贤"=>"賢",
+"败"=>"敗",
+"账"=>"賬",
+"货"=>"貨",
+"质"=>"質",
+"贩"=>"販",
+"贪"=>"貪",
+"贫"=>"貧",
+"贬"=>"貶",
+"购"=>"購",
+"贮"=>"貯",
+"贯"=>"貫",
+"贰"=>"貳",
+"贱"=>"賤",
+"贲"=>"賁",
+"贳"=>"貰",
+"贴"=>"貼",
+"贵"=>"貴",
+"贶"=>"貺",
+"贷"=>"貸",
+"贸"=>"貿",
+"费"=>"費",
+"贺"=>"賀",
+"贻"=>"貽",
+"贼"=>"賊",
+"贽"=>"贄",
+"贾"=>"賈",
+"贿"=>"賄",
+"赀"=>"貲",
+"赁"=>"賃",
+"赂"=>"賂",
+"资"=>"資",
+"赅"=>"賅",
+"赆"=>"贐",
+"赇"=>"賕",
+"赈"=>"賑",
+"赉"=>"賚",
+"赊"=>"賒",
+"赋"=>"賦",
+"赌"=>"賭",
+"赎"=>"贖",
+"赏"=>"賞",
+"赐"=>"賜",
+"赑"=>"贔",
+"赒"=>"賙",
+"赓"=>"賡",
+"赔"=>"賠",
+"赕"=>"賧",
+"赖"=>"賴",
+"赗"=>"賵",
+"赘"=>"贅",
+"赙"=>"賻",
+"赚"=>"賺",
+"赛"=>"賽",
+"赜"=>"賾",
+"赞"=>"贊",
+"赟"=>"贇",
+"赠"=>"贈",
+"赡"=>"贍",
+"赢"=>"贏",
+"赣"=>"贛",
+"赪"=>"赬",
+"赵"=>"趙",
+"赶"=>"趕",
+"趋"=>"趨",
+"趱"=>"趲",
+"趸"=>"躉",
+"跃"=>"躍",
+"跄"=>"蹌",
+"跞"=>"躒",
+"践"=>"踐",
+"跶"=>"躂",
+"跷"=>"蹺",
+"跸"=>"蹕",
+"跹"=>"躚",
+"跻"=>"躋",
+"踊"=>"踴",
+"踌"=>"躊",
+"踪"=>"蹤",
+"踬"=>"躓",
+"踯"=>"躑",
+"蹑"=>"躡",
+"蹒"=>"蹣",
+"蹰"=>"躕",
+"蹿"=>"躥",
+"躏"=>"躪",
+"躜"=>"躦",
+"躯"=>"軀",
+"车"=>"車",
+"轧"=>"軋",
+"轨"=>"軌",
+"轩"=>"軒",
+"轪"=>"軑",
+"轫"=>"軔",
+"转"=>"轉",
+"轭"=>"軛",
+"轮"=>"輪",
+"软"=>"軟",
+"轰"=>"轟",
+"轱"=>"軲",
+"轲"=>"軻",
+"轳"=>"轤",
+"轴"=>"軸",
+"轵"=>"軹",
+"轶"=>"軼",
+"轷"=>"軤",
+"轸"=>"軫",
+"轹"=>"轢",
+"轺"=>"軺",
+"轻"=>"輕",
+"轼"=>"軾",
+"载"=>"載",
+"轾"=>"輊",
+"轿"=>"轎",
+"辀"=>"輈",
+"辁"=>"輇",
+"辂"=>"輅",
+"较"=>"較",
+"辄"=>"輒",
+"辅"=>"輔",
+"辆"=>"輛",
+"辇"=>"輦",
+"辈"=>"輩",
+"辉"=>"輝",
+"辊"=>"輥",
+"辋"=>"輞",
+"辌"=>"輬",
+"辍"=>"輟",
+"辎"=>"輜",
+"辏"=>"輳",
+"辐"=>"輻",
+"辑"=>"輯",
+"辒"=>"轀",
+"输"=>"輸",
+"辔"=>"轡",
+"辕"=>"轅",
+"辖"=>"轄",
+"辗"=>"輾",
+"辘"=>"轆",
+"辙"=>"轍",
+"辚"=>"轔",
+"辞"=>"辭",
+"辩"=>"辯",
+"辫"=>"辮",
+"边"=>"邊",
+"辽"=>"遼",
+"达"=>"達",
+"迁"=>"遷",
+"过"=>"過",
+"迈"=>"邁",
+"运"=>"運",
+"还"=>"還",
+"这"=>"這",
+"进"=>"進",
+"远"=>"遠",
+"违"=>"違",
+"连"=>"連",
+"迟"=>"遲",
+"迩"=>"邇",
+"迳"=>"逕",
+"迹"=>"跡",
+"选"=>"選",
+"逊"=>"遜",
+"递"=>"遞",
+"逦"=>"邐",
+"逻"=>"邏",
+"遗"=>"遺",
+"遥"=>"遙",
+"邓"=>"鄧",
+"邝"=>"鄺",
+"邬"=>"鄔",
+"邮"=>"郵",
+"邹"=>"鄒",
+"邺"=>"鄴",
+"邻"=>"鄰",
+"郏"=>"郟",
+"郐"=>"鄶",
+"郑"=>"鄭",
+"郓"=>"鄆",
+"郦"=>"酈",
+"郧"=>"鄖",
+"郸"=>"鄲",
+"酂"=>"酇",
+"酦"=>"醱",
+"酱"=>"醬",
+"酽"=>"釅",
+"酾"=>"釃",
+"酿"=>"釀",
+"释"=>"釋",
+"鉴"=>"鑒",
+"銮"=>"鑾",
+"錾"=>"鏨",
+"钅"=>"釒",
+"钆"=>"釓",
+"钇"=>"釔",
+"针"=>"針",
+"钉"=>"釘",
+"钊"=>"釗",
+"钋"=>"釙",
+"钌"=>"釕",
+"钍"=>"釷",
+"钎"=>"釺",
+"钏"=>"釧",
+"钐"=>"釤",
+"钑"=>"鈒",
+"钒"=>"釩",
+"钓"=>"釣",
+"钔"=>"鍆",
+"钕"=>"釹",
+"钖"=>"鍚",
+"钗"=>"釵",
+"钘"=>"鈃",
+"钙"=>"鈣",
+"钚"=>"鈈",
+"钛"=>"鈦",
+"钜"=>"鉅",
+"钝"=>"鈍",
+"钞"=>"鈔",
+"钠"=>"鈉",
+"钡"=>"鋇",
+"钢"=>"鋼",
+"钣"=>"鈑",
+"钤"=>"鈐",
+"钥"=>"鑰",
+"钦"=>"欽",
+"钧"=>"鈞",
+"钨"=>"鎢",
+"钪"=>"鈧",
+"钫"=>"鈁",
+"钬"=>"鈥",
+"钭"=>"鈄",
+"钮"=>"鈕",
+"钯"=>"鈀",
+"钰"=>"鈺",
+"钱"=>"錢",
+"钲"=>"鉦",
+"钳"=>"鉗",
+"钴"=>"鈷",
+"钶"=>"鈳",
+"钷"=>"鉕",
+"钸"=>"鈽",
+"钹"=>"鈸",
+"钺"=>"鉞",
+"钻"=>"鑽",
+"钼"=>"鉬",
+"钽"=>"鉭",
+"钾"=>"鉀",
+"钿"=>"鈿",
+"铀"=>"鈾",
+"铁"=>"鐵",
+"铂"=>"鉑",
+"铃"=>"鈴",
+"铄"=>"鑠",
+"铅"=>"鉛",
+"铆"=>"鉚",
+"铇"=>"鉋",
+"铈"=>"鈰",
+"铉"=>"鉉",
+"铊"=>"鉈",
+"铋"=>"鉍",
+"铌"=>"鈮",
+"铍"=>"鈹",
+"铎"=>"鐸",
+"铏"=>"鉶",
+"铐"=>"銬",
+"铑"=>"銠",
+"铒"=>"鉺",
+"铓"=>"鋩",
+"铔"=>"錏",
+"铕"=>"銪",
+"铖"=>"鋮",
+"铗"=>"鋏",
+"铘"=>"鋣",
+"铙"=>"鐃",
+"铚"=>"銍",
+"铛"=>"鐺",
+"铜"=>"銅",
+"铝"=>"鋁",
+"铞"=>"銱",
+"铟"=>"銦",
+"铠"=>"鎧",
+"铡"=>"鍘",
+"铢"=>"銖",
+"铣"=>"銑",
+"铤"=>"鋌",
+"铥"=>"銩",
+"铦"=>"銛",
+"铧"=>"鏵",
+"铨"=>"銓",
+"铩"=>"鎩",
+"铪"=>"鉿",
+"铫"=>"銚",
+"铬"=>"鉻",
+"铭"=>"銘",
+"铮"=>"錚",
+"铯"=>"銫",
+"铰"=>"鉸",
+"铱"=>"銥",
+"铲"=>"鏟",
+"铳"=>"銃",
+"铴"=>"鐋",
+"铵"=>"銨",
+"银"=>"銀",
+"铷"=>"銣",
+"铸"=>"鑄",
+"铹"=>"鐒",
+"铺"=>"鋪",
+"铻"=>"鋙",
+"铼"=>"錸",
+"铽"=>"鋱",
+"链"=>"鏈",
+"铿"=>"鏗",
+"销"=>"銷",
+"锁"=>"鎖",
+"锂"=>"鋰",
+"锃"=>"鋥",
+"锄"=>"鋤",
+"锅"=>"鍋",
+"锆"=>"鋯",
+"锇"=>"鋨",
+"锉"=>"銼",
+"锊"=>"鋝",
+"锋"=>"鋒",
+"锌"=>"鋅",
+"锍"=>"鋶",
+"锎"=>"鐦",
+"锏"=>"鐧",
+"锑"=>"銻",
+"锒"=>"鋃",
+"锓"=>"鋟",
+"锔"=>"鋦",
+"锕"=>"錒",
+"锖"=>"錆",
+"锗"=>"鍺",
+"锘"=>"鍩",
+"错"=>"錯",
+"锚"=>"錨",
+"锛"=>"錛",
+"锜"=>"錡",
+"锝"=>"鍀",
+"锞"=>"錁",
+"锟"=>"錕",
+"锠"=>"錩",
+"锡"=>"錫",
+"锢"=>"錮",
+"锣"=>"鑼",
+"锤"=>"錘",
+"锥"=>"錐",
+"锦"=>"錦",
+"锧"=>"鑕",
+"锩"=>"錈",
+"锪"=>"鍃",
+"锫"=>"錇",
+"锬"=>"錟",
+"锭"=>"錠",
+"键"=>"鍵",
+"锯"=>"鋸",
+"锰"=>"錳",
+"锱"=>"錙",
+"锲"=>"鍥",
+"锳"=>"鍈",
+"锴"=>"鍇",
+"锵"=>"鏘",
+"锶"=>"鍶",
+"锷"=>"鍔",
+"锸"=>"鍤",
+"锹"=>"鍬",
+"锺"=>"鍾",
+"锻"=>"鍛",
+"锼"=>"鎪",
+"锽"=>"鍠",
+"锾"=>"鍰",
+"锿"=>"鎄",
+"镀"=>"鍍",
+"镁"=>"鎂",
+"镂"=>"鏤",
+"镃"=>"鎡",
+"镄"=>"鐨",
+"镅"=>"鎇",
+"镆"=>"鏌",
+"镇"=>"鎮",
+"镈"=>"鎛",
+"镉"=>"鎘",
+"镊"=>"鑷",
+"镋"=>"鎲",
+"镍"=>"鎳",
+"镎"=>"鎿",
+"镏"=>"鎦",
+"镐"=>"鎬",
+"镑"=>"鎊",
+"镒"=>"鎰",
+"镓"=>"鎵",
+"镔"=>"鑌",
+"镕"=>"鎔",
+"镖"=>"鏢",
+"镗"=>"鏜",
+"镘"=>"鏝",
+"镙"=>"鏍",
+"镚"=>"鏰",
+"镛"=>"鏞",
+"镜"=>"鏡",
+"镝"=>"鏑",
+"镞"=>"鏃",
+"镟"=>"鏇",
+"镠"=>"鏐",
+"镡"=>"鐔",
+"镣"=>"鐐",
+"镤"=>"鏷",
+"镥"=>"鑥",
+"镦"=>"鐓",
+"镧"=>"鑭",
+"镨"=>"鐠",
+"镩"=>"鑹",
+"镪"=>"鏹",
+"镫"=>"鐙",
+"镬"=>"鑊",
+"镭"=>"鐳",
+"镮"=>"鐶",
+"镯"=>"鐲",
+"镰"=>"鐮",
+"镱"=>"鐿",
+"镲"=>"鑔",
+"镳"=>"鑣",
+"镴"=>"鑞",
+"镵"=>"鑱",
+"镶"=>"鑲",
+"长"=>"長",
+"门"=>"門",
+"闩"=>"閂",
+"闪"=>"閃",
+"闫"=>"閆",
+"闬"=>"閈",
+"闭"=>"閉",
+"问"=>"問",
+"闯"=>"闖",
+"闰"=>"閏",
+"闱"=>"闈",
+"闲"=>"閑",
+"闳"=>"閎",
+"间"=>"間",
+"闵"=>"閔",
+"闶"=>"閌",
+"闷"=>"悶",
+"闸"=>"閘",
+"闹"=>"鬧",
+"闺"=>"閨",
+"闻"=>"聞",
+"闼"=>"闥",
+"闽"=>"閩",
+"闾"=>"閭",
+"闿"=>"闓",
+"阀"=>"閥",
+"阁"=>"閣",
+"阂"=>"閡",
+"阃"=>"閫",
+"阄"=>"鬮",
+"阆"=>"閬",
+"阇"=>"闍",
+"阈"=>"閾",
+"阉"=>"閹",
+"阊"=>"閶",
+"阋"=>"鬩",
+"阌"=>"閿",
+"阍"=>"閽",
+"阎"=>"閻",
+"阏"=>"閼",
+"阐"=>"闡",
+"阑"=>"闌",
+"阒"=>"闃",
+"阓"=>"闠",
+"阔"=>"闊",
+"阕"=>"闋",
+"阖"=>"闔",
+"阗"=>"闐",
+"阘"=>"闒",
+"阙"=>"闕",
+"阚"=>"闞",
+"阛"=>"闤",
+"队"=>"隊",
+"阳"=>"陽",
+"阴"=>"陰",
+"阵"=>"陣",
+"阶"=>"階",
+"际"=>"際",
+"陆"=>"陸",
+"陇"=>"隴",
+"陈"=>"陳",
+"陉"=>"陘",
+"陕"=>"陝",
+"陧"=>"隉",
+"陨"=>"隕",
+"险"=>"險",
+"随"=>"隨",
+"隐"=>"隱",
+"隶"=>"隸",
+"隽"=>"雋",
+"难"=>"難",
+"雏"=>"雛",
+"雠"=>"讎",
+"雳"=>"靂",
+"雾"=>"霧",
+"霁"=>"霽",
+"霡"=>"霢",
+"霭"=>"靄",
+"靓"=>"靚",
+"静"=>"靜",
+"靥"=>"靨",
+"鞑"=>"韃",
+"鞒"=>"鞽",
+"鞯"=>"韉",
+"韦"=>"韋",
+"韧"=>"韌",
+"韨"=>"韍",
+"韩"=>"韓",
+"韪"=>"韙",
+"韫"=>"韞",
+"韬"=>"韜",
+"韵"=>"韻",
+"页"=>"頁",
+"顶"=>"頂",
+"顷"=>"頃",
+"顸"=>"頇",
+"项"=>"項",
+"顺"=>"順",
+"顼"=>"頊",
+"顽"=>"頑",
+"顾"=>"顧",
+"顿"=>"頓",
+"颀"=>"頎",
+"颁"=>"頒",
+"颂"=>"頌",
+"颃"=>"頏",
+"预"=>"預",
+"颅"=>"顱",
+"领"=>"領",
+"颇"=>"頗",
+"颈"=>"頸",
+"颉"=>"頡",
+"颊"=>"頰",
+"颋"=>"頲",
+"颌"=>"頜",
+"颍"=>"潁",
+"颎"=>"熲",
+"颏"=>"頦",
+"颐"=>"頤",
+"频"=>"頻",
+"颒"=>"頮",
+"颔"=>"頷",
+"颕"=>"頴",
+"颖"=>"穎",
+"颗"=>"顆",
+"题"=>"題",
+"颙"=>"顒",
+"颚"=>"顎",
+"颛"=>"顓",
+"额"=>"額",
+"颞"=>"顳",
+"颟"=>"顢",
+"颠"=>"顛",
+"颡"=>"顙",
+"颢"=>"顥",
+"颤"=>"顫",
+"颥"=>"顬",
+"颦"=>"顰",
+"颧"=>"顴",
+"风"=>"風",
+"飏"=>"颺",
+"飐"=>"颭",
+"飑"=>"颮",
+"飒"=>"颯",
+"飓"=>"颶",
+"飔"=>"颸",
+"飕"=>"颼",
+"飖"=>"颻",
+"飗"=>"飀",
+"飘"=>"飄",
+"飙"=>"飆",
+"飚"=>"飈",
+"飞"=>"飛",
+"飨"=>"饗",
+"餍"=>"饜",
+"饣"=>"飠",
+"饤"=>"飣",
+"饦"=>"飥",
+"饧"=>"餳",
+"饨"=>"飩",
+"饩"=>"餼",
+"饪"=>"飪",
+"饫"=>"飫",
+"饬"=>"飭",
+"饭"=>"飯",
+"饮"=>"飲",
+"饯"=>"餞",
+"饰"=>"飾",
+"饱"=>"飽",
+"饲"=>"飼",
+"饳"=>"飿",
+"饴"=>"飴",
+"饵"=>"餌",
+"饶"=>"饒",
+"饷"=>"餉",
+"饸"=>"餄",
+"饹"=>"餎",
+"饺"=>"餃",
+"饻"=>"餏",
+"饼"=>"餅",
+"饽"=>"餑",
+"饾"=>"餖",
+"饿"=>"餓",
+"馀"=>"餘",
+"馁"=>"餒",
+"馂"=>"餕",
+"馃"=>"餜",
+"馄"=>"餛",
+"馅"=>"餡",
+"馆"=>"館",
+"馇"=>"餷",
+"馈"=>"饋",
+"馉"=>"餶",
+"馊"=>"餿",
+"馋"=>"饞",
+"馌"=>"饁",
+"馍"=>"饃",
+"馎"=>"餺",
+"馏"=>"餾",
+"馐"=>"饈",
+"馑"=>"饉",
+"馒"=>"饅",
+"馓"=>"饊",
+"馔"=>"饌",
+"馕"=>"饢",
+"马"=>"馬",
+"驭"=>"馭",
+"驮"=>"馱",
+"驯"=>"馴",
+"驰"=>"馳",
+"驱"=>"驅",
+"驲"=>"馹",
+"驳"=>"駁",
+"驴"=>"驢",
+"驵"=>"駔",
+"驶"=>"駛",
+"驷"=>"駟",
+"驸"=>"駙",
+"驹"=>"駒",
+"驺"=>"騶",
+"驻"=>"駐",
+"驼"=>"駝",
+"驽"=>"駑",
+"驾"=>"駕",
+"驿"=>"驛",
+"骀"=>"駘",
+"骁"=>"驍",
+"骃"=>"駰",
+"骄"=>"驕",
+"骅"=>"驊",
+"骆"=>"駱",
+"骇"=>"駭",
+"骈"=>"駢",
+"骉"=>"驫",
+"骊"=>"驪",
+"骋"=>"騁",
+"验"=>"驗",
+"骍"=>"騂",
+"骎"=>"駸",
+"骏"=>"駿",
+"骐"=>"騏",
+"骑"=>"騎",
+"骒"=>"騍",
+"骓"=>"騅",
+"骔"=>"騌",
+"骕"=>"驌",
+"骖"=>"驂",
+"骗"=>"騙",
+"骘"=>"騭",
+"骙"=>"騤",
+"骚"=>"騷",
+"骛"=>"騖",
+"骜"=>"驁",
+"骝"=>"騮",
+"骞"=>"騫",
+"骟"=>"騸",
+"骠"=>"驃",
+"骡"=>"騾",
+"骢"=>"驄",
+"骣"=>"驏",
+"骤"=>"驟",
+"骥"=>"驥",
+"骦"=>"驦",
+"骧"=>"驤",
+"髅"=>"髏",
+"髋"=>"髖",
+"髌"=>"髕",
+"鬓"=>"鬢",
+"魇"=>"魘",
+"魉"=>"魎",
+"鱼"=>"魚",
+"鱽"=>"魛",
+"鱾"=>"魢",
+"鱿"=>"魷",
+"鲀"=>"魨",
+"鲁"=>"魯",
+"鲂"=>"魴",
+"鲃"=>"䰾",
+"鲄"=>"魺",
+"鲅"=>"鮁",
+"鲆"=>"鮃",
+"鲈"=>"鱸",
+"鲉"=>"鮋",
+"鲊"=>"鮓",
+"鲋"=>"鮒",
+"鲌"=>"鮊",
+"鲍"=>"鮑",
+"鲎"=>"鱟",
+"鲏"=>"鮍",
+"鲐"=>"鮐",
+"鲑"=>"鮭",
+"鲒"=>"鮚",
+"鲓"=>"鮳",
+"鲔"=>"鮪",
+"鲕"=>"鮞",
+"鲖"=>"鮦",
+"鲗"=>"鰂",
+"鲘"=>"鮜",
+"鲙"=>"鱠",
+"鲚"=>"鱭",
+"鲛"=>"鮫",
+"鲜"=>"鮮",
+"鲝"=>"鮺",
+"鲟"=>"鱘",
+"鲠"=>"鯁",
+"鲡"=>"鱺",
+"鲢"=>"鰱",
+"鲣"=>"鰹",
+"鲤"=>"鯉",
+"鲥"=>"鰣",
+"鲦"=>"鰷",
+"鲧"=>"鯀",
+"鲨"=>"鯊",
+"鲩"=>"鯇",
+"鲪"=>"鮶",
+"鲫"=>"鯽",
+"鲬"=>"鯒",
+"鲭"=>"鯖",
+"鲮"=>"鯪",
+"鲯"=>"鯕",
+"鲰"=>"鯫",
+"鲱"=>"鯡",
+"鲲"=>"鯤",
+"鲳"=>"鯧",
+"鲴"=>"鯝",
+"鲵"=>"鯢",
+"鲶"=>"鯰",
+"鲷"=>"鯛",
+"鲸"=>"鯨",
+"鲹"=>"鰺",
+"鲺"=>"鯴",
+"鲻"=>"鯔",
+"鲼"=>"鱝",
+"鲽"=>"鰈",
+"鲾"=>"鰏",
+"鲿"=>"鱨",
+"鳀"=>"鯷",
+"鳁"=>"鰮",
+"鳂"=>"鰃",
+"鳃"=>"鰓",
+"鳅"=>"鰍",
+"鳆"=>"鰒",
+"鳇"=>"鰉",
+"鳈"=>"鰁",
+"鳉"=>"鱂",
+"鳊"=>"鯿",
+"鳋"=>"鰠",
+"鳌"=>"鰲",
+"鳍"=>"鰭",
+"鳎"=>"鰨",
+"鳏"=>"鰥",
+"鳐"=>"鰩",
+"鳑"=>"鰟",
+"鳒"=>"鰜",
+"鳓"=>"鰳",
+"鳔"=>"鰾",
+"鳕"=>"鱈",
+"鳖"=>"鱉",
+"鳗"=>"鰻",
+"鳘"=>"鰵",
+"鳙"=>"鱅",
+"鳚"=>"䲁",
+"鳛"=>"鰼",
+"鳜"=>"鱖",
+"鳝"=>"鱔",
+"鳞"=>"鱗",
+"鳟"=>"鱒",
+"鳠"=>"鱯",
+"鳡"=>"鱤",
+"鳢"=>"鱧",
+"鳣"=>"鱣",
+"䴓"=>"鳾",
+"䴕"=>"鴷",
+"䴔"=>"鵁",
+"䴖"=>"鶄",
+"䴗"=>"鶪",
+"䴘"=>"鷈",
+"䴙"=>"鷿",
+"鸟"=>"鳥",
+"鸠"=>"鳩",
+"鸢"=>"鳶",
+"鸣"=>"鳴",
+"鸤"=>"鳲",
+"鸥"=>"鷗",
+"鸦"=>"鴉",
+"鸧"=>"鶬",
+"鸨"=>"鴇",
+"鸩"=>"鴆",
+"鸪"=>"鴣",
+"鸫"=>"鶇",
+"鸬"=>"鸕",
+"鸭"=>"鴨",
+"鸮"=>"鴞",
+"鸯"=>"鴦",
+"鸰"=>"鴒",
+"鸱"=>"鴟",
+"鸲"=>"鴝",
+"鸳"=>"鴛",
+"鸴"=>"鷽",
+"鸵"=>"鴕",
+"鸶"=>"鷥",
+"鸷"=>"鷙",
+"鸸"=>"鴯",
+"鸹"=>"鴰",
+"鸺"=>"鵂",
+"鸻"=>"鴴",
+"鸼"=>"鵃",
+"鸽"=>"鴿",
+"鸾"=>"鸞",
+"鸿"=>"鴻",
+"鹀"=>"鵐",
+"鹁"=>"鵓",
+"鹂"=>"鸝",
+"鹃"=>"鵑",
+"鹄"=>"鵠",
+"鹅"=>"鵝",
+"鹆"=>"鵒",
+"鹇"=>"鷳",
+"鹈"=>"鵜",
+"鹉"=>"鵡",
+"鹊"=>"鵲",
+"鹋"=>"鶓",
+"鹌"=>"鵪",
+"鹍"=>"鵾",
+"鹎"=>"鵯",
+"鹏"=>"鵬",
+"鹐"=>"鵮",
+"鹑"=>"鶉",
+"鹒"=>"鶊",
+"鹓"=>"鵷",
+"鹔"=>"鷫",
+"鹕"=>"鶘",
+"鹖"=>"鶡",
+"鹗"=>"鶚",
+"鹘"=>"鶻",
+"鹙"=>"鶖",
+"鹛"=>"鶥",
+"鹜"=>"鶩",
+"鹝"=>"鷊",
+"鹞"=>"鷂",
+"鹟"=>"鶲",
+"鹠"=>"鶹",
+"鹡"=>"鶺",
+"鹢"=>"鷁",
+"鹣"=>"鶼",
+"鹤"=>"鶴",
+"鹥"=>"鷖",
+"鹦"=>"鸚",
+"鹧"=>"鷓",
+"鹨"=>"鷚",
+"鹩"=>"鷯",
+"鹪"=>"鷦",
+"鹫"=>"鷲",
+"鹬"=>"鷸",
+"鹭"=>"鷺",
+"鹯"=>"鸇",
+"鹰"=>"鷹",
+"鹱"=>"鸌",
+"鹲"=>"鸏",
+"鹳"=>"鸛",
+"鹴"=>"鸘",
+"鹾"=>"鹺",
+"麦"=>"麥",
+"麸"=>"麩",
+"麽"=>"麼",
+"黄"=>"黃",
+"黉"=>"黌",
+"黡"=>"黶",
+"黩"=>"黷",
+"黪"=>"黲",
+"黾"=>"黽",
+"鼋"=>"黿",
+"鼍"=>"鼉",
+"鼗"=>"鞀",
+"鼹"=>"鼴",
+"齐"=>"齊",
+"齑"=>"齏",
+"齿"=>"齒",
+"龀"=>"齔",
+"龁"=>"齕",
+"龂"=>"齗",
+"龃"=>"齟",
+"龄"=>"齡",
+"龅"=>"齙",
+"龆"=>"齠",
+"龇"=>"齜",
+"龈"=>"齦",
+"龉"=>"齬",
+"龊"=>"齪",
+"龋"=>"齲",
+"龌"=>"齷",
+"龙"=>"龍",
+"龚"=>"龔",
+"龛"=>"龕",
+"龟"=>"龜",
+
+"BIG-" => "BIG-",
+".PRG" => ".PRG",
+"一伙" => "一伙",
+"一并" => "一併",
+"一准" => "一准",
+"一划" => "一划",
+"一地里" => "一地裡",
+"一干" => "一干",
+"一树百获" => "一樹百穫",
+"一台" => "一臺",
+"一冲" => "一衝",
+"一只" => "一隻",
+"一发千钧" => "一髮千鈞",
+"一出" => "一齣",
+"七只" => "七隻",
+"三元里" => "三元裡",
+"三国志" => "三國誌",
+"三复" => "三複",
+"三只" => "三隻",
+"上吊" => "上吊",
+"上台" => "上臺",
+"下不了台" => "下不了臺",
+"下台" => "下臺",
+"下面" => "下麵",
+"不准" => "不准",
+"不吊" => "不吊",
+"不干" => "不幹",
+"不舍" => "不捨",
+"不知所云" => "不知所云",
+"不识台举" => "不識檯舉",
+"不锈钢" => "不鏽鋼",
+"丑剧" => "丑劇",
+"丑旦" => "丑旦",
+"丑角" => "丑角",
+"世界杯" => "世界盃",
+"并存着" => "並存著",
+"中岳" => "中嶽",
+"中台路" => "中臺路",
+"中台医专" => "中臺醫專",
+"丰南" => "丰南",
+"丰台" => "丰台",
+"丰姿" => "丰姿",
+"丰神俊朗" => "丰神俊朗",
+"丰采" => "丰采",
+"丰韵" => "丰韻",
+"主干" => "主幹",
+"九世之雠" => "九世之讎",
+"九只" => "九隻",
+"干丝" => "乾絲",
+"干着急" => "乾著急",
+"干面" => "乾麵",
+"乱发" => "亂髮",
+"云云" => "云云",
+"云何" => "云何",
+"云尔" => "云爾",
+"五岳" => "五嶽",
+"五斗柜" => "五斗櫃",
+"五斗橱" => "五斗櫥",
+"五斗米" => "五斗米",
+"五谷" => "五穀",
+"五行生克" => "五行生剋",
+"五只" => "五隻",
+"五出" => "五齣",
+"井里" => "井裡",
+"交卷" => "交卷",
+"人云亦云" => "人云亦云",
+"人物志" => "人物誌",
+"什锦面" => "什錦麵",
+"什么" => "什麼",
+"仆倒" => "仆倒",
+"仇雠" => "仇讎",
+"介系词" => "介係詞",
+"介系词" => "介繫詞",
+"仿制" => "仿製",
+"伙伕" => "伙伕",
+"伙伴" => "伙伴",
+"伙同" => "伙同",
+"伙夫" => "伙夫",
+"伙房" => "伙房",
+"伙计" => "伙計",
+"伙食" => "伙食",
+"布下" => "佈下",
+"布告" => "佈告",
+"布哨" => "佈哨",
+"布局" => "佈局",
+"布岗" => "佈崗",
+"布施" => "佈施",
+"布景" => "佈景",
+"布有" => "佈有",
+"布满" => "佈滿",
+"布线" => "佈線",
+"布置" => "佈置",
+"布署" => "佈署",
+"布道" => "佈道",
+"布达" => "佈達",
+"布防" => "佈防",
+"布阵" => "佈陣",
+"布雷" => "佈雷",
+"体育锻鍊" => "体育鍛鍊",
+"何干" => "何干",
+"作准" => "作准",
+"佣人" => "佣人",
+"佣工" => "佣工",
+"佣金" => "佣金",
+"并入" => "併入",
+"并列" => "併列",
+"并到" => "併到",
+"并合" => "併合",
+"并吞" => "併吞",
+"并在" => "併在",
+"并成" => "併成",
+"并排" => "併排",
+"并拢" => "併攏",
+"并案" => "併案",
+"并为" => "併為",
+"并发" => "併發",
+"并科" => "併科",
+"并购" => "併購",
+"并进" => "併進",
+"来复" => "來複",
+"供制" => "供製",
+"侵并" => "侵併",
+"便辟" => "便辟",
+"系数" => "係數",
+"系为" => "係為",
+"保险柜" => "保險柜",
+"信号台" => "信號臺",
+"修复" => "修複",
+"修胡刀" => "修鬍刀",
+"俯冲" => "俯衝",
+"个里" => "個裡",
+"倒绷孩儿" => "倒繃孩兒",
+"借着" => "借著",
+"偃仆" => "偃仆",
+"假发" => "假髮",
+"停制" => "停製",
+"偷鸡不着" => "偷雞不著",
+"家伙" => "傢伙",
+"家俱" => "傢俱",
+"家具" => "傢具",
+"传布" => "傳佈",
+"债台高筑" => "債臺高築",
+"傻里傻气" => "傻裡傻氣",
+"倾复" => "傾複",
+"倾复" => "傾覆",
+"僱佣" => "僱佣",
+"仪表" => "儀錶",
+"亿只" => "億隻",
+"尽尽" => "儘儘",
+"尽先" => "儘先",
+"尽其所有" => "儘其所有",
+"尽力" => "儘力",
+"尽可能" => "儘可能",
+"尽快" => "儘快",
+"尽早" => "儘早",
+"尽是" => "儘是",
+"尽管" => "儘管",
+"尽速" => "儘速",
+"尽量" => "儘量",
+"允准" => "允准",
+"兄台" => "兄臺",
+"充饥" => "充饑",
+"光采" => "光采",
+"克里" => "克裡",
+"克复" => "克複",
+"入伙" => "入伙",
+"内制" => "內製",
+"两只" => "兩隻",
+"八字胡" => "八字鬍",
+"八只" => "八隻",
+"公布" => "公佈",
+"公干" => "公幹",
+"公斗" => "公斗",
+"公历" => "公曆",
+"公里" => "公裡",
+"六谷" => "六穀",
+"六只" => "六隻",
+"六出" => "六齣",
+"兼并" => "兼併",
+"册卷" => "冊卷",
+"冤雠" => "冤讎",
+"准予" => "准予",
+"准假" => "准假",
+"准定" => "准定",
+"准将" => "准將",
+"准尉" => "准尉",
+"准此" => "准此",
+"准考证" => "准考證",
+"准许" => "准許",
+"几几" => "几几",
+"几杖" => "几杖",
+"几案" => "几案",
+"几筵" => "几筵",
+"几丝" => "几絲",
+"凹洞里" => "凹洞裡",
+"出征" => "出征",
+"函复" => "函覆",
+"刀削面" => "刀削麵",
+"刁斗" => "刁斗",
+"分布" => "分佈",
+"切面" => "切麵",
+"刊布" => "刊佈",
+"划上" => "划上",
+"划下" => "划下",
+"划不来" => "划不來",
+"划了" => "划了",
+"划具" => "划具",
+"划出" => "划出",
+"划到" => "划到",
+"划动" => "划動",
+"划去" => "划去",
+"划子" => "划子",
+"划得来" => "划得來",
+"划拳" => "划拳",
+"划桨" => "划槳",
+"划水" => "划水",
+"划算" => "划算",
+"划船" => "划船",
+"划艇" => "划艇",
+"划着" => "划著",
+"划着走" => "划著走",
+"划行" => "划行",
+"划走" => "划走",
+"划起" => "划起",
+"划进" => "划進",
+"划过" => "划過",
+"初征" => "初征",
+"别致" => "別緻",
+"别着" => "別著",
+"别只" => "別隻",
+"利比里亚" => "利比裡亞",
+"刮着" => "刮著",
+"刮胡刀" => "刮鬍刀",
+"剃发" => "剃髮",
+"剃须" => "剃鬚",
+"削发" => "削髮",
+"克制" => "剋制",
+"克扣" => "剋扣",
+"克日" => "剋日",
+"克星" => "剋星",
+"克服" => "剋服",
+"克期" => "剋期",
+"克死" => "剋死",
+"克薄" => "剋薄",
+"前仆后仰" => "前仆後仰",
+"前仆后继" => "前仆後繼",
+"前台" => "前臺",
+"前车之复" => "前車之覆",
+"刚才" => "剛纔",
+"剥制" => "剝製",
+"剪发" => "剪髮",
+"割舍" => "割捨",
+"创获" => "創穫",
+"创制" => "創製",
+"加里宁" => "加裡寧",
+"劳力士表" => "勞力士錶",
+"包准" => "包准",
+"包谷" => "包穀",
+"匏系" => "匏繫",
+"北岳" => "北嶽",
+"北斗" => "北斗",
+"北回" => "北迴",
+"匡复" => "匡複",
+"匪干" => "匪幹",
+"十卷" => "十卷",
+"十干" => "十干",
+"十台" => "十臺",
+"十只" => "十隻",
+"十出" => "十齣",
+"千百只" => "千百隻",
+"千丝万缕" => "千絲萬縷",
+"千回百折" => "千迴百折",
+"千回百转" => "千迴百轉",
+"千钧一发" => "千鈞一髮",
+"千只" => "千隻",
+"升斗小民" => "升斗小民",
+"半只" => "半隻",
+"南岳" => "南嶽",
+"南征" => "南征",
+"南斗" => "南斗",
+"南台" => "南臺",
+"南回" => "南迴",
+"卡里" => "卡裡",
+"印制" => "印製",
+"卷入" => "卷入",
+"卷取" => "卷取",
+"卷土重来" => "卷土重來",
+"卷子" => "卷子",
+"卷宗" => "卷宗",
+"卷尺" => "卷尺",
+"卷层云" => "卷層雲",
+"卷帙" => "卷帙",
+"卷扬机" => "卷揚機",
+"卷曲" => "卷曲",
+"卷染" => "卷染",
+"卷烟" => "卷煙",
+"卷筒" => "卷筒",
+"卷纬" => "卷緯",
+"卷绕" => "卷繞",
+"卷舌" => "卷舌",
+"卷装" => "卷裝",
+"卷轴" => "卷軸",
+"卷云" => "卷雲",
+"卷领" => "卷領",
+"卷发" => "卷髮",
+"卷须" => "卷鬚",
+"厚朴" => "厚朴",
+"参与" => "參与",
+"参与者" => "參与者",
+"参合" => "參合",
+"参考价值" => "參考價值",
+"参与" => "參與",
+"参与人员" => "參與人員",
+"参与制" => "參與制",
+"参与感" => "參與感",
+"参与者" => "參與者",
+"参观团" => "參觀團",
+"参观团体" => "參觀團體",
+"参阅" => "參閱",
+"反冲" => "反衝",
+"反复" => "反複",
+"反复" => "反覆",
+"取舍" => "取捨",
+"口里" => "口裡",
+"古柯咸" => "古柯鹹",
+"只准" => "只准",
+"只冲" => "只衝",
+"叮当" => "叮噹",
+"可怜虫" => "可憐虫",
+"可紧可松" => "可緊可鬆",
+"台制" => "台製",
+"司令台" => "司令臺",
+"吃着不尽" => "吃著不盡",
+"吃里扒外" => "吃裡扒外",
+"吃里爬外" => "吃裡爬外",
+"各吊" => "各吊",
+"合伙" => "合伙",
+"合并" => "合併",
+"合着" => "合著",
+"合着者" => "合著者",
+"吊上" => "吊上",
+"吊下" => "吊下",
+"吊了" => "吊了",
+"吊个" => "吊個",
+"吊儿郎当" => "吊兒郎當",
+"吊到" => "吊到",
+"吊去" => "吊去",
+"吊取" => "吊取",
+"吊吊" => "吊吊",
+"吊嗓" => "吊嗓",
+"吊好" => "吊好",
+"吊子" => "吊子",
+"吊带" => "吊帶",
+"吊带裤" => "吊帶褲",
+"吊床" => "吊床",
+"吊得" => "吊得",
+"吊挂" => "吊掛",
+"吊挂着" => "吊掛著",
+"吊杆" => "吊杆",
+"吊架" => "吊架",
+"吊桶" => "吊桶",
+"吊杆" => "吊桿",
+"吊桥" => "吊橋",
+"吊死" => "吊死",
+"吊灯" => "吊燈",
+"吊环" => "吊環",
+"吊盘" => "吊盤",
+"吊索" => "吊索",
+"吊着" => "吊著",
+"吊装" => "吊裝",
+"吊裤" => "吊褲",
+"吊裤带" => "吊褲帶",
+"吊袜" => "吊襪",
+"吊走" => "吊走",
+"吊起" => "吊起",
+"吊车" => "吊車",
+"吊钩" => "吊鉤",
+"吊销" => "吊銷",
+"吊钟" => "吊鐘",
+"同伙" => "同伙",
+"名表" => "名錶",
+"後冠" => "后冠",
+"後北街" => "后北街",
+"後土" => "后土",
+"後妃" => "后妃",
+"後安路" => "后安路",
+"後平路" => "后平路",
+"後座" => "后座",
+"後稷" => "后稷",
+"後羿" => "后羿",
+"後街" => "后街",
+"後里" => "后里",
+"向着" => "向著",
+"吞并" => "吞併",
+"吹发" => "吹髮",
+"吕後" => "呂后",
+"呆里呆气" => "呆裡呆氣",
+"呈准" => "呈准",
+"周而复始" => "周而複始",
+"呼吁" => "呼籲",
+"和面" => "和麵",
+"哪里" => "哪裡",
+"哭脏" => "哭髒",
+"问卷" => "問卷",
+"喝采" => "喝采",
+"乔岳" => "喬嶽",
+"单干" => "單干",
+"单只" => "單隻",
+"嘴里" => "嘴裏",
+"嘴里" => "嘴裡",
+"恶心" => "噁心",
+"当啷" => "噹啷",
+"当当" => "噹噹",
+"噜苏" => "嚕囌",
+"向导" => "嚮導",
+"向往" => "嚮往",
+"向应" => "嚮應",
+"向日" => "嚮日",
+"向迩" => "嚮邇",
+"严丝合缝" => "嚴絲合縫",
+"严复" => "嚴複",
+"囉苏" => "囉囌",
+"四舍五入" => "四捨五入",
+"四只" => "四隻",
+"四出" => "四齣",
+"回历新年" => "回曆新年",
+"回丝" => "回絲",
+"回着" => "回著",
+"回复" => "回覆",
+"回采" => "回采",
+"圈子里" => "圈子裡",
+"圈里" => "圈裡",
+"国历" => "國曆",
+"国雠" => "國讎",
+"园里" => "園裡",
+"圆台" => "圓臺",
+"图里" => "圖裡",
+"土里" => "土裡",
+"土制" => "土製",
+"地志" => "地誌",
+"坍台" => "坍臺",
+"坑里" => "坑裡",
+"垂发" => "垂髮",
+"垮台" => "垮臺",
+"埃及豔後" => "埃及豔后",
+"埃荣冲" => "埃榮衝",
+"埋布" => "埋佈",
+"城里" => "城裡",
+"基干" => "基幹",
+"报复" => "報複",
+"塌台" => "塌臺",
+"塔台" => "塔臺",
+"涂着" => "塗著",
+"墓志" => "墓誌",
+"墨斗" => "墨斗",
+"墨索里尼" => "墨索裡尼",
+"垦复" => "墾複",
+"压卷" => "壓卷",
+"垄断价格" => "壟斷價格",
+"垄断资产" => "壟斷資產",
+"垄断集团" => "壟斷集團",
+"壶里" => "壺裡",
+"寿面" => "壽麵",
+"夏天里" => "夏天裡",
+"夏历" => "夏曆",
+"外制" => "外製",
+"多冲" => "多衝",
+"多采多姿" => "多采多姿",
+"多么" => "多麼",
+"夜光表" => "夜光錶",
+"夜里" => "夜裡",
+"梦里" => "夢裡",
+"大伙" => "大伙",
+"大卷" => "大卷",
+"大干" => "大干",
+"大干" => "大幹",
+"大辟" => "大辟",
+"大只" => "大隻",
+"天後" => "天后",
+"天干" => "天干",
+"天文台" => "天文臺",
+"天翻地复" => "天翻地覆",
+"太後" => "太后",
+"奏折" => "奏摺",
+"女丑" => "女丑",
+"女佣" => "女佣",
+"好家夥" => "好傢夥",
+"好戏连台" => "好戲連臺",
+"好困" => "好睏",
+"如饥似渴" => "如饑似渴",
+"妆台" => "妝臺",
+"姜太公" => "姜太公",
+"姜子牙" => "姜子牙",
+"姜丝" => "姜絲",
+"字汇" => "字彙",
+"字里行间" => "字裡行間",
+"存折" => "存摺",
+"孟姜女" => "孟姜女",
+"宇宙志" => "宇宙誌",
+"宋皇台道" => "宋皇臺道",
+"定准" => "定准",
+"定制" => "定製",
+"宣布" => "宣佈",
+"宫里" => "宮裡",
+"家伙" => "家伙",
+"家里" => "家裏",
+"家里" => "家裡",
+"密布" => "密佈",
+"密致" => "密緻",
+"寇雠" => "寇讎",
+"富台街" => "富臺街",
+"寓禁于征" => "寓禁於征",
+"实干" => "實幹",
+"写字台" => "寫字檯",
+"写字台" => "寫字臺",
+"宽松" => "寬鬆",
+"宝卷" => "寶卷",
+"宝里宝气" => "寶裡寶氣",
+"封後" => "封后",
+"封面里" => "封面裡",
+"射干" => "射干",
+"对表" => "對錶",
+"小丑" => "小丑",
+"小伙" => "小伙",
+"小只" => "小隻",
+"少吊" => "少吊",
+"就里" => "就裡",
+"尺布斗粟" => "尺布斗粟",
+"尼克松" => "尼克鬆",
+"尼采" => "尼采",
+"尿斗" => "尿斗",
+"局里" => "局裡",
+"居里" => "居裡",
+"屋子里" => "屋子裡",
+"屋里" => "屋裡",
+"展布" => "展佈",
+"展卷" => "展卷",
+"屡仆屡起" => "屢仆屢起",
+"屯里" => "屯裡",
+"山岳" => "山嶽",
+"山斗" => "山斗",
+"山里" => "山裡",
+"山重水复" => "山重水複",
+"岱岳" => "岱嶽",
+"峰回" => "峰迴",
+"岳岳" => "嶽嶽",
+"巅复" => "巔覆",
+"巡回" => "巡迴",
+"巧干" => "巧幹",
+"巴尔干" => "巴爾幹",
+"巴里" => "巴裡",
+"巷里" => "巷裡",
+"市里" => "市裡",
+"布谷" => "布穀",
+"希腊" => "希腊",
+"帘子" => "帘子",
+"帘布" => "帘布",
+"席卷" => "席卷",
+"带团参加" => "帶團參加",
+"带发修行" => "帶髮修行",
+"干世" => "干世",
+"干休" => "干休",
+"干系" => "干係",
+"干冒" => "干冒",
+"干卿何事" => "干卿何事",
+"干卿底事" => "干卿底事",
+"干城" => "干城",
+"干将" => "干將",
+"干德道" => "干德道",
+"干戈" => "干戈",
+"干挠" => "干撓",
+"干扰" => "干擾",
+"干支" => "干支",
+"干政" => "干政",
+"干时" => "干時",
+"干没" => "干沒",
+"干涉" => "干涉",
+"干犯" => "干犯",
+"干禄" => "干祿",
+"干与" => "干與",
+"干着急" => "干著急",
+"干诺道中" => "干諾道中",
+"干诺道西" => "干諾道西",
+"干谒" => "干謁",
+"干证" => "干證",
+"干誉" => "干譽",
+"干贝" => "干貝",
+"干连" => "干連",
+"干云蔽日" => "干雲蔽日",
+"干预" => "干預",
+"平台" => "平臺",
+"年历" => "年曆",
+"年里" => "年裡",
+"干上" => "幹上",
+"干下去" => "幹下去",
+"干不了" => "幹不了",
+"干不成" => "幹不成",
+"干了" => "幹了",
+"干事" => "幹事",
+"干些" => "幹些",
+"干个" => "幹個",
+"干劲" => "幹勁",
+"干员" => "幹員",
+"干啥" => "幹啥",
+"干吗" => "幹嗎",
+"干嘛" => "幹嘛",
+"干坏事" => "幹壞事",
+"干完" => "幹完",
+"干将" => "幹將",
+"干得" => "幹得",
+"干性油" => "幹性油",
+"干才" => "幹才",
+"干掉" => "幹掉",
+"干校" => "幹校",
+"干活" => "幹活",
+"干流" => "幹流",
+"干球温度" => "幹球溫度",
+"干略" => "幹略",
+"干线" => "幹線",
+"干练" => "幹練",
+"干警" => "幹警",
+"干起来" => "幹起來",
+"干路" => "幹路",
+"干办" => "幹辦",
+"干这一行" => "幹這一行",
+"干这种事" => "幹這種事",
+"干道" => "幹道",
+"干部" => "幹部",
+"干么" => "幹麼",
+"几丝" => "幾絲",
+"几只" => "幾隻",
+"几出" => "幾齣",
+"底里" => "底裡",
+"店里" => "店裡",
+"康采恩" => "康采恩",
+"庙里" => "廟裡",
+"建台" => "建臺",
+"弄脏" => "弄髒",
+"弔卷" => "弔卷",
+"弘历" => "弘曆",
+"强干弱枝" => "強幹弱枝",
+"别扭" => "彆扭",
+"别拗" => "彆拗",
+"别气" => "彆氣",
+"别脚" => "彆腳",
+"别着" => "彆著",
+"弹子台" => "彈子檯",
+"弹珠台" => "彈珠檯",
+"弹药" => "彈葯",
+"汇刊" => "彙刊",
+"汇报" => "彙報",
+"汇整" => "彙整",
+"汇算" => "彙算",
+"汇编" => "彙編",
+"汇总" => "彙總",
+"汇纂" => "彙纂",
+"汇辑" => "彙輯",
+"汇集" => "彙集",
+"形单影只" => "形單影隻",
+"影後" => "影后",
+"往里" => "往裡",
+"往复" => "往複",
+"征伐" => "征伐",
+"征兵" => "征兵",
+"征利" => "征利",
+"征尘" => "征塵",
+"征夫" => "征夫",
+"征属" => "征屬",
+"征帆" => "征帆",
+"征戌" => "征戌",
+"征战" => "征戰",
+"征收" => "征收",
+"征服" => "征服",
+"征求" => "征求",
+"征发" => "征發",
+"征衣" => "征衣",
+"征讨" => "征討",
+"征途" => "征途",
+"后台" => "後臺",
+"从里到外" => "從裡到外",
+"从里向外" => "從裡向外",
+"复雠" => "復讎",
+"复辟" => "復辟",
+"德干高原" => "德干高原",
+"心愿" => "心愿",
+"心里" => "心裏",
+"心里" => "心裡",
+"忙里" => "忙裡",
+"快干" => "快幹",
+"快冲" => "快衝",
+"怎么" => "怎麼",
+"怎么着" => "怎麼著",
+"急冲而下" => "急衝而下",
+"怪里怪气" => "怪裡怪氣",
+"恩准" => "恩准",
+"情有所钟" => "情有所鍾",
+"情有独钟" => "情有獨鍾",
+"意面" => "意麵",
+"慌里慌张" => "慌裡慌張",
+"慰借" => "慰藉",
+"忧郁" => "憂郁",
+"凭吊" => "憑吊",
+"凭借" => "憑藉",
+"凭借着" => "憑藉著",
+"蒙懂" => "懞懂",
+"怀里" => "懷裡",
+"怀表" => "懷錶",
+"悬吊" => "懸吊",
+"悬心吊胆" => "懸心吊膽",
+"戏台" => "戲臺",
+"戴表" => "戴錶",
+"戽斗" => "戽斗",
+"房里" => "房裡",
+"手不释卷" => "手不釋卷",
+"手卷" => "手卷",
+"手折" => "手摺",
+"手里" => "手裏",
+"手里" => "手裡",
+"手表" => "手錶",
+"手松" => "手鬆",
+"才干" => "才幹",
+"才高八斗" => "才高八斗",
+"打谷" => "打穀",
+"扞御" => "扞禦",
+"批准" => "批准",
+"批复" => "批複",
+"批复" => "批覆",
+"承制" => "承製",
+"抗御" => "抗禦",
+"折冲" => "折衝",
+"披复" => "披覆",
+"披发" => "披髮",
+"抱朴" => "抱朴",
+"抵御" => "抵禦",
+"拆伙" => "拆伙",
+"拆台" => "拆臺",
+"拈须" => "拈鬚",
+"拉纤" => "拉縴",
+"拉面" => "拉麵",
+"拖吊" => "拖吊",
+"拗别" => "拗彆",
+"拮据" => "拮据",
+"捍御" => "捍禦",
+"舍不得" => "捨不得",
+"舍出" => "捨出",
+"舍去" => "捨去",
+"舍命" => "捨命",
+"舍己从人" => "捨己從人",
+"舍己救人" => "捨己救人",
+"舍己为人" => "捨己為人",
+"舍己为公" => "捨己為公",
+"舍己为国" => "捨己為國",
+"舍得" => "捨得",
+"舍我其谁" => "捨我其誰",
+"舍本逐末" => "捨本逐末",
+"舍弃" => "捨棄",
+"舍死忘生" => "捨死忘生",
+"舍生" => "捨生",
+"舍短取长" => "捨短取長",
+"舍身" => "捨身",
+"舍车保帅" => "捨車保帥",
+"舍近求远" => "捨近求遠",
+"捲发" => "捲髮",
+"捵面" => "捵麵",
+"掌柜" => "掌柜",
+"排骨面" => "排骨麵",
+"挂帘" => "掛帘",
+"挂面" => "掛麵",
+"接着说" => "接著說",
+"掩卷" => "掩卷",
+"提心吊胆" => "提心吊膽",
+"插图卷" => "插圖卷",
+"换吊" => "換吊",
+"换只" => "換隻",
+"换发" => "換髮",
+"握发" => "握髮",
+"搭伙" => "搭伙",
+"折合" => "摺合",
+"折奏" => "摺奏",
+"折子" => "摺子",
+"折尺" => "摺尺",
+"折扇" => "摺扇",
+"折梯" => "摺梯",
+"折椅" => "摺椅",
+"折叠" => "摺疊",
+"折痕" => "摺痕",
+"折篷" => "摺篷",
+"折纸" => "摺紙",
+"折裙" => "摺裙",
+"撒布" => "撒佈",
+"撚须" => "撚鬚",
+"撞球台" => "撞球檯",
+"擂台" => "擂臺",
+"担仔面" => "擔仔麵",
+"担担面" => "擔擔麵",
+"担着" => "擔著",
+"担负着" => "擔負著",
+"据云" => "據云",
+"擢发难数" => "擢髮難數",
+"拟准" => "擬准",
+"摆布" => "擺佈",
+"摄制" => "攝製",
+"支干" => "支幹",
+"收获" => "收穫",
+"改制" => "改製",
+"攻克" => "攻剋",
+"放松" => "放鬆",
+"故布疑阵" => "故佈疑陣",
+"叙说着" => "敘說著",
+"散伙" => "散伙",
+"散布" => "散佈",
+"散发" => "散髮",
+"整只" => "整隻",
+"整出" => "整齣",
+"敌忾同雠" => "敵愾同讎",
+"文借" => "文藉",
+"文采" => "文采",
+"斗亚兰路" => "斗亞蘭路",
+"斗六" => "斗六",
+"斗南" => "斗南",
+"斗大" => "斗大",
+"斗子" => "斗子",
+"斗室" => "斗室",
+"斗宿" => "斗宿",
+"斗方" => "斗方",
+"斗栱" => "斗栱",
+"斗笠" => "斗笠",
+"斗筲" => "斗筲",
+"斗箕" => "斗箕",
+"斗篷" => "斗篷",
+"斗胆" => "斗膽",
+"斗蓬" => "斗蓬",
+"斗转参横" => "斗轉參橫",
+"斗量" => "斗量",
+"斗门" => "斗門",
+"料斗" => "料斗",
+"斤斗" => "斤斗",
+"斯里兰卡" => "斯裡蘭卡",
+"新历" => "新曆",
+"断头台" => "斷頭臺",
+"断发文身" => "斷髮文身",
+"方才" => "方纔",
+"方志" => "方誌",
+"施舍" => "施捨",
+"旋绕着" => "旋繞著",
+"旋回" => "旋迴",
+"族里" => "族裡",
+"日历" => "日曆",
+"日志" => "日誌",
+"日进斗金" => "日進斗金",
+"明了" => "明瞭",
+"明窗净几" => "明窗淨几",
+"明里" => "明裡",
+"星斗" => "星斗",
+"星历" => "星曆",
+"星移斗换" => "星移斗換",
+"星移斗转" => "星移斗轉",
+"星罗棋布" => "星羅棋佈",
+"星辰表" => "星辰錶",
+"春假里" => "春假裡",
+"春天里" => "春天裡",
+"景致" => "景緻",
+"暗地里" => "暗地裡",
+"暗沟里" => "暗溝裡",
+"暗里" => "暗裡",
+"暴敛横征" => "暴斂橫征",
+"历数" => "曆數",
+"历书" => "曆書",
+"历法" => "曆法",
+"历象" => "曆象",
+"书卷" => "書卷",
+"会干" => "會幹",
+"会里" => "會裡",
+"月历" => "月曆",
+"月台" => "月臺",
+"有只" => "有隻",
+"木制" => "木製",
+"本台" => "本臺",
+"朴子" => "朴子",
+"朴实" => "朴實",
+"朴忠" => "朴忠",
+"朴直" => "朴直",
+"朴硝" => "朴硝",
+"朴素" => "朴素",
+"朴茂" => "朴茂",
+"朴资茅斯" => "朴資茅斯",
+"朴钝" => "朴鈍",
+"材干" => "材幹",
+"村里" => "村裡",
+"杜老志道" => "杜老誌道",
+"束发" => "束髮",
+"杯面" => "杯麵",
+"东岳" => "東嶽",
+"东征" => "東征",
+"松赞干布" => "松贊干布",
+"板着脸" => "板著臉",
+"枕借" => "枕藉",
+"林宏岳" => "林宏嶽",
+"枝干" => "枝幹",
+"枯干" => "枯幹",
+"某只" => "某隻",
+"染发" => "染髮",
+"柜上" => "柜上",
+"柜台" => "柜台",
+"柜子" => "柜子",
+"柜柳" => "柜柳",
+"查卷" => "查卷",
+"查号台" => "查號臺",
+"校雠学" => "校讎學",
+"核准" => "核准",
+"核复" => "核覆",
+"格里" => "格裡",
+"案准" => "案准",
+"案卷" => "案卷",
+"条干" => "條幹",
+"梯冲" => "梯衝",
+"械系" => "械繫",
+"棉卷" => "棉卷",
+"棉制" => "棉製",
+"植发" => "植髮",
+"楼台" => "樓臺",
+"标志着" => "標志著",
+"标致" => "標緻",
+"标志" => "標誌",
+"模制" => "模製",
+"树干" => "樹幹",
+"横征暴敛" => "橫征暴斂",
+"横冲" => "橫衝",
+"档卷" => "檔卷",
+"检复" => "檢覆",
+"台子" => "檯子",
+"台布" => "檯布",
+"台灯" => "檯燈",
+"台球" => "檯球",
+"台面" => "檯面",
+"柜台" => "櫃檯",
+"柜台" => "櫃臺",
+"栏干" => "欄干",
+"欺蒙" => "欺矇",
+"歌後" => "歌后",
+"歌台舞榭" => "歌臺舞榭",
+"欧几里得" => "歐幾裡得",
+"正当着" => "正當著",
+"此仆彼起" => "此仆彼起",
+"武後" => "武后",
+"武松" => "武鬆",
+"归并" => "歸併",
+"死里求生" => "死裡求生",
+"死里逃生" => "死裡逃生",
+"残卷" => "殘卷",
+"杀虫药" => "殺虫藥",
+"壳里" => "殼裡",
+"母後" => "母后",
+"每只" => "每隻",
+"比干" => "比干",
+"毛卷" => "毛卷",
+"毛坏" => "毛坏",
+"毛发" => "毛髮",
+"毫发" => "毫髮",
+"气冲斗牛" => "氣沖斗牛",
+"气冲牛斗" => "氣沖牛斗",
+"气象台" => "氣象臺",
+"水斗" => "水斗",
+"水里" => "水裡",
+"水表" => "水錶",
+"永历" => "永曆",
+"永志不忘" => "永誌不忘",
+"污蔑" => "汙衊",
+"江干" => "江干",
+"池里" => "池裡",
+"污蔑" => "污衊",
+"沈着" => "沈著",
+"没事干" => "沒事幹",
+"没精打采" => "沒精打采",
+"冲着" => "沖著",
+"沙里淘金" => "沙裡淘金",
+"河岳" => "河嶽",
+"河里" => "河裡",
+"油面" => "油麵",
+"泡制" => "泡製",
+"泡面" => "泡麵",
+"泰斗" => "泰斗",
+"洗发" => "洗髮",
+"派团参加" => "派團參加",
+"浪琴表" => "浪琴錶",
+"浮吊" => "浮吊",
+"海里" => "海裡",
+"涂着" => "涂著",
+"液晶表" => "液晶錶",
+"凉面" => "涼麵",
+"淡朱" => "淡硃",
+"渊淳岳峙" => "淵淳嶽峙",
+"渠冲" => "渠衝",
+"测验卷" => "測驗卷",
+"港制" => "港製",
+"凑合着" => "湊合著",
+"湖里" => "湖裡",
+"汤团" => "湯糰",
+"汤面" => "湯麵",
+"温郁" => "溫郁",
+"卤制" => "滷製",
+"卤面" => "滷麵",
+"满布" => "滿佈",
+"漏斗" => "漏斗",
+"演奏台" => "演奏臺",
+"潜意识里" => "潛意識裡",
+"潭里" => "潭裡",
+"浓郁" => "濃郁",
+"浓发" => "濃髮",
+"湿地松" => "濕地鬆",
+"蒙蒙" => "濛濛",
+"蒙雾" => "濛霧",
+"蒙鸿" => "濛鴻",
+"瀛台" => "瀛臺",
+"弥漫" => "瀰漫",
+"弥漫着" => "瀰漫著",
+"漓江" => "灕江",
+"火并" => "火併",
+"灰蒙" => "灰濛",
+"炒面" => "炒麵",
+"炮制" => "炮製",
+"炸药" => "炸葯",
+"炸酱面" => "炸醬麵",
+"为着" => "為著",
+"乌干达" => "烏干達",
+"乌苏里江" => "烏蘇裡江",
+"乌发" => "烏髮",
+"乌龙面" => "烏龍麵",
+"烘制" => "烘製",
+"烽火台" => "烽火臺",
+"无干" => "無干",
+"无精打采" => "無精打采",
+"炼制" => "煉製",
+"烟卷儿" => "煙卷兒",
+"烟斗" => "煙斗",
+"烟斗丝" => "煙斗絲",
+"烟台" => "煙臺",
+"照准" => "照准",
+"熨斗" => "熨斗",
+"灯台" => "燈臺",
+"燎发" => "燎髮",
+"烫发" => "燙髮",
+"烫面" => "燙麵",
+"烛台" => "燭臺",
+"炉台" => "爐臺",
+"墙里" => "牆裡",
+"片言只语" => "片言隻語",
+"牛肉面" => "牛肉麵",
+"牛只" => "牛隻",
+"特准" => "特准",
+"特征" => "特征",
+"特里" => "特裡",
+"特制" => "特製",
+"牵系" => "牽繫",
+"狼借" => "狼藉",
+"猛冲" => "猛衝",
+"奖杯" => "獎盃",
+"获准" => "獲准",
+"率团参加" => "率團參加",
+"王侯後" => "王侯后",
+"王後" => "王后",
+"班里" => "班裡",
+"理发" => "理髮",
+"瑶台" => "瑤臺",
+"甚么" => "甚麼",
+"甜面酱" => "甜麵醬",
+"生力面" => "生力麵",
+"生锈" => "生鏽",
+"生发" => "生髮",
+"田里" => "田裡",
+"由馀" => "由余",
+"由表及里" => "由表及裡",
+"男佣" => "男佣",
+"男用表" => "男用錶",
+"留发" => "留髮",
+"畚斗" => "畚斗",
+"当着" => "當著",
+"疏松" => "疏鬆",
+"疑系" => "疑係",
+"疲困" => "疲睏",
+"病症" => "病癥",
+"症候" => "癥候",
+"症状" => "癥狀",
+"症结" => "癥結",
+"登台" => "登臺",
+"发布" => "發佈",
+"发蒙" => "發矇",
+"发着" => "發著",
+"发面" => "發麵",
+"发霉" => "發黴",
+"白卷" => "白卷",
+"白干儿" => "白干兒",
+"白里透红" => "白裡透紅",
+"白发" => "白髮",
+"白面" => "白麵",
+"百谷" => "百穀",
+"百里" => "百裡",
+"百只" => "百隻",
+"皇後" => "皇后",
+"皇历" => "皇曆",
+"皓发" => "皓髮",
+"皮里阳秋" => "皮裏陽秋",
+"皮里春秋" => "皮裡春秋",
+"皮制" => "皮製",
+"皱折" => "皺摺",
+"盒里" => "盒裡",
+"监制" => "監製",
+"盘里" => "盤裡",
+"盘回" => "盤迴",
+"直接参与" => "直接參与",
+"直冲" => "直衝",
+"相克" => "相剋",
+"相干" => "相干",
+"相冲" => "相衝",
+"看台" => "看臺",
+"眼帘" => "眼帘",
+"眼眶里" => "眼眶裡",
+"眼里" => "眼裡",
+"困乏" => "睏乏",
+"困倦" => "睏倦",
+"睡着了" => "睡著了",
+"了如" => "瞭如",
+"了望" => "瞭望",
+"了然" => "瞭然",
+"了若指掌" => "瞭若指掌",
+"了解" => "瞭解",
+"瞳蒙" => "瞳矇",
+"蒙住" => "矇住",
+"蒙昧无知" => "矇昧無知",
+"蒙混" => "矇混",
+"蒙蒙" => "矇矇",
+"蒙眬" => "矇矓",
+"蒙蔽" => "矇蔽",
+"蒙骗" => "矇騙",
+"短发" => "短髮",
+"矮几" => "矮几",
+"石英表" => "石英錶",
+"石莼" => "石蓴",
+"研制" => "研製",
+"砰当" => "砰噹",
+"砲台" => "砲臺",
+"朱唇皓齿" => "硃唇皓齒",
+"朱批" => "硃批",
+"朱砂" => "硃砂",
+"朱笔" => "硃筆",
+"朱红色" => "硃紅色",
+"朱色" => "硃色",
+"朱谕" => "硃諭",
+"硬干" => "硬幹",
+"砚台" => "硯臺",
+"碑志" => "碑誌",
+"磁制" => "磁製",
+"磨制" => "磨製",
+"示复" => "示覆",
+"社里" => "社裡",
+"神采" => "神采",
+"御侮" => "禦侮",
+"御寇" => "禦寇",
+"御寒" => "禦寒",
+"御敌" => "禦敵",
+"礼义干橹" => "禮義干櫓",
+"秃发" => "禿髮",
+"秀斗" => "秀斗",
+"秀发" => "秀髮",
+"私下里" => "私下裡",
+"秋天里" => "秋天裡",
+"秋裤" => "秋褲",
+"秒表" => "秒錶",
+"稀松" => "稀鬆",
+"禀复" => "稟覆",
+"稻谷" => "稻穀",
+"稽征" => "稽征",
+"谷人" => "穀人",
+"谷保家商" => "穀保家商",
+"谷仓" => "穀倉",
+"谷场" => "穀場",
+"谷子" => "穀子",
+"谷梁" => "穀梁",
+"谷壳" => "穀殼",
+"谷物" => "穀物",
+"谷皮" => "穀皮",
+"谷神" => "穀神",
+"谷谷" => "穀穀",
+"谷粒" => "穀粒",
+"谷舱" => "穀艙",
+"谷苗" => "穀苗",
+"谷草" => "穀草",
+"谷贱伤农" => "穀賤傷農",
+"谷道" => "穀道",
+"谷雨" => "穀雨",
+"谷类" => "穀類",
+"谷风" => "穀風",
+"积极参与" => "積极參与",
+"积极参加" => "積极參加",
+"积谷防饥" => "積穀防饑",
+"空蒙" => "空濛",
+"窗帘" => "窗帘",
+"窗明几净" => "窗明几淨",
+"窗台" => "窗檯",
+"窗台" => "窗臺",
+"窝里" => "窩裡",
+"窝阔台" => "窩闊臺",
+"穷发" => "窮髮",
+"站台" => "站臺",
+"笆斗" => "笆斗",
+"笑里藏刀" => "笑裡藏刀",
+"第一卷" => "第一卷",
+"筋斗" => "筋斗",
+"答卷" => "答卷",
+"答复" => "答複",
+"答复" => "答覆",
+"筵几" => "筵几",
+"箕斗" => "箕斗",
+"算历" => "算曆",
+"签着" => "簽著",
+"吁求" => "籲求",
+"吁请" => "籲請",
+"粗制" => "粗製",
+"粗卤" => "粗鹵",
+"精干" => "精幹",
+"精明强干" => "精明強幹",
+"精致" => "精緻",
+"精制" => "精製",
+"精辟" => "精辟",
+"精采" => "精采",
+"糊里糊涂" => "糊裡糊塗",
+"团子" => "糰子",
+"系着" => "系著",
+"系里" => "系裡",
+"纪历" => "紀曆",
+"红绳系足" => "紅繩繫足",
+"红发" => "紅髮",
+"纡回" => "紆迴",
+"纳采" => "納采",
+"素食面" => "素食麵",
+"素发" => "素髮",
+"素面" => "素麵",
+"紫微斗数" => "紫微斗數",
+"细致" => "細緻",
+"组里" => "組裡",
+"结发" => "結髮",
+"绝对参照" => "絕對參照",
+"丝来线去" => "絲來線去",
+"丝布" => "絲布",
+"丝板" => "絲板",
+"丝瓜布" => "絲瓜布",
+"丝绒布" => "絲絨布",
+"丝线" => "絲線",
+"丝织厂" => "絲織廠",
+"丝虫" => "絲蟲",
+"綑吊" => "綑吊",
+"经卷" => "經卷",
+"维系" => "維繫",
+"绾发" => "綰髮",
+"网里" => "網裡",
+"紧绷" => "緊繃",
+"紧绷着" => "緊繃著",
+"编制" => "編製",
+"编发" => "編髮",
+"缓冲" => "緩衝",
+"致密" => "緻密",
+"萦回" => "縈迴",
+"县里" => "縣裡",
+"县志" => "縣誌",
+"缝里" => "縫裡",
+"缝制" => "縫製",
+"纤夫" => "縴夫",
+"纤手" => "縴手",
+"繁复" => "繁複",
+"绷住" => "繃住",
+"绷子" => "繃子",
+"绷带" => "繃帶",
+"绷紧" => "繃緊",
+"绷脸" => "繃臉",
+"绷着" => "繃著",
+"绷着脸" => "繃著臉",
+"绷着脸儿" => "繃著臉兒",
+"绷开" => "繃開",
+"绘制" => "繪製",
+"系上" => "繫上",
+"系世" => "繫世",
+"系到" => "繫到",
+"系囚" => "繫囚",
+"系心" => "繫心",
+"系念" => "繫念",
+"系怀" => "繫懷",
+"系恋" => "繫戀",
+"系数" => "繫數",
+"系于" => "繫於",
+"系系" => "繫系",
+"系结" => "繫結",
+"系紧" => "繫緊",
+"系绳" => "繫繩",
+"系累" => "繫纍",
+"系着" => "繫著",
+"系辞" => "繫辭",
+"系风捕影" => "繫風捕影",
+"缴卷" => "繳卷",
+"累囚" => "纍囚",
+"累累" => "纍纍",
+"坛子" => "罈子",
+"坛坛罐罐" => "罈罈罐罐",
+"骂着" => "罵著",
+"羁系" => "羈繫",
+"美制" => "美製",
+"美发" => "美髮",
+"翻来复去" => "翻來覆去",
+"翻天复地" => "翻天覆地",
+"翻复" => "翻覆",
+"翻云复雨" => "翻雲覆雨",
+"老么" => "老么",
+"老板" => "老闆",
+"考卷" => "考卷",
+"耕获" => "耕穫",
+"聊斋志异" => "聊齋誌異",
+"联系" => "聯係",
+"联系" => "聯繫",
+"肉丝面" => "肉絲麵",
+"肉羹面" => "肉羹麵",
+"肉松" => "肉鬆",
+"肚里" => "肚裏",
+"肚里" => "肚裡",
+"肢体" => "肢体",
+"胃里" => "胃裡",
+"背向着" => "背向著",
+"背地里" => "背地裡",
+"胡里胡涂" => "胡裡胡塗",
+"能干" => "能幹",
+"脉冲" => "脈衝",
+"脱发" => "脫髮",
+"腊味" => "腊味",
+"腊笔" => "腊筆",
+"腊肉" => "腊肉",
+"脑子里" => "腦子裡",
+"腰里" => "腰裡",
+"胶卷" => "膠卷",
+"膨松" => "膨鬆",
+"自制" => "自製",
+"自觉自愿" => "自覺自愿",
+"台上" => "臺上",
+"台下" => "臺下",
+"台中" => "臺中",
+"台儿庄" => "臺兒莊",
+"台北" => "臺北",
+"台南" => "臺南",
+"台地" => "臺地",
+"台塑" => "臺塑",
+"台大" => "臺大",
+"台币" => "臺幣",
+"台座" => "臺座",
+"台东" => "臺東",
+"台柱" => "臺柱",
+"台榭" => "臺榭",
+"台机路" => "臺機路",
+"台步" => "臺步",
+"台汽" => "臺汽",
+"台海" => "臺海",
+"台澎金马" => "臺澎金馬",
+"台湾" => "臺灣",
+"台灯" => "臺燈",
+"台球" => "臺球",
+"台省" => "臺省",
+"台端" => "臺端",
+"台糖" => "臺糖",
+"台肥" => "臺肥",
+"台航" => "臺航",
+"台西" => "臺西",
+"台视" => "臺視",
+"台词" => "臺詞",
+"台车" => "臺車",
+"台铁" => "臺鐵",
+"台阶" => "臺階",
+"台电" => "臺電",
+"台面" => "臺面",
+"舂谷" => "舂穀",
+"兴致" => "興緻",
+"兴高采烈" => "興高采烈",
+"旧历" => "舊曆",
+"舒卷" => "舒卷",
+"舞榭歌台" => "舞榭歌臺",
+"舞台" => "舞臺",
+"航海历" => "航海曆",
+"船只" => "船隻",
+"舰只" => "艦隻",
+"芬郁" => "芬郁",
+"花卷" => "花卷",
+"花盆里" => "花盆裡",
+"花采" => "花采",
+"苑里" => "苑裡",
+"若干" => "若干",
+"若干" => "若幹",
+"苦干" => "苦幹",
+"苦里" => "苦裏",
+"苦卤" => "苦鹵",
+"范仲淹" => "范仲淹",
+"范蠡" => "范蠡",
+"范阳" => "范陽",
+"茅台" => "茅臺",
+"茶几" => "茶几",
+"草丛里" => "草叢裡",
+"庄里" => "莊裡",
+"茎干" => "莖幹",
+"菌丝体" => "菌絲体",
+"菌丝体" => "菌絲體",
+"华里" => "華裡",
+"华发" => "華髮",
+"万卷" => "萬卷",
+"万历" => "萬曆",
+"万只" => "萬隻",
+"落发" => "落髮",
+"着儿" => "著兒",
+"着书立说" => "著書立說",
+"着色软体" => "著色軟體",
+"着重指出" => "著重指出",
+"着录" => "著錄",
+"着录规则" => "著錄規則",
+"蓄发" => "蓄髮",
+"蓄须" => "蓄鬚",
+"蓬发" => "蓬髮",
+"蓬松" => "蓬鬆",
+"莲台" => "蓮臺",
+"薑丝" => "薑絲",
+"薙发" => "薙髮",
+"借以" => "藉以",
+"借助" => "藉助",
+"借口" => "藉口",
+"借故" => "藉故",
+"借机" => "藉機",
+"借此" => "藉此",
+"借由" => "藉由",
+"借端" => "藉端",
+"借着" => "藉著",
+"借借" => "藉藉",
+"借词" => "藉詞",
+"借资" => "藉資",
+"借酒浇愁" => "藉酒澆愁",
+"藤制" => "藤製",
+"蕴含着" => "蘊含著",
+"蕴涵着" => "蘊涵著",
+"蕴借" => "蘊藉",
+"萝卜" => "蘿蔔",
+"虎须" => "虎鬚",
+"号志" => "號誌",
+"蜂後" => "蜂后",
+"蜜里调油" => "蜜裡調油",
+"蠁干" => "蠁幹",
+"蛮干" => "蠻幹",
+"行事历" => "行事曆",
+"胡同" => "衚衕",
+"冲上" => "衝上",
+"冲下" => "衝下",
+"冲来" => "衝來",
+"冲倒" => "衝倒",
+"冲冠" => "衝冠",
+"冲出" => "衝出",
+"冲到" => "衝到",
+"冲刺" => "衝刺",
+"冲克" => "衝剋",
+"冲力" => "衝力",
+"冲劲" => "衝勁",
+"冲动" => "衝動",
+"冲去" => "衝去",
+"冲口" => "衝口",
+"冲垮" => "衝垮",
+"冲堂" => "衝堂",
+"冲压" => "衝壓",
+"冲天" => "衝天",
+"冲掉" => "衝掉",
+"冲撞" => "衝撞",
+"冲击" => "衝擊",
+"冲散" => "衝散",
+"冲决" => "衝決",
+"冲浪" => "衝浪",
+"冲激" => "衝激",
+"冲破" => "衝破",
+"冲程" => "衝程",
+"冲突" => "衝突",
+"冲线" => "衝線",
+"冲着" => "衝著",
+"冲冲" => "衝衝",
+"冲要" => "衝要",
+"冲起" => "衝起",
+"冲进" => "衝進",
+"冲过" => "衝過",
+"冲锋" => "衝鋒",
+"表里" => "表裡",
+"袋里" => "袋裡",
+"袖里" => "袖裡",
+"被里" => "被裡",
+"被复" => "被複",
+"被复" => "被覆",
+"被复着" => "被覆著",
+"被发" => "被髮",
+"裁并" => "裁併",
+"裁制" => "裁製",
+"里面" => "裏面",
+"里人" => "裡人",
+"里加" => "裡加",
+"里外" => "裡外",
+"里子" => "裡子",
+"里屋" => "裡屋",
+"里层" => "裡層",
+"里布" => "裡布",
+"里带" => "裡帶",
+"里弦" => "裡弦",
+"里应外合" => "裡應外合",
+"里拉" => "裡拉",
+"里斯" => "裡斯",
+"里海" => "裡海",
+"里脊" => "裡脊",
+"里衣" => "裡衣",
+"里里" => "裡裡",
+"里通外国" => "裡通外國",
+"里通外敌" => "裡通外敵",
+"里边" => "裡邊",
+"里间" => "裡間",
+"里面" => "裡面",
+"里头" => "裡頭",
+"制件" => "製件",
+"制作" => "製作",
+"制做" => "製做",
+"制备" => "製備",
+"制冰" => "製冰",
+"制冷" => "製冷",
+"制剂" => "製劑",
+"制品" => "製品",
+"制图" => "製圖",
+"制成" => "製成",
+"制法" => "製法",
+"制为" => "製為",
+"制片" => "製片",
+"制版" => "製版",
+"制程" => "製程",
+"制糖" => "製糖",
+"制纸" => "製紙",
+"制药" => "製藥",
+"制表" => "製表",
+"制裁" => "製裁",
+"制造" => "製造",
+"制革" => "製革",
+"制鞋" => "製鞋",
+"制盐" => "製鹽",
+"复仞年如" => "複仞年如",
+"复以百万" => "複以百萬",
+"复位" => "複位",
+"复信" => "複信",
+"复分数" => "複分數",
+"复列" => "複列",
+"复利" => "複利",
+"复印" => "複印",
+"复原" => "複原",
+"复句" => "複句",
+"复合" => "複合",
+"复名" => "複名",
+"复员" => "複員",
+"复壁" => "複壁",
+"复壮" => "複壯",
+"复姓" => "複姓",
+"复字键" => "複字鍵",
+"复审" => "複審",
+"复写" => "複寫",
+"复式" => "複式",
+"复复" => "複復",
+"复数" => "複數",
+"复本" => "複本",
+"复查" => "複查",
+"复核" => "複核",
+"复检" => "複檢",
+"复次" => "複次",
+"复比" => "複比",
+"复决" => "複決",
+"复活" => "複活",
+"复测" => "複測",
+"复亩珍" => "複畝珍",
+"复发" => "複發",
+"复目" => "複目",
+"复眼" => "複眼",
+"复种" => "複種",
+"复线" => "複線",
+"复习" => "複習",
+"复兴社" => "複興社",
+"复旧" => "複舊",
+"复色" => "複色",
+"复叶" => "複葉",
+"复盖" => "複蓋",
+"复苏" => "複蘇",
+"复制" => "複製",
+"复诊" => "複診",
+"复评" => "複評",
+"复词" => "複詞",
+"复试" => "複試",
+"复课" => "複課",
+"复议" => "複議",
+"复变函数" => "複變函數",
+"复赛" => "複賽",
+"复述" => "複述",
+"复选" => "複選",
+"复钱" => "複錢",
+"复阅" => "複閱",
+"复杂" => "複雜",
+"复电" => "複電",
+"复音" => "複音",
+"复韵" => "複韻",
+"衬里" => "襯裡",
+"西岳" => "西嶽",
+"西征" => "西征",
+"西历" => "西曆",
+"要冲" => "要衝",
+"要么" => "要麼",
+"复上" => "覆上",
+"复亡" => "覆亡",
+"复住" => "覆住",
+"复信" => "覆信",
+"复冒" => "覆冒",
+"复判" => "覆判",
+"复命" => "覆命",
+"复在" => "覆在",
+"复审" => "覆審",
+"复写" => "覆寫",
+"复巢" => "覆巢",
+"复成" => "覆成",
+"复败" => "覆敗",
+"复文" => "覆文",
+"复校" => "覆校",
+"复核" => "覆核",
+"复水难收" => "覆水難收",
+"复没" => "覆沒",
+"复灭" => "覆滅",
+"复叠" => "覆疊",
+"复盆" => "覆盆",
+"复舟" => "覆舟",
+"复着" => "覆著",
+"复盖" => "覆蓋",
+"复盖着" => "覆蓋著",
+"复试" => "覆試",
+"复诵" => "覆誦",
+"复议" => "覆議",
+"复车" => "覆車",
+"复载" => "覆載",
+"复辙" => "覆轍",
+"复述" => "覆述",
+"复选" => "覆選",
+"复电" => "覆電",
+"复鼎金" => "覆鼎金",
+"见复" => "見覆",
+"亲征" => "親征",
+"观众台" => "觀眾臺",
+"观台" => "觀臺",
+"观象台" => "觀象臺",
+"角落里" => "角落裡",
+"觔斗" => "觔斗",
+"触须" => "觸鬚",
+"订制" => "訂製",
+"诉说着" => "訴說著",
+"词汇" => "詞彙",
+"词采" => "詞采",
+"试卷" => "試卷",
+"试制" => "試製",
+"诗卷" => "詩卷",
+"话里有话" => "話裡有話",
+"志哀" => "誌哀",
+"志喜" => "誌喜",
+"志庆" => "誌慶",
+"语云" => "語云",
+"语汇" => "語彙",
+"诬蔑" => "誣衊",
+"诵经台" => "誦經臺",
+"说着" => "說著",
+"课征" => "課征",
+"调制" => "調製",
+"调频台" => "調頻臺",
+"请参阅" => "請參閱",
+"讲台" => "講臺",
+"谢绝参观" => "謝絕參觀",
+"护发" => "護髮",
+"雠正" => "讎正",
+"雠隙" => "讎隙",
+"豆腐干" => "豆腐干",
+"竖着" => "豎著",
+"丰富多采" => "豐富多采",
+"丰滨" => "豐濱",
+"丰滨乡" => "豐濱鄉",
+"丰采" => "豐采",
+"象征着" => "象徵著",
+"贵干" => "貴幹",
+"贾後" => "賈后",
+"赈饥" => "賑饑",
+"赐复" => "賜覆",
+"贤後" => "賢后",
+"质朴" => "質朴",
+"赌台" => "賭檯",
+"购并" => "購併",
+"赤绳系足" => "赤繩繫足",
+"赤松" => "赤鬆",
+"起吊" => "起吊",
+"起复" => "起複",
+"超级杯" => "超級盃",
+"赶制" => "趕製",
+"跟斗" => "跟斗",
+"跳表" => "跳錶",
+"蹈借" => "蹈藉",
+"踬仆" => "躓仆",
+"躯干" => "軀幹",
+"车库里" => "車庫裡",
+"车站里" => "車站裡",
+"车里" => "車裡",
+"轻松" => "輕鬆",
+"轮回" => "輪迴",
+"转台" => "轉檯",
+"辛丑" => "辛丑",
+"辟易" => "辟易",
+"辟邪" => "辟邪",
+"办伙" => "辦伙",
+"办公台" => "辦公檯",
+"辞汇" => "辭彙",
+"农历" => "農曆",
+"迂回" => "迂迴",
+"近日里" => "近日裡",
+"迥然回异" => "迥然迴異",
+"回光返照" => "迴光返照",
+"回向" => "迴向",
+"回圈" => "迴圈",
+"回廊" => "迴廊",
+"回形夹" => "迴形夾",
+"回文" => "迴文",
+"回旋" => "迴旋",
+"回流" => "迴流",
+"回环" => "迴環",
+"回盪" => "迴盪",
+"回纹针" => "迴紋針",
+"回绕" => "迴繞",
+"回翔" => "迴翔",
+"回肠" => "迴腸",
+"回荡" => "迴蕩",
+"回诵" => "迴誦",
+"回路" => "迴路",
+"回转" => "迴轉",
+"回递性" => "迴遞性",
+"回避" => "迴避",
+"回銮" => "迴鑾",
+"回音" => "迴音",
+"回响" => "迴響",
+"回风" => "迴風",
+"回首" => "迴首",
+"迷蒙" => "迷濛",
+"退伙" => "退伙",
+"这么着" => "這么著",
+"这里" => "這裏",
+"这里" => "這裡",
+"这只" => "這隻",
+"这么" => "這麼",
+"这么着" => "這麼著",
+"通心面" => "通心麵",
+"速食面" => "速食麵",
+"连系" => "連繫",
+"连台好戏" => "連臺好戲",
+"遍布" => "遍佈",
+"递回" => "遞迴",
+"远征" => "遠征",
+"适才" => "適纔",
+"遮复" => "遮覆",
+"还冲" => "還衝",
+"邋里邋遢" => "邋裡邋遢",
+"那里" => "那裏",
+"那里" => "那裡",
+"那只" => "那隻",
+"那么" => "那麼",
+"那么着" => "那麼著",
+"邪辟" => "邪辟",
+"郁烈" => "郁烈",
+"郁穆" => "郁穆",
+"郁郁" => "郁郁",
+"郁闭" => "郁閉",
+"郁馥" => "郁馥",
+"乡愿" => "鄉愿",
+"乡里" => "鄉裡",
+"邻里" => "鄰裡",
+"配合着" => "配合著",
+"配制" => "配製",
+"酒杯" => "酒盃",
+"酒坛" => "酒罈",
+"酥松" => "酥鬆",
+"醋坛" => "醋罈",
+"酝借" => "醞藉",
+"酝酿着" => "醞釀著",
+"医药" => "醫葯",
+"醲郁" => "醲郁",
+"酿制" => "釀製",
+"采地" => "采地",
+"采女" => "采女",
+"采声" => "采聲",
+"采色" => "采色",
+"采薇" => "采薇",
+"采薪之忧" => "采薪之憂",
+"采兰赠药" => "采蘭贈藥",
+"采邑" => "采邑",
+"采采" => "采采",
+"采风" => "采風",
+"里程表" => "里程錶",
+"重折" => "重摺",
+"重制" => "重製",
+"重复" => "重複",
+"重复" => "重覆",
+"野台戏" => "野臺戲",
+"金斗" => "金斗",
+"金装玉里" => "金裝玉裡",
+"金表" => "金錶",
+"金发" => "金髮",
+"银朱" => "銀硃",
+"银发" => "銀髮",
+"铜制" => "銅製",
+"铝制" => "鋁製",
+"钢制" => "鋼製",
+"录着" => "錄著",
+"录制" => "錄製",
+"表带" => "錶帶",
+"表店" => "錶店",
+"表厂" => "錶廠",
+"表壳" => "錶殼",
+"表链" => "錶鏈",
+"表面" => "錶面",
+"锅台" => "鍋臺",
+"锻鍊出" => "鍛鍊出",
+"锻鍊身体" => "鍛鍊身体",
+"镜台" => "鏡臺",
+"锈病" => "鏽病",
+"锈菌" => "鏽菌",
+"锈蚀" => "鏽蝕",
+"钟表" => "鐘錶",
+"铁锈" => "鐵鏽",
+"长征" => "長征",
+"长发" => "長髮",
+"长须鲸" => "長鬚鯨",
+"门帘" => "門帘",
+"门斗" => "門斗",
+"门里" => "門裡",
+"开伙" => "開伙",
+"开卷" => "開卷",
+"开诚布公" => "開誠佈公",
+"开采" => "開采",
+"閒情逸致" => "閒情逸緻",
+"间不容发" => "間不容髮",
+"闵采尔" => "閔采爾",
+"阅卷" => "閱卷",
+"阑干" => "闌干",
+"关系" => "關係",
+"关系着" => "關係著",
+"防御" => "防禦",
+"防锈" => "防鏽",
+"防台" => "防颱",
+"阿斗" => "阿斗",
+"阿里" => "阿裡",
+"除旧布新" => "除舊佈新",
+"阴干" => "陰干",
+"阴历" => "陰曆",
+"阴郁" => "陰郁",
+"陆征祥" => "陸征祥",
+"阳春面" => "陽春麵",
+"阳历" => "陽曆",
+"阳台" => "陽臺",
+"只字" => "隻字",
+"只影" => "隻影",
+"只手遮天" => "隻手遮天",
+"只眼" => "隻眼",
+"只言片语" => "隻言片語",
+"只身" => "隻身",
+"雅致" => "雅緻",
+"雇佣" => "雇佣",
+"双折" => "雙摺",
+"杂志" => "雜誌",
+"鸡丝" => "雞絲",
+"鸡丝面" => "雞絲麵",
+"鸡腿面" => "雞腿麵",
+"鸡只" => "雞隻",
+"难舍" => "難捨",
+"雨花台" => "雨花臺",
+"雪里" => "雪裡",
+"云须" => "雲鬚",
+"电子表" => "電子錶",
+"电台" => "電臺",
+"电冲" => "電衝",
+"电复" => "電覆",
+"电视台" => "電視臺",
+"电表" => "電錶",
+"雾台" => "霧臺",
+"雾里" => "霧裡",
+"露台" => "露臺",
+"灵台" => "靈臺",
+"青瓦台" => "青瓦臺",
+"青霉" => "青黴",
+"面朝着" => "面朝著",
+"面临着" => "面臨著",
+"鞋里" => "鞋裡",
+"鞣制" => "鞣製",
+"秋千" => "鞦韆",
+"鞭辟入里" => "鞭辟入裡",
+"韩国制" => "韓國製",
+"韩制" => "韓製",
+"颂系" => "頌繫",
+"预制" => "預製",
+"颁布" => "頒佈",
+"头里" => "頭裡",
+"头发" => "頭髮",
+"颊须" => "頰鬚",
+"颠仆" => "顛仆",
+"颠复" => "顛複",
+"颠复" => "顛覆",
+"显着标志" => "顯著標志",
+"风土志" => "風土誌",
+"风斗" => "風斗",
+"风物志" => "風物誌",
+"风里" => "風裡",
+"风采" => "風采",
+"台风" => "颱風",
+"刮了" => "颳了",
+"刮倒" => "颳倒",
+"刮去" => "颳去",
+"刮得" => "颳得",
+"刮着" => "颳著",
+"刮走" => "颳走",
+"刮起" => "颳起",
+"刮风" => "颳風",
+"饭团" => "飯糰",
+"饼干" => "餅干",
+"馄饨面" => "餛飩麵",
+"饥不择食" => "饑不擇食",
+"饥寒" => "饑寒",
+"饥民" => "饑民",
+"饥渴" => "饑渴",
+"饥溺" => "饑溺",
+"饥荒" => "饑荒",
+"饥饱" => "饑飽",
+"饥饿" => "饑餓",
+"饥馑" => "饑饉",
+"首当其冲" => "首當其衝",
+"香郁" => "香郁",
+"馥郁" => "馥郁",
+"马里" => "馬裡",
+"马表" => "馬錶",
+"腾冲" => "騰衝",
+"骨子里" => "骨子裡",
+"骨干" => "骨幹",
+"骨灰坛" => "骨灰罈",
+"肮脏" => "骯髒",
+"脏乱" => "髒亂",
+"脏了" => "髒了",
+"脏兮兮" => "髒兮兮",
+"脏字" => "髒字",
+"脏得" => "髒得",
+"脏东西" => "髒東西",
+"脏水" => "髒水",
+"脏的" => "髒的",
+"脏话" => "髒話",
+"脏钱" => "髒錢",
+"高干" => "高幹",
+"高台" => "高臺",
+"髭须" => "髭鬚",
+"发型" => "髮型",
+"发夹" => "髮夾",
+"发妻" => "髮妻",
+"发姐" => "髮姐",
+"发带" => "髮帶",
+"发廊" => "髮廊",
+"发式" => "髮式",
+"发指" => "髮指",
+"发捲" => "髮捲",
+"发根" => "髮根",
+"发毛" => "髮毛",
+"发油" => "髮油",
+"发状" => "髮狀",
+"发短心长" => "髮短心長",
+"发端" => "髮端",
+"发结" => "髮結",
+"发丝" => "髮絲",
+"发网" => "髮網",
+"发肤" => "髮膚",
+"发胶" => "髮膠",
+"发菜" => "髮菜",
+"发蜡" => "髮蠟",
+"发辫" => "髮辮",
+"发针" => "髮針",
+"发长" => "髮長",
+"发际" => "髮際",
+"发雕" => "髮雕",
+"发霜" => "髮霜",
+"发髻" => "髮髻",
+"发鬓" => "髮鬢",
+"鬅松" => "鬅鬆",
+"松了" => "鬆了",
+"松些" => "鬆些",
+"松劲" => "鬆勁",
+"松动" => "鬆動",
+"松口" => "鬆口",
+"松土" => "鬆土",
+"松弛" => "鬆弛",
+"松快" => "鬆快",
+"松懈" => "鬆懈",
+"松手" => "鬆手",
+"松掉" => "鬆掉",
+"松散" => "鬆散",
+"松林" => "鬆林",
+"松柔" => "鬆柔",
+"松毛虫" => "鬆毛蟲",
+"松浮" => "鬆浮",
+"松涛" => "鬆濤",
+"松科" => "鬆科",
+"松节油" => "鬆節油",
+"松绑" => "鬆綁",
+"松紧" => "鬆緊",
+"松缓" => "鬆緩",
+"松脆" => "鬆脆",
+"松脱" => "鬆脫",
+"松起" => "鬆起",
+"松软" => "鬆軟",
+"松通" => "鬆通",
+"松开" => "鬆開",
+"松饼" => "鬆餅",
+"松松" => "鬆鬆",
+"鬈发" => "鬈髮",
+"胡子" => "鬍子",
+"胡梢" => "鬍梢",
+"胡渣" => "鬍渣",
+"胡髭" => "鬍髭",
+"胡须" => "鬍鬚",
+"须根" => "鬚根",
+"须毛" => "鬚毛",
+"须生" => "鬚生",
+"须眉" => "鬚眉",
+"须发" => "鬚髮",
+"须须" => "鬚鬚",
+"鬓发" => "鬢髮",
+"斗着" => "鬥著",
+"闹着玩儿" => "鬧著玩儿",
+"闹着玩儿" => "鬧著玩兒",
+"郁郁" => "鬱郁",
+"魂牵梦系" => "魂牽夢繫",
+"鱼松" => "魚鬆",
+"鲸须" => "鯨鬚",
+"鲇鱼" => "鯰魚",
+"鸿篇巨制" => "鴻篇巨製",
+"鹤发" => "鶴髮",
+"卤化" => "鹵化",
+"卤味" => "鹵味",
+"卤族" => "鹵族",
+"卤水" => "鹵水",
+"卤汁" => "鹵汁",
+"卤簿" => "鹵簿",
+"卤素" => "鹵素",
+"卤莽" => "鹵莽",
+"卤钝" => "鹵鈍",
+"咸味" => "鹹味",
+"咸土" => "鹹土",
+"咸度" => "鹹度",
+"咸得" => "鹹得",
+"咸水" => "鹹水",
+"咸海" => "鹹海",
+"咸淡" => "鹹淡",
+"咸湖" => "鹹湖",
+"咸汤" => "鹹湯",
+"咸的" => "鹹的",
+"咸肉" => "鹹肉",
+"咸菜" => "鹹菜",
+"咸蛋" => "鹹蛋",
+"咸猪肉" => "鹹豬肉",
+"咸类" => "鹹類",
+"咸鱼" => "鹹魚",
+"咸鸭蛋" => "鹹鴨蛋",
+"咸卤" => "鹹鹵",
+"咸咸" => "鹹鹹",
+"盐卤" => "鹽鹵",
+"面价" => "麵價",
+"面包" => "麵包",
+"面团" => "麵團",
+"面店" => "麵店",
+"面厂" => "麵廠",
+"面摊" => "麵攤",
+"面杖" => "麵杖",
+"面条" => "麵條",
+"面灰" => "麵灰",
+"面皮" => "麵皮",
+"面筋" => "麵筋",
+"面粉" => "麵粉",
+"面糊" => "麵糊",
+"面线" => "麵線",
+"面茶" => "麵茶",
+"面食" => "麵食",
+"面饺" => "麵餃",
+"面饼" => "麵餅",
+"麻酱面" => "麻醬麵",
+"黄卷" => "黃卷",
+"黄历" => "黃曆",
+"黄发" => "黃髮",
+"黑发" => "黑髮",
+"黑松" => "黑鬆",
+"霉毒" => "黴毒",
+"霉素" => "黴素",
+"霉菌" => "黴菌",
+"鼓里" => "鼓裡",
+"冬冬" => "鼕鼕",
+"龙卷" => "龍卷",
+"龙须" => "龍鬚",
+"内存"=>"記憶體",
+"默认"=>"預設",
+"缺省"=>"預設",
+"串行"=>"串列",
+"以太网"=>"乙太網",
+"位图"=>"點陣圖",
+"例程"=>"常式",
+"信道"=>"通道",
+"光标"=>"游標",
+"光盘"=>"光碟",
+"光驱"=>"光碟機",
+"全角"=>"全形",
+"共享"=>"共用",
+"兼容"=>"相容",
+"前缀"=>"首碼",
+"后缀"=>"尾碼",
+"加载"=>"載入",
+"半角"=>"半形",
+"变量"=>"變數",
+"噪声"=>"雜訊",
+"因子"=>"因數",
+"在线"=>"線上",
+"脱机"=>"離線",
+"域名"=>"功能變數名稱",
+"声卡"=>"音效卡",
+"字号"=>"字型大小",
+"字库"=>"字型檔",
+"字段"=>"欄位",
+"字符"=>"字元",
+"存盘"=>"存檔",
+"寻址"=>"定址",
+"尾注"=>"章節附註",
+"异步"=>"非同步",
+"总线"=>"匯流排",
+"括号"=>"括弧",
+"接口"=>"介面",
+"控件"=>"控制項",
+"权限"=>"許可權",
+"盘片"=>"碟片",
+"硅片"=>"矽片",
+"硅谷"=>"矽谷",
+"硬盘"=>"硬碟",
+"磁盘"=>"磁碟",
+"磁道"=>"磁軌",
+"程控"=>"程式控制",
+"端口"=>"埠",
+"算子"=>"運算元",
+"算法"=>"演算法",
+"芯片"=>"晶片",
+"芯片"=>"晶元",
+"词组"=>"片語",
+"译码"=>"解碼",
+"软驱"=>"軟碟機",
+"闪存"=>"快閃記憶體",
+"鼠标"=>"滑鼠",
+"进制"=>"進位",
+"交互式"=>"互動式",
+"仿真"=>"模擬",
+"优先级"=>"優先順序",
+"传感"=>"感測",
+"便携式"=>"攜帶型",
+"信息论"=>"資訊理論",
+"循环"=>"迴圈",
+"写保护"=>"防寫",
+"分布式"=>"分散式",
+"分辨率"=>"解析度",
+"程序"=>"程式",
+"服务器"=>"伺服器",
+"等于"=>"等於",
+"局域网"=>"區域網",
+"上载"=>"上傳",
+"计算机"=>"電腦",
+"宏"=>"巨集",
+"扫瞄仪"=>"掃瞄器",
+"宽带"=>"寬頻",
+"窗口"=>"視窗",
+"数据库"=>"資料庫",
+"公历"=>"西曆",
+"奶酪"=>"乳酪",
+"巨商"=>"鉅賈",
+"手电"=>"手電筒",
+"万历"=>"萬曆",
+"永历"=>"永曆",
+"词汇"=>"辭彙",
+"保安"=>"保全",
+"习用"=>"慣用",
+"元音"=>"母音",
+"任意球"=>"自由球",
+"头球"=>"頭槌",
+"入球"=>"進球",
+"粒入球"=>"顆進球",
+"打门"=>"射門",
+"火锅盖帽"=>"蓋火鍋",
+"打印机"=>"印表機",
+"打印機"=>"印表機",
+"字节"=>"位元組",
+"字節"=>"位元組",
+"打印"=>"列印",
+"打印"=>"列印",
+"硬件"=>"硬體",
+"硬件"=>"硬體",
+"二极管"=>"二極體",
+"二極管"=>"二極體",
+"三极管"=>"三極體",
+"三極管"=>"三極體",
+"数码"=>"數位",
+"數碼"=>"數位",
+"软件"=>"軟體",
+"軟件"=>"軟體",
+"网络"=>"網路",
+"網絡"=>"網路",
+"人工智能"=>"人工智慧",
+"航天飞机"=>"太空梭",
+"穿梭機"=>"太空梭",
+"因特网"=>"網際網路",
+"互聯網"=>"網際網路",
+"机器人"=>"機器人",
+"機械人"=>"機器人",
+"移动电话"=>"行動電話",
+"流動電話"=>"行動電話",
+"调制解调器"=>"數據機",
+"調制解調器"=>"數據機",
+"短信"=>"簡訊",
+"短訊"=>"簡訊",
+"乌兹别克斯坦"=>"烏茲別克",
+"乍得"=>"查德",
+"乍得"=>"查德",
+"也门"=>"葉門",
+"也門"=>"葉門",
+"伯利兹"=>"貝里斯",
+"伯利茲"=>"貝里斯",
+"佛得角"=>"維德角",
+"佛得角"=>"維德角",
+"克罗地亚"=>"克羅埃西亞",
+"克羅地亞"=>"克羅埃西亞",
+"冈比亚"=>"甘比亞",
+"岡比亞"=>"甘比亞",
+"几内亚比绍"=>"幾內亞比索",
+"幾內亞比紹"=>"幾內亞比索",
+"列支敦士登"=>"列支敦斯登",
+"列支敦士登"=>"列支敦斯登",
+"利比里亚"=>"賴比瑞亞",
+"利比里亞"=>"賴比瑞亞",
+"加纳"=>"迦納",
+"加納"=>"迦納",
+"加蓬"=>"加彭",
+"加蓬"=>"加彭",
+"博茨瓦纳"=>"波札那",
+"博茨瓦納"=>"波札那",
+"卡塔尔"=>"卡達",
+"卡塔爾"=>"卡達",
+"卢旺达"=>"盧安達",
+"盧旺達"=>"盧安達",
+"危地马拉"=>"瓜地馬拉",
+"危地馬拉"=>"瓜地馬拉",
+"厄瓜多尔"=>"厄瓜多",
+"厄瓜多爾"=>"厄瓜多",
+"厄立特里亚"=>"厄利垂亞",
+"厄立特里亞"=>"厄利垂亞",
+"吉布提"=>"吉布地",
+"吉布堤"=>"吉布地",
+"哈萨克斯坦"=>"哈薩克",
+"哥斯达黎加"=>"哥斯大黎加",
+"哥斯達黎加"=>"哥斯大黎加",
+"图瓦卢"=>"吐瓦魯",
+"圖瓦盧"=>"吐瓦魯",
+"土库曼斯坦"=>"土庫曼",
+"圣卢西亚"=>"聖露西亞",
+"聖盧西亞"=>"聖露西亞",
+"圣基茨和尼维斯"=>"聖克里斯多福及尼維斯",
+"聖吉斯納域斯"=>"聖克里斯多福及尼維斯",
+"圣文森特和格林纳丁斯"=>"聖文森及格瑞那丁",
+"聖文森特和格林納丁斯"=>"聖文森及格瑞那丁",
+"圣马力诺"=>"聖馬利諾",
+"聖馬力諾"=>"聖馬利諾",
+"圭亚那"=>"蓋亞那",
+"圭亞那"=>"蓋亞那",
+"坦桑尼亚"=>"坦尚尼亞",
+"坦桑尼亞"=>"坦尚尼亞",
+"埃塞俄比亚"=>"衣索比亞",
+"埃塞俄比亞"=>"衣索比亞",
+"基里巴斯"=>"吉里巴斯",
+"基里巴斯"=>"吉里巴斯",
+"塔吉克斯坦"=>"塔吉克",
+"塞拉利昂"=>"獅子山",
+"塞拉利昂"=>"獅子山",
+"塞浦路斯"=>"塞普勒斯",
+"塞浦路斯"=>"塞普勒斯",
+"塞舌尔"=>"塞席爾",
+"塞舌爾"=>"塞席爾",
+"多米尼加"=>"多明尼加",
+"多明尼加共和國"=>"多明尼加",
+"多米尼加联邦"=>"多米尼克",
+"多明尼加聯邦"=>"多米尼克",
+"安提瓜和巴布达"=>"安地卡及巴布達",
+"安提瓜和巴布達"=>"安地卡及巴布達",
+"尼日利亚"=>"奈及利亞",
+"尼日利亞"=>"奈及利亞",
+"尼日尔"=>"尼日",
+"尼日爾"=>"尼日",
+"巴巴多斯"=>"巴貝多",
+"巴巴多斯"=>"巴貝多",
+"巴布亚新几内亚"=>"巴布亞紐幾內亞",
+"巴布亞新畿內亞"=>"巴布亞紐幾內亞",
+"布基纳法索"=>"布吉納法索",
+"布基納法索"=>"布吉納法索",
+"布隆迪"=>"蒲隆地",
+"布隆迪"=>"蒲隆地",
+"希腊"=>"希臘",
+"帕劳"=>"帛琉",
+"意大利"=>"義大利",
+"意大利"=>"義大利",
+"所罗门群岛"=>"索羅門群島",
+"所羅門群島"=>"索羅門群島",
+"文莱"=>"汶萊",
+"斯威士兰"=>"史瓦濟蘭",
+"斯威士蘭"=>"史瓦濟蘭",
+"斯洛文尼亚"=>"斯洛維尼亞",
+"斯洛文尼亞"=>"斯洛維尼亞",
+"新西兰"=>"紐西蘭",
+"新西蘭"=>"紐西蘭",
+"朝鲜"=>"北韓",
+"格林纳达"=>"格瑞那達",
+"格林納達"=>"格瑞那達",
+"格鲁吉亚"=>"喬治亞",
+"格魯吉亞"=>"喬治亞",
+"梵蒂冈"=>"教廷",
+"梵蒂岡"=>"教廷",
+"毛里塔尼亚"=>"茅利塔尼亞",
+"毛里塔尼亞"=>"茅利塔尼亞",
+"毛里求斯"=>"模里西斯",
+"毛里裘斯"=>"模里西斯",
+"沙特阿拉伯"=>"沙烏地阿拉伯",
+"沙地阿拉伯"=>"沙烏地阿拉伯",
+"波斯尼亚和黑塞哥维那"=>"波士尼亞赫塞哥維納",
+"波斯尼亞黑塞哥維那"=>"波士尼亞赫塞哥維納",
+"津巴布韦"=>"辛巴威",
+"津巴布韋"=>"辛巴威",
+"洪都拉斯"=>"宏都拉斯",
+"洪都拉斯"=>"宏都拉斯",
+"特立尼达和托巴哥"=>"千里達托貝哥",
+"特立尼達和多巴哥"=>"千里達托貝哥",
+"瑙鲁"=>"諾魯",
+"瑙魯"=>"諾魯",
+"瓦努阿图"=>"萬那杜",
+"瓦努阿圖"=>"萬那杜",
+"溫納圖萬"=>"那杜",
+"科摩罗"=>"葛摩",
+"科摩羅"=>"葛摩",
+"科特迪瓦"=>"象牙海岸",
+"突尼斯"=>"突尼西亞",
+"索马里"=>"索馬利亞",
+"索馬里"=>"索馬利亞",
+"老挝"=>"寮國",
+"老撾"=>"寮國",
+"肯尼亚"=>"肯亞",
+"肯雅"=>"肯亞",
+"苏里南"=>"蘇利南",
+"莫桑比克"=>"莫三比克",
+"莱索托"=>"賴索托",
+"萊索托"=>"賴索托",
+"贝宁"=>"貝南",
+"貝寧"=>"貝南",
+"赞比亚"=>"尚比亞",
+"贊比亞"=>"尚比亞",
+"阿塞拜疆"=>"亞塞拜然",
+"阿塞拜疆"=>"亞塞拜然",
+"阿拉伯联合酋长国"=>"阿拉伯聯合大公國",
+"阿拉伯聯合酋長國"=>"阿拉伯聯合大公國",
+"韩国"=>"南韓",
+"马尔代夫"=>"馬爾地夫",
+"馬爾代夫"=>"馬爾地夫",
+"马耳他"=>"馬爾他",
+"马里"=>"馬利",
+"馬里"=>"馬利",
+"方便面"=>"速食麵",
+"快速面"=>"速食麵",
+"即食麵"=>"速食麵",
+"薯仔"=>"土豆",
+"蹦极跳"=>"笨豬跳",
+"绑紧跳"=>"笨豬跳",
+"冷菜"=>"冷盤",
+"凉菜"=>"冷盤",
+"的士"=>"計程車",
+"出租车"=>"計程車",
+"巴士"=>"公車",
+"公共汽车"=>"公車",
+"台球"=>"撞球",
+"桌球"=>"撞球",
+"雪糕"=>"冰淇淋",
+"卫生"=>"衛生",
+"衞生"=>"衛生",
+"平治"=>"賓士",
+"奔驰"=>"賓士",
+"積架"=>"捷豹",
+"福士"=>"福斯",
+"雪铁龙"=>"雪鐵龍",
+"马自达"=>"馬自達",
+"萬事得"=>"馬自達",
+"布什"=>"布希",
+"布殊"=>"布希",
+"克林顿"=>"柯林頓",
+"克林頓"=>"柯林頓",
+"萨达姆"=>"海珊",
+"薩達姆"=>"海珊",
+"凡高"=>"梵谷",
+"狄安娜"=>"黛安娜",
+"戴安娜"=>"黛安娜",
+"赫拉"=>"希拉",
+);
+
+
+$zh2CN=array(
+"么"=>"么",
+"瀋"=>"沈",
+"畫"=>"划",
+"鍾"=>"钟",
+"餘"=>"余",
+"鯰"=>"鲇",
+"鹼"=>"硷",
+"麼"=>"么",
+"䊷"=>"䌶",
+"𧩙"=>"䜥",
+"万"=>"万",
+"与"=>"与",
+"丑"=>"丑",
+"丟"=>"丢",
+"並"=>"并",
+"丰"=>"丰",
+"么"=>"么",
+"乾"=>"干",
+"亂"=>"乱",
+"云"=>"云",
+"亙"=>"亘",
+"亞"=>"亚",
+"仆"=>"仆",
+"价"=>"价",
+"伙"=>"伙",
+"佇"=>"伫",
+"佈"=>"布",
+"体"=>"体",
+"余"=>"余",
+"余"=>"馀",
+"佣"=>"佣",
+"併"=>"并",
+"來"=>"来",
+"侖"=>"仑",
+"侶"=>"侣",
+"俁"=>"俣",
+"係"=>"系",
+"俔"=>"伣",
+"俠"=>"侠",
+"倀"=>"伥",
+"倆"=>"俩",
+"倈"=>"俫",
+"倉"=>"仓",
+"個"=>"个",
+"們"=>"们",
+"倫"=>"伦",
+"偉"=>"伟",
+"側"=>"侧",
+"偵"=>"侦",
+"偽"=>"伪",
+"傑"=>"杰",
+"傖"=>"伧",
+"傘"=>"伞",
+"備"=>"备",
+"傢"=>"家",
+"傭"=>"佣",
+"傯"=>"偬",
+"傳"=>"传",
+"傴"=>"伛",
+"債"=>"债",
+"傷"=>"伤",
+"傾"=>"倾",
+"僂"=>"偻",
+"僅"=>"仅",
+"僉"=>"佥",
+"僑"=>"侨",
+"僕"=>"仆",
+"僞"=>"伪",
+"僥"=>"侥",
+"僨"=>"偾",
+"價"=>"价",
+"儀"=>"仪",
+"儂"=>"侬",
+"億"=>"亿",
+"儈"=>"侩",
+"儉"=>"俭",
+"儐"=>"傧",
+"儔"=>"俦",
+"儕"=>"侪",
+"儘"=>"尽",
+"償"=>"偿",
+"優"=>"优",
+"儲"=>"储",
+"儷"=>"俪",
+"儺"=>"傩",
+"儻"=>"傥",
+"儼"=>"俨",
+"儿"=>"儿",
+"兇"=>"凶",
+"兌"=>"兑",
+"兒"=>"儿",
+"兗"=>"兖",
+"党"=>"党",
+"內"=>"内",
+"兩"=>"两",
+"冊"=>"册",
+"冪"=>"幂",
+"准"=>"准",
+"凈"=>"净",
+"凍"=>"冻",
+"凜"=>"凛",
+"几"=>"几",
+"凱"=>"凯",
+"划"=>"划",
+"別"=>"别",
+"刪"=>"删",
+"剄"=>"刭",
+"則"=>"则",
+"剋"=>"克",
+"剎"=>"刹",
+"剗"=>"刬",
+"剛"=>"刚",
+"剝"=>"剥",
+"剮"=>"剐",
+"剴"=>"剀",
+"創"=>"创",
+"劃"=>"划",
+"劇"=>"剧",
+"劉"=>"刘",
+"劊"=>"刽",
+"劌"=>"刿",
+"劍"=>"剑",
+"劑"=>"剂",
+"勁"=>"劲",
+"動"=>"动",
+"務"=>"务",
+"勛"=>"勋",
+"勝"=>"胜",
+"勞"=>"劳",
+"勢"=>"势",
+"勩"=>"勚",
+"勱"=>"劢",
+"勵"=>"励",
+"勸"=>"劝",
+"勻"=>"匀",
+"匭"=>"匦",
+"匯"=>"汇",
+"匱"=>"匮",
+"區"=>"区",
+"協"=>"协",
+"卷"=>"卷",
+"卻"=>"却",
+"厂"=>"厂",
+"厙"=>"厍",
+"厠"=>"厕",
+"厭"=>"厌",
+"厲"=>"厉",
+"厴"=>"厣",
+"參"=>"参",
+"叄"=>"叁",
+"叢"=>"丛",
+"台"=>"台",
+"叶"=>"叶",
+"吊"=>"吊",
+"后"=>"后",
+"后"=>"後",
+"吒"=>"咤",
+"吳"=>"吴",
+"吶"=>"呐",
+"呂"=>"吕",
+"咼"=>"呙",
+"員"=>"员",
+"唄"=>"呗",
+"唚"=>"吣",
+"問"=>"问",
+"啓"=>"启",
+"啞"=>"哑",
+"啟"=>"启",
+"啢"=>"唡",
+"喎"=>"㖞",
+"喚"=>"唤",
+"喪"=>"丧",
+"喬"=>"乔",
+"單"=>"单",
+"喲"=>"哟",
+"嗆"=>"呛",
+"嗇"=>"啬",
+"嗊"=>"唝",
+"嗎"=>"吗",
+"嗚"=>"呜",
+"嗩"=>"唢",
+"嗶"=>"哔",
+"嘆"=>"叹",
+"嘍"=>"喽",
+"嘔"=>"呕",
+"嘖"=>"啧",
+"嘗"=>"尝",
+"嘜"=>"唛",
+"嘩"=>"哗",
+"嘮"=>"唠",
+"嘯"=>"啸",
+"嘰"=>"叽",
+"嘵"=>"哓",
+"嘸"=>"呒",
+"嘽"=>"啴",
+"噁"=>"恶",
+"噓"=>"嘘",
+"噝"=>"咝",
+"噠"=>"哒",
+"噥"=>"哝",
+"噦"=>"哕",
+"噯"=>"嗳",
+"噲"=>"哙",
+"噴"=>"喷",
+"噸"=>"吨",
+"噹"=>"当",
+"嚀"=>"咛",
+"嚇"=>"吓",
+"嚌"=>"哜",
+"嚕"=>"噜",
+"嚙"=>"啮",
+"嚥"=>"咽",
+"嚦"=>"呖",
+"嚨"=>"咙",
+"嚮"=>"向",
+"嚲"=>"亸",
+"嚳"=>"喾",
+"嚴"=>"严",
+"嚶"=>"嘤",
+"囀"=>"啭",
+"囁"=>"嗫",
+"囂"=>"嚣",
+"囅"=>"冁",
+"囈"=>"呓",
+"囌"=>"苏",
+"囑"=>"嘱",
+"囪"=>"囱",
+"圇"=>"囵",
+"國"=>"国",
+"圍"=>"围",
+"園"=>"园",
+"圓"=>"圆",
+"圖"=>"图",
+"團"=>"团",
+"坏"=>"坏",
+"垵"=>"埯",
+"埡"=>"垭",
+"埰"=>"采",
+"執"=>"执",
+"堅"=>"坚",
+"堊"=>"垩",
+"堖"=>"垴",
+"堝"=>"埚",
+"堯"=>"尧",
+"報"=>"报",
+"場"=>"场",
+"塊"=>"块",
+"塋"=>"茔",
+"塏"=>"垲",
+"塒"=>"埘",
+"塗"=>"涂",
+"塚"=>"冢",
+"塢"=>"坞",
+"塤"=>"埙",
+"塵"=>"尘",
+"塹"=>"堑",
+"墊"=>"垫",
+"墜"=>"坠",
+"墮"=>"堕",
+"墳"=>"坟",
+"墻"=>"墙",
+"墾"=>"垦",
+"壇"=>"坛",
+"壈"=>"𡒄",
+"壋"=>"垱",
+"壓"=>"压",
+"壘"=>"垒",
+"壙"=>"圹",
+"壚"=>"垆",
+"壞"=>"坏",
+"壟"=>"垄",
+"壠"=>"垅",
+"壢"=>"坜",
+"壩"=>"坝",
+"壯"=>"壮",
+"壺"=>"壶",
+"壼"=>"壸",
+"壽"=>"寿",
+"夠"=>"够",
+"夢"=>"梦",
+"夾"=>"夹",
+"奐"=>"奂",
+"奧"=>"奥",
+"奩"=>"奁",
+"奪"=>"夺",
+"奬"=>"奖",
+"奮"=>"奋",
+"奼"=>"姹",
+"妝"=>"妆",
+"姍"=>"姗",
+"姜"=>"姜",
+"姦"=>"奸",
+"娛"=>"娱",
+"婁"=>"娄",
+"婦"=>"妇",
+"婭"=>"娅",
+"媧"=>"娲",
+"媯"=>"妫",
+"媼"=>"媪",
+"媽"=>"妈",
+"嫗"=>"妪",
+"嫵"=>"妩",
+"嫻"=>"娴",
+"嫿"=>"婳",
+"嬀"=>"妫",
+"嬈"=>"娆",
+"嬋"=>"婵",
+"嬌"=>"娇",
+"嬙"=>"嫱",
+"嬡"=>"嫒",
+"嬤"=>"嬷",
+"嬪"=>"嫔",
+"嬰"=>"婴",
+"嬸"=>"婶",
+"孌"=>"娈",
+"孫"=>"孙",
+"學"=>"学",
+"孿"=>"孪",
+"宁"=>"宁",
+"宮"=>"宫",
+"寢"=>"寝",
+"實"=>"实",
+"寧"=>"宁",
+"審"=>"审",
+"寫"=>"写",
+"寬"=>"宽",
+"寵"=>"宠",
+"寶"=>"宝",
+"將"=>"将",
+"專"=>"专",
+"尋"=>"寻",
+"對"=>"对",
+"導"=>"导",
+"尷"=>"尴",
+"屆"=>"届",
+"屍"=>"尸",
+"屓"=>"屃",
+"屜"=>"屉",
+"屢"=>"屡",
+"層"=>"层",
+"屨"=>"屦",
+"屬"=>"属",
+"岡"=>"冈",
+"峴"=>"岘",
+"島"=>"岛",
+"峽"=>"峡",
+"崍"=>"崃",
+"崗"=>"岗",
+"崢"=>"峥",
+"崬"=>"岽",
+"嵐"=>"岚",
+"嶁"=>"嵝",
+"嶄"=>"崭",
+"嶇"=>"岖",
+"嶔"=>"嵚",
+"嶗"=>"崂",
+"嶠"=>"峤",
+"嶢"=>"峣",
+"嶧"=>"峄",
+"嶮"=>"崄",
+"嶴"=>"岙",
+"嶸"=>"嵘",
+"嶺"=>"岭",
+"嶼"=>"屿",
+"嶽"=>"岳",
+"巋"=>"岿",
+"巒"=>"峦",
+"巔"=>"巅",
+"巰"=>"巯",
+"帘"=>"帘",
+"帥"=>"帅",
+"師"=>"师",
+"帳"=>"帐",
+"帶"=>"带",
+"幀"=>"帧",
+"幃"=>"帏",
+"幗"=>"帼",
+"幘"=>"帻",
+"幟"=>"帜",
+"幣"=>"币",
+"幫"=>"帮",
+"幬"=>"帱",
+"幹"=>"干",
+"幺"=>"么",
+"幾"=>"几",
+"广"=>"广",
+"庫"=>"库",
+"廁"=>"厕",
+"廂"=>"厢",
+"廄"=>"厩",
+"廈"=>"厦",
+"廚"=>"厨",
+"廝"=>"厮",
+"廟"=>"庙",
+"廠"=>"厂",
+"廡"=>"庑",
+"廢"=>"废",
+"廣"=>"广",
+"廩"=>"廪",
+"廬"=>"庐",
+"廳"=>"厅",
+"弒"=>"弑",
+"弳"=>"弪",
+"張"=>"张",
+"強"=>"强",
+"彆"=>"别",
+"彈"=>"弹",
+"彌"=>"弥",
+"彎"=>"弯",
+"彙"=>"汇",
+"彞"=>"彝",
+"彥"=>"彦",
+"征"=>"征",
+"後"=>"后",
+"徑"=>"径",
+"從"=>"从",
+"徠"=>"徕",
+"復"=>"复",
+"徵"=>"征",
+"徹"=>"彻",
+"恆"=>"恒",
+"恥"=>"耻",
+"悅"=>"悦",
+"悞"=>"悮",
+"悵"=>"怅",
+"悶"=>"闷",
+"惡"=>"恶",
+"惱"=>"恼",
+"惲"=>"恽",
+"惻"=>"恻",
+"愛"=>"爱",
+"愜"=>"惬",
+"愨"=>"悫",
+"愴"=>"怆",
+"愷"=>"恺",
+"愾"=>"忾",
+"愿"=>"愿",
+"慄"=>"栗",
+"態"=>"态",
+"慍"=>"愠",
+"慘"=>"惨",
+"慚"=>"惭",
+"慟"=>"恸",
+"慣"=>"惯",
+"慤"=>"悫",
+"慪"=>"怄",
+"慫"=>"怂",
+"慮"=>"虑",
+"慳"=>"悭",
+"慶"=>"庆",
+"憂"=>"忧",
+"憊"=>"惫",
+"憐"=>"怜",
+"憑"=>"凭",
+"憒"=>"愦",
+"憚"=>"惮",
+"憤"=>"愤",
+"憫"=>"悯",
+"憮"=>"怃",
+"憲"=>"宪",
+"憶"=>"忆",
+"懇"=>"恳",
+"應"=>"应",
+"懌"=>"怿",
+"懍"=>"懔",
+"懞"=>"蒙",
+"懟"=>"怼",
+"懣"=>"懑",
+"懨"=>"恹",
+"懲"=>"惩",
+"懶"=>"懒",
+"懷"=>"怀",
+"懸"=>"悬",
+"懺"=>"忏",
+"懼"=>"惧",
+"懾"=>"慑",
+"戀"=>"恋",
+"戇"=>"戆",
+"戔"=>"戋",
+"戧"=>"戗",
+"戩"=>"戬",
+"戰"=>"战",
+"戱"=>"戯",
+"戲"=>"戏",
+"戶"=>"户",
+"担"=>"担",
+"拋"=>"抛",
+"拾"=>"十",
+"挩"=>"捝",
+"挾"=>"挟",
+"捨"=>"舍",
+"捫"=>"扪",
+"据"=>"据",
+"掃"=>"扫",
+"掄"=>"抡",
+"掗"=>"挜",
+"掙"=>"挣",
+"掛"=>"挂",
+"採"=>"采",
+"揀"=>"拣",
+"揚"=>"扬",
+"換"=>"换",
+"揮"=>"挥",
+"損"=>"损",
+"搖"=>"摇",
+"搗"=>"捣",
+"搵"=>"揾",
+"搶"=>"抢",
+"摑"=>"掴",
+"摜"=>"掼",
+"摟"=>"搂",
+"摯"=>"挚",
+"摳"=>"抠",
+"摶"=>"抟",
+"摺"=>"折",
+"摻"=>"掺",
+"撈"=>"捞",
+"撏"=>"挦",
+"撐"=>"撑",
+"撓"=>"挠",
+"撝"=>"㧑",
+"撟"=>"挢",
+"撣"=>"掸",
+"撥"=>"拨",
+"撫"=>"抚",
+"撲"=>"扑",
+"撳"=>"揿",
+"撻"=>"挞",
+"撾"=>"挝",
+"撿"=>"捡",
+"擁"=>"拥",
+"擄"=>"掳",
+"擇"=>"择",
+"擊"=>"击",
+"擋"=>"挡",
+"擓"=>"㧟",
+"擔"=>"担",
+"據"=>"据",
+"擠"=>"挤",
+"擬"=>"拟",
+"擯"=>"摈",
+"擰"=>"拧",
+"擱"=>"搁",
+"擲"=>"掷",
+"擴"=>"扩",
+"擷"=>"撷",
+"擺"=>"摆",
+"擻"=>"擞",
+"擼"=>"撸",
+"擾"=>"扰",
+"攄"=>"摅",
+"攆"=>"撵",
+"攏"=>"拢",
+"攔"=>"拦",
+"攖"=>"撄",
+"攙"=>"搀",
+"攛"=>"撺",
+"攜"=>"携",
+"攝"=>"摄",
+"攢"=>"攒",
+"攣"=>"挛",
+"攤"=>"摊",
+"攪"=>"搅",
+"攬"=>"揽",
+"敗"=>"败",
+"敘"=>"叙",
+"敵"=>"敌",
+"數"=>"数",
+"斂"=>"敛",
+"斃"=>"毙",
+"斕"=>"斓",
+"斗"=>"斗",
+"斬"=>"斩",
+"斷"=>"断",
+"於"=>"于",
+"時"=>"时",
+"晉"=>"晋",
+"晝"=>"昼",
+"暈"=>"晕",
+"暉"=>"晖",
+"暘"=>"旸",
+"暢"=>"畅",
+"暫"=>"暂",
+"曄"=>"晔",
+"曆"=>"历",
+"曇"=>"昙",
+"曉"=>"晓",
+"曏"=>"向",
+"曖"=>"暧",
+"曠"=>"旷",
+"曨"=>"昽",
+"曬"=>"晒",
+"書"=>"书",
+"會"=>"会",
+"朧"=>"胧",
+"朮"=>"术",
+"术"=>"术",
+"朴"=>"朴",
+"東"=>"东",
+"杴"=>"锨",
+"极"=>"极",
+"柜"=>"柜",
+"柵"=>"栅",
+"桿"=>"杆",
+"梔"=>"栀",
+"梘"=>"枧",
+"條"=>"条",
+"梟"=>"枭",
+"梲"=>"棁",
+"棄"=>"弃",
+"棖"=>"枨",
+"棗"=>"枣",
+"棟"=>"栋",
+"棧"=>"栈",
+"棲"=>"栖",
+"棶"=>"梾",
+"椏"=>"桠",
+"楊"=>"杨",
+"楓"=>"枫",
+"楨"=>"桢",
+"業"=>"业",
+"極"=>"极",
+"榪"=>"杩",
+"榮"=>"荣",
+"榲"=>"榅",
+"榿"=>"桤",
+"構"=>"构",
+"槍"=>"枪",
+"槤"=>"梿",
+"槧"=>"椠",
+"槨"=>"椁",
+"槳"=>"桨",
+"樁"=>"桩",
+"樂"=>"乐",
+"樅"=>"枞",
+"樓"=>"楼",
+"標"=>"标",
+"樞"=>"枢",
+"樣"=>"样",
+"樸"=>"朴",
+"樹"=>"树",
+"樺"=>"桦",
+"橈"=>"桡",
+"橋"=>"桥",
+"機"=>"机",
+"橢"=>"椭",
+"橫"=>"横",
+"檁"=>"檩",
+"檉"=>"柽",
+"檔"=>"档",
+"檜"=>"桧",
+"檟"=>"槚",
+"檢"=>"检",
+"檣"=>"樯",
+"檮"=>"梼",
+"檯"=>"台",
+"檳"=>"槟",
+"檸"=>"柠",
+"檻"=>"槛",
+"櫃"=>"柜",
+"櫓"=>"橹",
+"櫚"=>"榈",
+"櫛"=>"栉",
+"櫝"=>"椟",
+"櫞"=>"橼",
+"櫟"=>"栎",
+"櫥"=>"橱",
+"櫧"=>"槠",
+"櫨"=>"栌",
+"櫪"=>"枥",
+"櫫"=>"橥",
+"櫬"=>"榇",
+"櫱"=>"蘖",
+"櫳"=>"栊",
+"櫸"=>"榉",
+"櫻"=>"樱",
+"欄"=>"栏",
+"權"=>"权",
+"欏"=>"椤",
+"欒"=>"栾",
+"欖"=>"榄",
+"欞"=>"棂",
+"欽"=>"钦",
+"歐"=>"欧",
+"歟"=>"欤",
+"歡"=>"欢",
+"歲"=>"岁",
+"歷"=>"历",
+"歸"=>"归",
+"歿"=>"殁",
+"殘"=>"残",
+"殞"=>"殒",
+"殤"=>"殇",
+"殨"=>"㱮",
+"殫"=>"殚",
+"殮"=>"殓",
+"殯"=>"殡",
+"殲"=>"歼",
+"殺"=>"杀",
+"殻"=>"壳",
+"殼"=>"壳",
+"毀"=>"毁",
+"毆"=>"殴",
+"毿"=>"毵",
+"氂"=>"牦",
+"氈"=>"毡",
+"氌"=>"氇",
+"氣"=>"气",
+"氫"=>"氢",
+"氬"=>"氩",
+"氳"=>"氲",
+"汙"=>"污",
+"決"=>"决",
+"沒"=>"没",
+"沖"=>"冲",
+"況"=>"况",
+"洶"=>"汹",
+"浹"=>"浃",
+"涂"=>"涂",
+"涇"=>"泾",
+"涼"=>"凉",
+"淀"=>"淀",
+"淒"=>"凄",
+"淚"=>"泪",
+"淥"=>"渌",
+"淨"=>"净",
+"淩"=>"凌",
+"淪"=>"沦",
+"淵"=>"渊",
+"淶"=>"涞",
+"淺"=>"浅",
+"渙"=>"涣",
+"減"=>"减",
+"渦"=>"涡",
+"測"=>"测",
+"渾"=>"浑",
+"湊"=>"凑",
+"湞"=>"浈",
+"湯"=>"汤",
+"溈"=>"沩",
+"準"=>"准",
+"溝"=>"沟",
+"溫"=>"温",
+"滄"=>"沧",
+"滅"=>"灭",
+"滌"=>"涤",
+"滎"=>"荥",
+"滬"=>"沪",
+"滯"=>"滞",
+"滲"=>"渗",
+"滷"=>"卤",
+"滸"=>"浒",
+"滻"=>"浐",
+"滾"=>"滚",
+"滿"=>"满",
+"漁"=>"渔",
+"漚"=>"沤",
+"漢"=>"汉",
+"漣"=>"涟",
+"漬"=>"渍",
+"漲"=>"涨",
+"漵"=>"溆",
+"漸"=>"渐",
+"漿"=>"浆",
+"潁"=>"颍",
+"潑"=>"泼",
+"潔"=>"洁",
+"潙"=>"沩",
+"潛"=>"潜",
+"潤"=>"润",
+"潯"=>"浔",
+"潰"=>"溃",
+"潷"=>"滗",
+"潿"=>"涠",
+"澀"=>"涩",
+"澆"=>"浇",
+"澇"=>"涝",
+"澐"=>"沄",
+"澗"=>"涧",
+"澠"=>"渑",
+"澤"=>"泽",
+"澦"=>"滪",
+"澩"=>"泶",
+"澮"=>"浍",
+"澱"=>"淀",
+"濁"=>"浊",
+"濃"=>"浓",
+"濕"=>"湿",
+"濘"=>"泞",
+"濛"=>"蒙",
+"濟"=>"济",
+"濤"=>"涛",
+"濫"=>"滥",
+"濰"=>"潍",
+"濱"=>"滨",
+"濺"=>"溅",
+"濼"=>"泺",
+"濾"=>"滤",
+"瀅"=>"滢",
+"瀆"=>"渎",
+"瀉"=>"泻",
+"瀋"=>"沈",
+"瀏"=>"浏",
+"瀕"=>"濒",
+"瀘"=>"泸",
+"瀝"=>"沥",
+"瀟"=>"潇",
+"瀠"=>"潆",
+"瀦"=>"潴",
+"瀧"=>"泷",
+"瀨"=>"濑",
+"瀰"=>"弥",
+"瀲"=>"潋",
+"瀾"=>"澜",
+"灃"=>"沣",
+"灄"=>"滠",
+"灑"=>"洒",
+"灕"=>"漓",
+"灘"=>"滩",
+"灝"=>"灏",
+"灠"=>"漤",
+"灣"=>"湾",
+"灤"=>"滦",
+"灧"=>"滟",
+"災"=>"灾",
+"為"=>"为",
+"烏"=>"乌",
+"烴"=>"烃",
+"無"=>"无",
+"煉"=>"炼",
+"煒"=>"炜",
+"煙"=>"烟",
+"煢"=>"茕",
+"煥"=>"焕",
+"煩"=>"烦",
+"煬"=>"炀",
+"熅"=>"煴",
+"熒"=>"荧",
+"熗"=>"炝",
+"熱"=>"热",
+"熲"=>"颎",
+"熾"=>"炽",
+"燁"=>"烨",
+"燈"=>"灯",
+"燉"=>"炖",
+"燒"=>"烧",
+"燙"=>"烫",
+"燜"=>"焖",
+"營"=>"营",
+"燦"=>"灿",
+"燭"=>"烛",
+"燴"=>"烩",
+"燼"=>"烬",
+"燾"=>"焘",
+"爍"=>"烁",
+"爐"=>"炉",
+"爛"=>"烂",
+"爭"=>"争",
+"爲"=>"为",
+"爺"=>"爷",
+"爾"=>"尔",
+"牆"=>"墙",
+"牘"=>"牍",
+"牽"=>"牵",
+"犖"=>"荦",
+"犢"=>"犊",
+"犧"=>"牺",
+"狀"=>"状",
+"狹"=>"狭",
+"狽"=>"狈",
+"猙"=>"狰",
+"猶"=>"犹",
+"猻"=>"狲",
+"獁"=>"犸",
+"獄"=>"狱",
+"獅"=>"狮",
+"獎"=>"奖",
+"獨"=>"独",
+"獪"=>"狯",
+"獫"=>"猃",
+"獮"=>"狝",
+"獰"=>"狞",
+"獲"=>"获",
+"獵"=>"猎",
+"獷"=>"犷",
+"獸"=>"兽",
+"獺"=>"獭",
+"獻"=>"献",
+"獼"=>"猕",
+"玀"=>"猡",
+"現"=>"现",
+"琺"=>"珐",
+"琿"=>"珲",
+"瑋"=>"玮",
+"瑒"=>"玚",
+"瑣"=>"琐",
+"瑤"=>"瑶",
+"瑩"=>"莹",
+"瑪"=>"玛",
+"瑲"=>"玱",
+"璉"=>"琏",
+"璣"=>"玑",
+"璦"=>"瑷",
+"璫"=>"珰",
+"環"=>"环",
+"璽"=>"玺",
+"瓊"=>"琼",
+"瓏"=>"珑",
+"瓔"=>"璎",
+"瓚"=>"瓒",
+"甌"=>"瓯",
+"產"=>"产",
+"産"=>"产",
+"畝"=>"亩",
+"畢"=>"毕",
+"異"=>"异",
+"畵"=>"画",
+"當"=>"当",
+"疇"=>"畴",
+"疊"=>"叠",
+"痙"=>"痉",
+"痾"=>"疴",
+"瘂"=>"痖",
+"瘋"=>"疯",
+"瘍"=>"疡",
+"瘓"=>"痪",
+"瘞"=>"瘗",
+"瘡"=>"疮",
+"瘧"=>"疟",
+"瘮"=>"瘆",
+"瘲"=>"疭",
+"瘺"=>"瘘",
+"瘻"=>"瘘",
+"療"=>"疗",
+"癆"=>"痨",
+"癇"=>"痫",
+"癉"=>"瘅",
+"癘"=>"疠",
+"癟"=>"瘪",
+"癢"=>"痒",
+"癤"=>"疖",
+"癥"=>"症",
+"癧"=>"疬",
+"癩"=>"癞",
+"癬"=>"癣",
+"癭"=>"瘿",
+"癮"=>"瘾",
+"癰"=>"痈",
+"癱"=>"瘫",
+"癲"=>"癫",
+"發"=>"发",
+"皚"=>"皑",
+"皰"=>"疱",
+"皸"=>"皲",
+"皺"=>"皱",
+"盃"=>"杯",
+"盜"=>"盗",
+"盞"=>"盏",
+"盡"=>"尽",
+"監"=>"监",
+"盤"=>"盘",
+"盧"=>"卢",
+"眥"=>"眦",
+"眾"=>"众",
+"睏"=>"困",
+"睜"=>"睁",
+"睞"=>"睐",
+"瞘"=>"眍",
+"瞜"=>"䁖",
+"瞞"=>"瞒",
+"瞭"=>"了",
+"瞶"=>"瞆",
+"瞼"=>"睑",
+"矇"=>"蒙",
+"矓"=>"眬",
+"矚"=>"瞩",
+"矯"=>"矫",
+"硃"=>"朱",
+"硜"=>"硁",
+"硤"=>"硖",
+"硨"=>"砗",
+"确"=>"确",
+"硯"=>"砚",
+"碩"=>"硕",
+"碭"=>"砀",
+"碸"=>"砜",
+"確"=>"确",
+"碼"=>"码",
+"磑"=>"硙",
+"磚"=>"砖",
+"磣"=>"碜",
+"磧"=>"碛",
+"磯"=>"矶",
+"磽"=>"硗",
+"礆"=>"硷",
+"礎"=>"础",
+"礙"=>"碍",
+"礦"=>"矿",
+"礪"=>"砺",
+"礫"=>"砾",
+"礬"=>"矾",
+"礱"=>"砻",
+"祿"=>"禄",
+"禍"=>"祸",
+"禎"=>"祯",
+"禕"=>"祎",
+"禡"=>"祃",
+"禦"=>"御",
+"禪"=>"禅",
+"禮"=>"礼",
+"禰"=>"祢",
+"禱"=>"祷",
+"禿"=>"秃",
+"秈"=>"籼",
+"种"=>"种",
+"稅"=>"税",
+"稈"=>"秆",
+"稟"=>"禀",
+"種"=>"种",
+"稱"=>"称",
+"穀"=>"谷",
+"穌"=>"稣",
+"積"=>"积",
+"穎"=>"颖",
+"穠"=>"秾",
+"穡"=>"穑",
+"穢"=>"秽",
+"穩"=>"稳",
+"穫"=>"获",
+"穭"=>"稆",
+"窩"=>"窝",
+"窪"=>"洼",
+"窮"=>"穷",
+"窯"=>"窑",
+"窵"=>"窎",
+"窶"=>"窭",
+"窺"=>"窥",
+"竄"=>"窜",
+"竅"=>"窍",
+"竇"=>"窦",
+"竈"=>"灶",
+"竊"=>"窃",
+"竪"=>"竖",
+"競"=>"竞",
+"筆"=>"笔",
+"筍"=>"笋",
+"筑"=>"筑",
+"筧"=>"笕",
+"筴"=>"䇲",
+"箋"=>"笺",
+"箏"=>"筝",
+"節"=>"节",
+"範"=>"范",
+"築"=>"筑",
+"篋"=>"箧",
+"篔"=>"筼",
+"篤"=>"笃",
+"篩"=>"筛",
+"篳"=>"筚",
+"簀"=>"箦",
+"簍"=>"篓",
+"簞"=>"箪",
+"簡"=>"简",
+"簣"=>"篑",
+"簫"=>"箫",
+"簹"=>"筜",
+"簽"=>"签",
+"簾"=>"帘",
+"籃"=>"篮",
+"籌"=>"筹",
+"籖"=>"签",
+"籙"=>"箓",
+"籜"=>"箨",
+"籟"=>"籁",
+"籠"=>"笼",
+"籩"=>"笾",
+"籪"=>"簖",
+"籬"=>"篱",
+"籮"=>"箩",
+"籲"=>"吁",
+"粵"=>"粤",
+"糝"=>"糁",
+"糞"=>"粪",
+"糧"=>"粮",
+"糰"=>"团",
+"糲"=>"粝",
+"糴"=>"籴",
+"糶"=>"粜",
+"糹"=>"纟",
+"糾"=>"纠",
+"紀"=>"纪",
+"紂"=>"纣",
+"約"=>"约",
+"紅"=>"红",
+"紆"=>"纡",
+"紇"=>"纥",
+"紈"=>"纨",
+"紉"=>"纫",
+"紋"=>"纹",
+"納"=>"纳",
+"紐"=>"纽",
+"紓"=>"纾",
+"純"=>"纯",
+"紕"=>"纰",
+"紖"=>"纼",
+"紗"=>"纱",
+"紘"=>"纮",
+"紙"=>"纸",
+"級"=>"级",
+"紛"=>"纷",
+"紜"=>"纭",
+"紝"=>"纴",
+"紡"=>"纺",
+"紬"=>"䌷",
+"細"=>"细",
+"紱"=>"绂",
+"紲"=>"绁",
+"紳"=>"绅",
+"紵"=>"纻",
+"紹"=>"绍",
+"紺"=>"绀",
+"紼"=>"绋",
+"紿"=>"绐",
+"絀"=>"绌",
+"終"=>"终",
+"組"=>"组",
+"絅"=>"䌹",
+"絆"=>"绊",
+"絎"=>"绗",
+"結"=>"结",
+"絕"=>"绝",
+"絛"=>"绦",
+"絝"=>"绔",
+"絞"=>"绞",
+"絡"=>"络",
+"絢"=>"绚",
+"給"=>"给",
+"絨"=>"绒",
+"絰"=>"绖",
+"統"=>"统",
+"絲"=>"丝",
+"絳"=>"绛",
+"絶"=>"绝",
+"絹"=>"绢",
+"綁"=>"绑",
+"綃"=>"绡",
+"綆"=>"绠",
+"綈"=>"绨",
+"綉"=>"绣",
+"綌"=>"绤",
+"綏"=>"绥",
+"經"=>"经",
+"綜"=>"综",
+"綞"=>"缍",
+"綠"=>"绿",
+"綢"=>"绸",
+"綣"=>"绻",
+"綫"=>"线",
+"綬"=>"绶",
+"維"=>"维",
+"綯"=>"绹",
+"綰"=>"绾",
+"綱"=>"纲",
+"網"=>"网",
+"綳"=>"绷",
+"綴"=>"缀",
+"綸"=>"纶",
+"綹"=>"绺",
+"綺"=>"绮",
+"綻"=>"绽",
+"綽"=>"绰",
+"綾"=>"绫",
+"綿"=>"绵",
+"緄"=>"绲",
+"緇"=>"缁",
+"緊"=>"紧",
+"緋"=>"绯",
+"緑"=>"绿",
+"緒"=>"绪",
+"緓"=>"绬",
+"緔"=>"绱",
+"緗"=>"缃",
+"緘"=>"缄",
+"緙"=>"缂",
+"線"=>"线",
+"緝"=>"缉",
+"緞"=>"缎",
+"締"=>"缔",
+"緡"=>"缗",
+"緣"=>"缘",
+"緦"=>"缌",
+"編"=>"编",
+"緩"=>"缓",
+"緬"=>"缅",
+"緯"=>"纬",
+"緱"=>"缑",
+"緲"=>"缈",
+"練"=>"练",
+"緶"=>"缏",
+"緹"=>"缇",
+"緻"=>"致",
+"縈"=>"萦",
+"縉"=>"缙",
+"縊"=>"缢",
+"縋"=>"缒",
+"縐"=>"绉",
+"縑"=>"缣",
+"縕"=>"缊",
+"縗"=>"缞",
+"縛"=>"缚",
+"縝"=>"缜",
+"縞"=>"缟",
+"縟"=>"缛",
+"縣"=>"县",
+"縧"=>"绦",
+"縫"=>"缝",
+"縭"=>"缡",
+"縮"=>"缩",
+"縱"=>"纵",
+"縲"=>"缧",
+"縳"=>"䌸",
+"縴"=>"纤",
+"縵"=>"缦",
+"縶"=>"絷",
+"縷"=>"缕",
+"縹"=>"缥",
+"總"=>"总",
+"績"=>"绩",
+"繃"=>"绷",
+"繅"=>"缫",
+"繆"=>"缪",
+"繒"=>"缯",
+"織"=>"织",
+"繕"=>"缮",
+"繚"=>"缭",
+"繞"=>"绕",
+"繡"=>"绣",
+"繢"=>"缋",
+"繩"=>"绳",
+"繪"=>"绘",
+"繫"=>"系",
+"繭"=>"茧",
+"繮"=>"缰",
+"繯"=>"缳",
+"繰"=>"缲",
+"繳"=>"缴",
+"繸"=>"䍁",
+"繹"=>"绎",
+"繼"=>"继",
+"繽"=>"缤",
+"繾"=>"缱",
+"纈"=>"缬",
+"纊"=>"纩",
+"續"=>"续",
+"纍"=>"累",
+"纏"=>"缠",
+"纓"=>"缨",
+"纔"=>"才",
+"纖"=>"纤",
+"纘"=>"缵",
+"纜"=>"缆",
+"缽"=>"钵",
+"罈"=>"坛",
+"罌"=>"罂",
+"罰"=>"罚",
+"罵"=>"骂",
+"罷"=>"罢",
+"羅"=>"罗",
+"羆"=>"罴",
+"羈"=>"羁",
+"羋"=>"芈",
+"羥"=>"羟",
+"義"=>"义",
+"習"=>"习",
+"翹"=>"翘",
+"耬"=>"耧",
+"耮"=>"耢",
+"聖"=>"圣",
+"聞"=>"闻",
+"聯"=>"联",
+"聰"=>"聪",
+"聲"=>"声",
+"聳"=>"耸",
+"聵"=>"聩",
+"聶"=>"聂",
+"職"=>"职",
+"聹"=>"聍",
+"聽"=>"听",
+"聾"=>"聋",
+"肅"=>"肃",
+"胜"=>"胜",
+"脅"=>"胁",
+"脈"=>"脉",
+"脛"=>"胫",
+"脫"=>"脱",
+"脹"=>"胀",
+"腊"=>"腊",
+"腎"=>"肾",
+"腖"=>"胨",
+"腡"=>"脶",
+"腦"=>"脑",
+"腫"=>"肿",
+"腳"=>"脚",
+"腸"=>"肠",
+"膃"=>"腽",
+"膚"=>"肤",
+"膠"=>"胶",
+"膩"=>"腻",
+"膽"=>"胆",
+"膾"=>"脍",
+"膿"=>"脓",
+"臉"=>"脸",
+"臍"=>"脐",
+"臏"=>"膑",
+"臘"=>"腊",
+"臚"=>"胪",
+"臟"=>"脏",
+"臠"=>"脔",
+"臢"=>"臜",
+"臥"=>"卧",
+"臨"=>"临",
+"臺"=>"台",
+"與"=>"与",
+"興"=>"兴",
+"舉"=>"举",
+"舊"=>"旧",
+"艙"=>"舱",
+"艤"=>"舣",
+"艦"=>"舰",
+"艫"=>"舻",
+"艱"=>"艰",
+"艷"=>"艳",
+"芻"=>"刍",
+"苎"=>"苧",
+"苧"=>"苎",
+"苹"=>"苹",
+"范"=>"范",
+"茲"=>"兹",
+"荊"=>"荆",
+"莊"=>"庄",
+"莖"=>"茎",
+"莢"=>"荚",
+"莧"=>"苋",
+"華"=>"华",
+"萇"=>"苌",
+"萊"=>"莱",
+"萬"=>"万",
+"萵"=>"莴",
+"葉"=>"叶",
+"葒"=>"荭",
+"著"=>"着",
+"葤"=>"荮",
+"葦"=>"苇",
+"葯"=>"药",
+"葷"=>"荤",
+"蒓"=>"莼",
+"蒔"=>"莳",
+"蒞"=>"莅",
+"蒼"=>"苍",
+"蓀"=>"荪",
+"蓋"=>"盖",
+"蓮"=>"莲",
+"蓯"=>"苁",
+"蓴"=>"莼",
+"蓽"=>"荜",
+"蔔"=>"卜",
+"蔞"=>"蒌",
+"蔣"=>"蒋",
+"蔥"=>"葱",
+"蔦"=>"茑",
+"蔭"=>"荫",
+"蕁"=>"荨",
+"蕆"=>"蒇",
+"蕎"=>"荞",
+"蕒"=>"荬",
+"蕓"=>"芸",
+"蕕"=>"莸",
+"蕘"=>"荛",
+"蕢"=>"蒉",
+"蕩"=>"荡",
+"蕪"=>"芜",
+"蕭"=>"萧",
+"蕷"=>"蓣",
+"薀"=>"蕰",
+"薈"=>"荟",
+"薊"=>"蓟",
+"薌"=>"芗",
+"薔"=>"蔷",
+"薘"=>"荙",
+"薟"=>"莶",
+"薦"=>"荐",
+"薩"=>"萨",
+"薴"=>"苧",
+"薺"=>"荠",
+"藉"=>"借",
+"藍"=>"蓝",
+"藎"=>"荩",
+"藝"=>"艺",
+"藥"=>"药",
+"藪"=>"薮",
+"藴"=>"蕴",
+"藶"=>"苈",
+"藹"=>"蔼",
+"藺"=>"蔺",
+"蘄"=>"蕲",
+"蘆"=>"芦",
+"蘇"=>"苏",
+"蘊"=>"蕴",
+"蘋"=>"苹",
+"蘚"=>"藓",
+"蘞"=>"蔹",
+"蘢"=>"茏",
+"蘭"=>"兰",
+"蘺"=>"蓠",
+"蘿"=>"萝",
+"虆"=>"蔂",
+"處"=>"处",
+"虛"=>"虚",
+"虜"=>"虏",
+"號"=>"号",
+"虧"=>"亏",
+"虫"=>"虫",
+"虯"=>"虬",
+"蛺"=>"蛱",
+"蛻"=>"蜕",
+"蜆"=>"蚬",
+"蜡"=>"蜡",
+"蝕"=>"蚀",
+"蝟"=>"猬",
+"蝦"=>"虾",
+"蝸"=>"蜗",
+"螄"=>"蛳",
+"螞"=>"蚂",
+"螢"=>"萤",
+"螻"=>"蝼",
+"螿"=>"螀",
+"蟄"=>"蛰",
+"蟈"=>"蝈",
+"蟎"=>"螨",
+"蟣"=>"虮",
+"蟬"=>"蝉",
+"蟯"=>"蛲",
+"蟲"=>"虫",
+"蟶"=>"蛏",
+"蟻"=>"蚁",
+"蠅"=>"蝇",
+"蠆"=>"虿",
+"蠐"=>"蛴",
+"蠑"=>"蝾",
+"蠟"=>"蜡",
+"蠣"=>"蛎",
+"蠨"=>"蟏",
+"蠱"=>"蛊",
+"蠶"=>"蚕",
+"蠻"=>"蛮",
+"衆"=>"众",
+"衊"=>"蔑",
+"術"=>"术",
+"衕"=>"同",
+"衚"=>"胡",
+"衛"=>"卫",
+"衝"=>"冲",
+"衹"=>"只",
+"袞"=>"衮",
+"裊"=>"袅",
+"裏"=>"里",
+"補"=>"补",
+"裝"=>"装",
+"裡"=>"里",
+"製"=>"制",
+"複"=>"复",
+"褌"=>"裈",
+"褘"=>"袆",
+"褲"=>"裤",
+"褳"=>"裢",
+"褸"=>"褛",
+"褻"=>"亵",
+"襇"=>"裥",
+"襏"=>"袯",
+"襖"=>"袄",
+"襝"=>"裣",
+"襠"=>"裆",
+"襤"=>"褴",
+"襪"=>"袜",
+"襯"=>"衬",
+"襲"=>"袭",
+"覆"=>"复",
+"見"=>"见",
+"覎"=>"觃",
+"規"=>"规",
+"覓"=>"觅",
+"視"=>"视",
+"覘"=>"觇",
+"覡"=>"觋",
+"覥"=>"觍",
+"覦"=>"觎",
+"親"=>"亲",
+"覬"=>"觊",
+"覯"=>"觏",
+"覲"=>"觐",
+"覷"=>"觑",
+"覺"=>"觉",
+"覽"=>"览",
+"覿"=>"觌",
+"觀"=>"观",
+"觴"=>"觞",
+"觶"=>"觯",
+"觸"=>"触",
+"訁"=>"讠",
+"訂"=>"订",
+"訃"=>"讣",
+"計"=>"计",
+"訊"=>"讯",
+"訌"=>"讧",
+"討"=>"讨",
+"訐"=>"讦",
+"訒"=>"讱",
+"訓"=>"训",
+"訕"=>"讪",
+"訖"=>"讫",
+"託"=>"讬",
+"記"=>"记",
+"訛"=>"讹",
+"訝"=>"讶",
+"訟"=>"讼",
+"訢"=>"䜣",
+"訣"=>"诀",
+"訥"=>"讷",
+"訩"=>"讻",
+"訪"=>"访",
+"設"=>"设",
+"許"=>"许",
+"訴"=>"诉",
+"訶"=>"诃",
+"診"=>"诊",
+"註"=>"注",
+"詁"=>"诂",
+"詆"=>"诋",
+"詎"=>"讵",
+"詐"=>"诈",
+"詒"=>"诒",
+"詔"=>"诏",
+"評"=>"评",
+"詖"=>"诐",
+"詗"=>"诇",
+"詘"=>"诎",
+"詛"=>"诅",
+"詞"=>"词",
+"詠"=>"咏",
+"詡"=>"诩",
+"詢"=>"询",
+"詣"=>"诣",
+"試"=>"试",
+"詩"=>"诗",
+"詫"=>"诧",
+"詬"=>"诟",
+"詭"=>"诡",
+"詮"=>"诠",
+"詰"=>"诘",
+"話"=>"话",
+"該"=>"该",
+"詳"=>"详",
+"詵"=>"诜",
+"詼"=>"诙",
+"詿"=>"诖",
+"誄"=>"诔",
+"誅"=>"诛",
+"誆"=>"诓",
+"誇"=>"夸",
+"誌"=>"志",
+"認"=>"认",
+"誑"=>"诳",
+"誒"=>"诶",
+"誕"=>"诞",
+"誘"=>"诱",
+"誚"=>"诮",
+"語"=>"语",
+"誠"=>"诚",
+"誡"=>"诫",
+"誣"=>"诬",
+"誤"=>"误",
+"誥"=>"诰",
+"誦"=>"诵",
+"誨"=>"诲",
+"說"=>"说",
+"説"=>"说",
+"誰"=>"谁",
+"課"=>"课",
+"誶"=>"谇",
+"誹"=>"诽",
+"誼"=>"谊",
+"誾"=>"訚",
+"調"=>"调",
+"諂"=>"谄",
+"諄"=>"谆",
+"談"=>"谈",
+"諉"=>"诿",
+"請"=>"请",
+"諍"=>"诤",
+"諏"=>"诹",
+"諑"=>"诼",
+"諒"=>"谅",
+"論"=>"论",
+"諗"=>"谂",
+"諛"=>"谀",
+"諜"=>"谍",
+"諝"=>"谞",
+"諞"=>"谝",
+"諢"=>"诨",
+"諤"=>"谔",
+"諦"=>"谛",
+"諧"=>"谐",
+"諫"=>"谏",
+"諭"=>"谕",
+"諮"=>"谘",
+"諱"=>"讳",
+"諳"=>"谙",
+"諶"=>"谌",
+"諷"=>"讽",
+"諸"=>"诸",
+"諺"=>"谚",
+"諼"=>"谖",
+"諾"=>"诺",
+"謀"=>"谋",
+"謁"=>"谒",
+"謂"=>"谓",
+"謄"=>"誊",
+"謅"=>"诌",
+"謊"=>"谎",
+"謎"=>"谜",
+"謐"=>"谧",
+"謔"=>"谑",
+"謖"=>"谡",
+"謗"=>"谤",
+"謙"=>"谦",
+"謚"=>"谥",
+"講"=>"讲",
+"謝"=>"谢",
+"謠"=>"谣",
+"謡"=>"谣",
+"謨"=>"谟",
+"謫"=>"谪",
+"謬"=>"谬",
+"謭"=>"谫",
+"謳"=>"讴",
+"謹"=>"谨",
+"謾"=>"谩",
+"證"=>"证",
+"譎"=>"谲",
+"譏"=>"讥",
+"譖"=>"谮",
+"識"=>"识",
+"譙"=>"谯",
+"譚"=>"谭",
+"譜"=>"谱",
+"譫"=>"谵",
+"譯"=>"译",
+"議"=>"议",
+"譴"=>"谴",
+"護"=>"护",
+"譸"=>"诪",
+"譽"=>"誉",
+"譾"=>"谫",
+"讀"=>"读",
+"變"=>"变",
+"讎"=>"仇",
+"讎"=>"雠",
+"讒"=>"谗",
+"讓"=>"让",
+"讕"=>"谰",
+"讖"=>"谶",
+"讜"=>"谠",
+"讞"=>"谳",
+"豈"=>"岂",
+"豎"=>"竖",
+"豐"=>"丰",
+"豬"=>"猪",
+"豶"=>"豮",
+"貓"=>"猫",
+"貝"=>"贝",
+"貞"=>"贞",
+"貟"=>"贠",
+"負"=>"负",
+"財"=>"财",
+"貢"=>"贡",
+"貧"=>"贫",
+"貨"=>"货",
+"販"=>"贩",
+"貪"=>"贪",
+"貫"=>"贯",
+"責"=>"责",
+"貯"=>"贮",
+"貰"=>"贳",
+"貲"=>"赀",
+"貳"=>"贰",
+"貴"=>"贵",
+"貶"=>"贬",
+"買"=>"买",
+"貸"=>"贷",
+"貺"=>"贶",
+"費"=>"费",
+"貼"=>"贴",
+"貽"=>"贻",
+"貿"=>"贸",
+"賀"=>"贺",
+"賁"=>"贲",
+"賂"=>"赂",
+"賃"=>"赁",
+"賄"=>"贿",
+"賅"=>"赅",
+"資"=>"资",
+"賈"=>"贾",
+"賊"=>"贼",
+"賑"=>"赈",
+"賒"=>"赊",
+"賓"=>"宾",
+"賕"=>"赇",
+"賙"=>"赒",
+"賚"=>"赉",
+"賜"=>"赐",
+"賞"=>"赏",
+"賠"=>"赔",
+"賡"=>"赓",
+"賢"=>"贤",
+"賣"=>"卖",
+"賤"=>"贱",
+"賦"=>"赋",
+"賧"=>"赕",
+"質"=>"质",
+"賫"=>"赍",
+"賬"=>"账",
+"賭"=>"赌",
+"賴"=>"赖",
+"賵"=>"赗",
+"賺"=>"赚",
+"賻"=>"赙",
+"購"=>"购",
+"賽"=>"赛",
+"賾"=>"赜",
+"贄"=>"贽",
+"贅"=>"赘",
+"贇"=>"赟",
+"贈"=>"赠",
+"贊"=>"赞",
+"贋"=>"赝",
+"贍"=>"赡",
+"贏"=>"赢",
+"贐"=>"赆",
+"贓"=>"赃",
+"贔"=>"赑",
+"贖"=>"赎",
+"贗"=>"赝",
+"贛"=>"赣",
+"贜"=>"赃",
+"赬"=>"赪",
+"趕"=>"赶",
+"趙"=>"赵",
+"趨"=>"趋",
+"趲"=>"趱",
+"跡"=>"迹",
+"踐"=>"践",
+"踴"=>"踊",
+"蹌"=>"跄",
+"蹕"=>"跸",
+"蹣"=>"蹒",
+"蹤"=>"踪",
+"蹺"=>"跷",
+"躂"=>"跶",
+"躉"=>"趸",
+"躊"=>"踌",
+"躋"=>"跻",
+"躍"=>"跃",
+"躑"=>"踯",
+"躒"=>"跞",
+"躓"=>"踬",
+"躕"=>"蹰",
+"躚"=>"跹",
+"躡"=>"蹑",
+"躥"=>"蹿",
+"躦"=>"躜",
+"躪"=>"躏",
+"軀"=>"躯",
+"車"=>"车",
+"軋"=>"轧",
+"軌"=>"轨",
+"軍"=>"军",
+"軑"=>"轪",
+"軒"=>"轩",
+"軔"=>"轫",
+"軛"=>"轭",
+"軟"=>"软",
+"軤"=>"轷",
+"軫"=>"轸",
+"軲"=>"轱",
+"軸"=>"轴",
+"軹"=>"轵",
+"軺"=>"轺",
+"軻"=>"轲",
+"軼"=>"轶",
+"軾"=>"轼",
+"較"=>"较",
+"輅"=>"辂",
+"輇"=>"辁",
+"輈"=>"辀",
+"載"=>"载",
+"輊"=>"轾",
+"輒"=>"辄",
+"輓"=>"挽",
+"輔"=>"辅",
+"輕"=>"轻",
+"輛"=>"辆",
+"輜"=>"辎",
+"輝"=>"辉",
+"輞"=>"辋",
+"輟"=>"辍",
+"輥"=>"辊",
+"輦"=>"辇",
+"輩"=>"辈",
+"輪"=>"轮",
+"輬"=>"辌",
+"輯"=>"辑",
+"輳"=>"辏",
+"輸"=>"输",
+"輻"=>"辐",
+"輾"=>"辗",
+"輿"=>"舆",
+"轀"=>"辒",
+"轂"=>"毂",
+"轄"=>"辖",
+"轅"=>"辕",
+"轆"=>"辘",
+"轉"=>"转",
+"轍"=>"辙",
+"轎"=>"轿",
+"轔"=>"辚",
+"轟"=>"轰",
+"轡"=>"辔",
+"轢"=>"轹",
+"轤"=>"轳",
+"辟"=>"辟",
+"辦"=>"办",
+"辭"=>"辞",
+"辮"=>"辫",
+"辯"=>"辩",
+"農"=>"农",
+"迴"=>"回",
+"适"=>"适",
+"逕"=>"迳",
+"這"=>"这",
+"連"=>"连",
+"週"=>"周",
+"進"=>"进",
+"遊"=>"游",
+"運"=>"运",
+"過"=>"过",
+"達"=>"达",
+"違"=>"违",
+"遙"=>"遥",
+"遜"=>"逊",
+"遞"=>"递",
+"遠"=>"远",
+"適"=>"适",
+"遲"=>"迟",
+"遷"=>"迁",
+"選"=>"选",
+"遺"=>"遗",
+"遼"=>"辽",
+"邁"=>"迈",
+"還"=>"还",
+"邇"=>"迩",
+"邊"=>"边",
+"邏"=>"逻",
+"邐"=>"逦",
+"郁"=>"郁",
+"郟"=>"郏",
+"郵"=>"邮",
+"鄆"=>"郓",
+"鄉"=>"乡",
+"鄒"=>"邹",
+"鄔"=>"邬",
+"鄖"=>"郧",
+"鄧"=>"邓",
+"鄭"=>"郑",
+"鄰"=>"邻",
+"鄲"=>"郸",
+"鄴"=>"邺",
+"鄶"=>"郐",
+"鄺"=>"邝",
+"酇"=>"酂",
+"酈"=>"郦",
+"醖"=>"酝",
+"醜"=>"丑",
+"醞"=>"酝",
+"醫"=>"医",
+"醬"=>"酱",
+"醱"=>"酦",
+"釀"=>"酿",
+"釁"=>"衅",
+"釃"=>"酾",
+"釅"=>"酽",
+"采"=>"采",
+"釋"=>"释",
+"釐"=>"厘",
+"釒"=>"钅",
+"釓"=>"钆",
+"釔"=>"钇",
+"釕"=>"钌",
+"釗"=>"钊",
+"釘"=>"钉",
+"釙"=>"钋",
+"針"=>"针",
+"釣"=>"钓",
+"釤"=>"钐",
+"釧"=>"钏",
+"釩"=>"钒",
+"釵"=>"钗",
+"釷"=>"钍",
+"釹"=>"钕",
+"釺"=>"钎",
+"鈀"=>"钯",
+"鈁"=>"钫",
+"鈃"=>"钘",
+"鈄"=>"钭",
+"鈈"=>"钚",
+"鈉"=>"钠",
+"鈍"=>"钝",
+"鈎"=>"钩",
+"鈐"=>"钤",
+"鈑"=>"钣",
+"鈒"=>"钑",
+"鈔"=>"钞",
+"鈕"=>"钮",
+"鈞"=>"钧",
+"鈣"=>"钙",
+"鈥"=>"钬",
+"鈦"=>"钛",
+"鈧"=>"钪",
+"鈮"=>"铌",
+"鈰"=>"铈",
+"鈳"=>"钶",
+"鈴"=>"铃",
+"鈷"=>"钴",
+"鈸"=>"钹",
+"鈹"=>"铍",
+"鈺"=>"钰",
+"鈽"=>"钸",
+"鈾"=>"铀",
+"鈿"=>"钿",
+"鉀"=>"钾",
+"鉅"=>"钜",
+"鉈"=>"铊",
+"鉉"=>"铉",
+"鉋"=>"铇",
+"鉍"=>"铋",
+"鉑"=>"铂",
+"鉕"=>"钷",
+"鉗"=>"钳",
+"鉚"=>"铆",
+"鉛"=>"铅",
+"鉞"=>"钺",
+"鉢"=>"钵",
+"鉤"=>"钩",
+"鉦"=>"钲",
+"鉬"=>"钼",
+"鉭"=>"钽",
+"鉶"=>"铏",
+"鉸"=>"铰",
+"鉺"=>"铒",
+"鉻"=>"铬",
+"鉿"=>"铪",
+"銀"=>"银",
+"銃"=>"铳",
+"銅"=>"铜",
+"銍"=>"铚",
+"銑"=>"铣",
+"銓"=>"铨",
+"銖"=>"铢",
+"銘"=>"铭",
+"銚"=>"铫",
+"銛"=>"铦",
+"銜"=>"衔",
+"銠"=>"铑",
+"銣"=>"铷",
+"銥"=>"铱",
+"銦"=>"铟",
+"銨"=>"铵",
+"銩"=>"铥",
+"銪"=>"铕",
+"銫"=>"铯",
+"銬"=>"铐",
+"銱"=>"铞",
+"銳"=>"锐",
+"銷"=>"销",
+"銹"=>"锈",
+"銻"=>"锑",
+"銼"=>"锉",
+"鋁"=>"铝",
+"鋃"=>"锒",
+"鋅"=>"锌",
+"鋇"=>"钡",
+"鋌"=>"铤",
+"鋏"=>"铗",
+"鋒"=>"锋",
+"鋙"=>"铻",
+"鋝"=>"锊",
+"鋟"=>"锓",
+"鋣"=>"铘",
+"鋤"=>"锄",
+"鋥"=>"锃",
+"鋦"=>"锔",
+"鋨"=>"锇",
+"鋩"=>"铓",
+"鋪"=>"铺",
+"鋭"=>"锐",
+"鋮"=>"铖",
+"鋯"=>"锆",
+"鋰"=>"锂",
+"鋱"=>"铽",
+"鋶"=>"锍",
+"鋸"=>"锯",
+"鋼"=>"钢",
+"錁"=>"锞",
+"錄"=>"录",
+"錆"=>"锖",
+"錇"=>"锫",
+"錈"=>"锩",
+"錏"=>"铔",
+"錐"=>"锥",
+"錒"=>"锕",
+"錕"=>"锟",
+"錘"=>"锤",
+"錙"=>"锱",
+"錚"=>"铮",
+"錛"=>"锛",
+"錟"=>"锬",
+"錠"=>"锭",
+"錡"=>"锜",
+"錢"=>"钱",
+"錦"=>"锦",
+"錨"=>"锚",
+"錩"=>"锠",
+"錫"=>"锡",
+"錮"=>"锢",
+"錯"=>"错",
+"録"=>"录",
+"錳"=>"锰",
+"錶"=>"表",
+"錸"=>"铼",
+"鍀"=>"锝",
+"鍁"=>"锨",
+"鍃"=>"锪",
+"鍆"=>"钔",
+"鍇"=>"锴",
+"鍈"=>"锳",
+"鍋"=>"锅",
+"鍍"=>"镀",
+"鍔"=>"锷",
+"鍘"=>"铡",
+"鍚"=>"钖",
+"鍛"=>"锻",
+"鍠"=>"锽",
+"鍤"=>"锸",
+"鍥"=>"锲",
+"鍩"=>"锘",
+"鍬"=>"锹",
+"鍰"=>"锾",
+"鍵"=>"键",
+"鍶"=>"锶",
+"鍺"=>"锗",
+"鍾"=>"钟",
+"鎂"=>"镁",
+"鎄"=>"锿",
+"鎇"=>"镅",
+"鎊"=>"镑",
+"鎔"=>"镕",
+"鎖"=>"锁",
+"鎘"=>"镉",
+"鎛"=>"镈",
+"鎡"=>"镃",
+"鎢"=>"钨",
+"鎣"=>"蓥",
+"鎦"=>"镏",
+"鎧"=>"铠",
+"鎩"=>"铩",
+"鎪"=>"锼",
+"鎬"=>"镐",
+"鎮"=>"镇",
+"鎰"=>"镒",
+"鎲"=>"镋",
+"鎳"=>"镍",
+"鎵"=>"镓",
+"鎸"=>"镌",
+"鎿"=>"镎",
+"鏃"=>"镞",
+"鏇"=>"镟",
+"鏈"=>"链",
+"鏌"=>"镆",
+"鏍"=>"镙",
+"鏐"=>"镠",
+"鏑"=>"镝",
+"鏗"=>"铿",
+"鏘"=>"锵",
+"鏜"=>"镗",
+"鏝"=>"镘",
+"鏞"=>"镛",
+"鏟"=>"铲",
+"鏡"=>"镜",
+"鏢"=>"镖",
+"鏤"=>"镂",
+"鏨"=>"錾",
+"鏰"=>"镚",
+"鏵"=>"铧",
+"鏷"=>"镤",
+"鏹"=>"镪",
+"鏽"=>"锈",
+"鐃"=>"铙",
+"鐋"=>"铴",
+"鐐"=>"镣",
+"鐒"=>"铹",
+"鐓"=>"镦",
+"鐔"=>"镡",
+"鐘"=>"钟",
+"鐙"=>"镫",
+"鐝"=>"镢",
+"鐠"=>"镨",
+"鐦"=>"锎",
+"鐧"=>"锏",
+"鐨"=>"镄",
+"鐫"=>"镌",
+"鐮"=>"镰",
+"鐲"=>"镯",
+"鐳"=>"镭",
+"鐵"=>"铁",
+"鐶"=>"镮",
+"鐸"=>"铎",
+"鐺"=>"铛",
+"鐿"=>"镱",
+"鑄"=>"铸",
+"鑊"=>"镬",
+"鑌"=>"镔",
+"鑒"=>"鉴",
+"鑔"=>"镲",
+"鑕"=>"锧",
+"鑞"=>"镴",
+"鑠"=>"铄",
+"鑣"=>"镳",
+"鑥"=>"镥",
+"鑭"=>"镧",
+"鑰"=>"钥",
+"鑱"=>"镵",
+"鑲"=>"镶",
+"鑷"=>"镊",
+"鑹"=>"镩",
+"鑼"=>"锣",
+"鑽"=>"钻",
+"鑾"=>"銮",
+"鑿"=>"凿",
+"钁"=>"镢",
+"镟"=>"旋",
+"長"=>"长",
+"門"=>"门",
+"閂"=>"闩",
+"閃"=>"闪",
+"閆"=>"闫",
+"閈"=>"闬",
+"閉"=>"闭",
+"開"=>"开",
+"閌"=>"闶",
+"閎"=>"闳",
+"閏"=>"闰",
+"閑"=>"闲",
+"間"=>"间",
+"閔"=>"闵",
+"閘"=>"闸",
+"閡"=>"阂",
+"閣"=>"阁",
+"閤"=>"合",
+"閥"=>"阀",
+"閨"=>"闺",
+"閩"=>"闽",
+"閫"=>"阃",
+"閬"=>"阆",
+"閭"=>"闾",
+"閱"=>"阅",
+"閲"=>"阅",
+"閶"=>"阊",
+"閹"=>"阉",
+"閻"=>"阎",
+"閼"=>"阏",
+"閽"=>"阍",
+"閾"=>"阈",
+"閿"=>"阌",
+"闃"=>"阒",
+"闆"=>"板",
+"闈"=>"闱",
+"闊"=>"阔",
+"闋"=>"阕",
+"闌"=>"阑",
+"闍"=>"阇",
+"闐"=>"阗",
+"闒"=>"阘",
+"闓"=>"闿",
+"闔"=>"阖",
+"闕"=>"阙",
+"闖"=>"闯",
+"關"=>"关",
+"闞"=>"阚",
+"闠"=>"阓",
+"闡"=>"阐",
+"闤"=>"阛",
+"闥"=>"闼",
+"阪"=>"坂",
+"陘"=>"陉",
+"陝"=>"陕",
+"陣"=>"阵",
+"陰"=>"阴",
+"陳"=>"陈",
+"陸"=>"陆",
+"陽"=>"阳",
+"隉"=>"陧",
+"隊"=>"队",
+"階"=>"阶",
+"隕"=>"陨",
+"際"=>"际",
+"隨"=>"随",
+"險"=>"险",
+"隱"=>"隐",
+"隴"=>"陇",
+"隸"=>"隶",
+"隻"=>"只",
+"雋"=>"隽",
+"雖"=>"虽",
+"雙"=>"双",
+"雛"=>"雏",
+"雜"=>"杂",
+"雞"=>"鸡",
+"離"=>"离",
+"難"=>"难",
+"雲"=>"云",
+"電"=>"电",
+"霢"=>"霡",
+"霧"=>"雾",
+"霽"=>"霁",
+"靂"=>"雳",
+"靄"=>"霭",
+"靈"=>"灵",
+"靚"=>"靓",
+"靜"=>"静",
+"靦"=>"腼",
+"靨"=>"靥",
+"鞀"=>"鼗",
+"鞏"=>"巩",
+"鞝"=>"绱",
+"鞦"=>"秋",
+"鞽"=>"鞒",
+"韁"=>"缰",
+"韃"=>"鞑",
+"韆"=>"千",
+"韉"=>"鞯",
+"韋"=>"韦",
+"韌"=>"韧",
+"韍"=>"韨",
+"韓"=>"韩",
+"韙"=>"韪",
+"韜"=>"韬",
+"韞"=>"韫",
+"韻"=>"韵",
+"響"=>"响",
+"頁"=>"页",
+"頂"=>"顶",
+"頃"=>"顷",
+"項"=>"项",
+"順"=>"顺",
+"頇"=>"顸",
+"須"=>"须",
+"頊"=>"顼",
+"頌"=>"颂",
+"頎"=>"颀",
+"頏"=>"颃",
+"預"=>"预",
+"頑"=>"顽",
+"頒"=>"颁",
+"頓"=>"顿",
+"頗"=>"颇",
+"領"=>"领",
+"頜"=>"颌",
+"頡"=>"颉",
+"頤"=>"颐",
+"頦"=>"颏",
+"頭"=>"头",
+"頮"=>"颒",
+"頰"=>"颊",
+"頲"=>"颋",
+"頴"=>"颕",
+"頷"=>"颔",
+"頸"=>"颈",
+"頹"=>"颓",
+"頻"=>"频",
+"頽"=>"颓",
+"顆"=>"颗",
+"題"=>"题",
+"額"=>"额",
+"顎"=>"颚",
+"顏"=>"颜",
+"顒"=>"颙",
+"顓"=>"颛",
+"顔"=>"颜",
+"願"=>"愿",
+"顙"=>"颡",
+"顛"=>"颠",
+"類"=>"类",
+"顢"=>"颟",
+"顥"=>"颢",
+"顧"=>"顾",
+"顫"=>"颤",
+"顬"=>"颥",
+"顯"=>"显",
+"顰"=>"颦",
+"顱"=>"颅",
+"顳"=>"颞",
+"顴"=>"颧",
+"風"=>"风",
+"颭"=>"飐",
+"颮"=>"飑",
+"颯"=>"飒",
+"颱"=>"台",
+"颳"=>"刮",
+"颶"=>"飓",
+"颸"=>"飔",
+"颺"=>"飏",
+"颻"=>"飖",
+"颼"=>"飕",
+"飀"=>"飗",
+"飄"=>"飘",
+"飆"=>"飙",
+"飈"=>"飚",
+"飛"=>"飞",
+"飠"=>"饣",
+"飢"=>"饥",
+"飣"=>"饤",
+"飥"=>"饦",
+"飩"=>"饨",
+"飪"=>"饪",
+"飫"=>"饫",
+"飭"=>"饬",
+"飯"=>"饭",
+"飲"=>"饮",
+"飴"=>"饴",
+"飼"=>"饲",
+"飽"=>"饱",
+"飾"=>"饰",
+"飿"=>"饳",
+"餃"=>"饺",
+"餄"=>"饸",
+"餅"=>"饼",
+"餉"=>"饷",
+"養"=>"养",
+"餌"=>"饵",
+"餎"=>"饹",
+"餏"=>"饻",
+"餑"=>"饽",
+"餒"=>"馁",
+"餓"=>"饿",
+"餕"=>"馂",
+"餖"=>"饾",
+"餚"=>"肴",
+"餛"=>"馄",
+"餜"=>"馃",
+"餞"=>"饯",
+"餡"=>"馅",
+"館"=>"馆",
+"餱"=>"糇",
+"餳"=>"饧",
+"餶"=>"馉",
+"餷"=>"馇",
+"餺"=>"馎",
+"餼"=>"饩",
+"餾"=>"馏",
+"餿"=>"馊",
+"饁"=>"馌",
+"饃"=>"馍",
+"饅"=>"馒",
+"饈"=>"馐",
+"饉"=>"馑",
+"饊"=>"馓",
+"饋"=>"馈",
+"饌"=>"馔",
+"饑"=>"饥",
+"饒"=>"饶",
+"饗"=>"飨",
+"饜"=>"餍",
+"饞"=>"馋",
+"饢"=>"馕",
+"馬"=>"马",
+"馭"=>"驭",
+"馮"=>"冯",
+"馱"=>"驮",
+"馳"=>"驰",
+"馴"=>"驯",
+"馹"=>"驲",
+"駁"=>"驳",
+"駐"=>"驻",
+"駑"=>"驽",
+"駒"=>"驹",
+"駔"=>"驵",
+"駕"=>"驾",
+"駘"=>"骀",
+"駙"=>"驸",
+"駛"=>"驶",
+"駝"=>"驼",
+"駟"=>"驷",
+"駡"=>"骂",
+"駢"=>"骈",
+"駭"=>"骇",
+"駰"=>"骃",
+"駱"=>"骆",
+"駸"=>"骎",
+"駿"=>"骏",
+"騁"=>"骋",
+"騂"=>"骍",
+"騅"=>"骓",
+"騌"=>"骔",
+"騍"=>"骒",
+"騎"=>"骑",
+"騏"=>"骐",
+"騖"=>"骛",
+"騙"=>"骗",
+"騤"=>"骙",
+"騫"=>"骞",
+"騭"=>"骘",
+"騮"=>"骝",
+"騰"=>"腾",
+"騶"=>"驺",
+"騷"=>"骚",
+"騸"=>"骟",
+"騾"=>"骡",
+"驀"=>"蓦",
+"驁"=>"骜",
+"驂"=>"骖",
+"驃"=>"骠",
+"驄"=>"骢",
+"驅"=>"驱",
+"驊"=>"骅",
+"驌"=>"骕",
+"驍"=>"骁",
+"驏"=>"骣",
+"驕"=>"骄",
+"驗"=>"验",
+"驚"=>"惊",
+"驛"=>"驿",
+"驟"=>"骤",
+"驢"=>"驴",
+"驤"=>"骧",
+"驥"=>"骥",
+"驦"=>"骦",
+"驪"=>"骊",
+"驫"=>"骉",
+"骯"=>"肮",
+"髏"=>"髅",
+"髒"=>"脏",
+"體"=>"体",
+"髕"=>"髌",
+"髖"=>"髋",
+"髮"=>"发",
+"鬆"=>"松",
+"鬍"=>"胡",
+"鬚"=>"须",
+"鬢"=>"鬓",
+"鬥"=>"斗",
+"鬧"=>"闹",
+"鬩"=>"阋",
+"鬮"=>"阄",
+"鬱"=>"郁",
+"魎"=>"魉",
+"魘"=>"魇",
+"魚"=>"鱼",
+"魛"=>"鱽",
+"魢"=>"鱾",
+"魨"=>"鲀",
+"魯"=>"鲁",
+"魴"=>"鲂",
+"魷"=>"鱿",
+"魺"=>"鲄",
+"鮁"=>"鲅",
+"鮃"=>"鲆",
+"鮊"=>"鲌",
+"鮋"=>"鲉",
+"鮍"=>"鲏",
+"鮎"=>"鲇",
+"鮐"=>"鲐",
+"鮑"=>"鲍",
+"鮒"=>"鲋",
+"鮓"=>"鲊",
+"鮚"=>"鲒",
+"鮜"=>"鲘",
+"鮝"=>"鲞",
+"鮞"=>"鲕",
+"鮦"=>"鲖",
+"鮪"=>"鲔",
+"鮫"=>"鲛",
+"鮭"=>"鲑",
+"鮮"=>"鲜",
+"鮳"=>"鲓",
+"鮶"=>"鲪",
+"鮺"=>"鲝",
+"鯀"=>"鲧",
+"鯁"=>"鲠",
+"鯇"=>"鲩",
+"鯉"=>"鲤",
+"鯊"=>"鲨",
+"鯒"=>"鲬",
+"鯔"=>"鲻",
+"鯕"=>"鲯",
+"鯖"=>"鲭",
+"鯗"=>"鲞",
+"鯛"=>"鲷",
+"鯝"=>"鲴",
+"鯡"=>"鲱",
+"鯢"=>"鲵",
+"鯤"=>"鲲",
+"鯧"=>"鲳",
+"鯨"=>"鲸",
+"鯪"=>"鲮",
+"鯫"=>"鲰",
+"鯴"=>"鲺",
+"鯷"=>"鳀",
+"鯽"=>"鲫",
+"鯿"=>"鳊",
+"鰁"=>"鳈",
+"鰂"=>"鲗",
+"鰃"=>"鳂",
+"鰈"=>"鲽",
+"鰉"=>"鳇",
+"鰍"=>"鳅",
+"鰏"=>"鲾",
+"鰐"=>"鳄",
+"鰒"=>"鳆",
+"鰓"=>"鳃",
+"鰜"=>"鳒",
+"鰟"=>"鳑",
+"鰠"=>"鳋",
+"鰣"=>"鲥",
+"鰥"=>"鳏",
+"鰨"=>"鳎",
+"鰩"=>"鳐",
+"鰭"=>"鳍",
+"鰮"=>"鳁",
+"鰱"=>"鲢",
+"鰲"=>"鳌",
+"鰳"=>"鳓",
+"鰵"=>"鳘",
+"鰷"=>"鲦",
+"鰹"=>"鲣",
+"鰺"=>"鲹",
+"鰻"=>"鳗",
+"鰼"=>"鳛",
+"鰾"=>"鳔",
+"鱂"=>"鳉",
+"鱅"=>"鳙",
+"鱈"=>"鳕",
+"鱉"=>"鳖",
+"鱒"=>"鳟",
+"鱔"=>"鳝",
+"鱖"=>"鳜",
+"鱗"=>"鳞",
+"鱘"=>"鲟",
+"鱝"=>"鲼",
+"鱟"=>"鲎",
+"鱠"=>"鲙",
+"鱣"=>"鳣",
+"鱤"=>"鳡",
+"鱧"=>"鳢",
+"鱨"=>"鲿",
+"鱭"=>"鲚",
+"鱯"=>"鳠",
+"鱷"=>"鳄",
+"鱸"=>"鲈",
+"鱺"=>"鲡",
+"䰾"=>"鲃",
+"䲁"=>"鳚",
+"鳥"=>"鸟",
+"鳧"=>"凫",
+"鳩"=>"鸠",
+"鳬"=>"凫",
+"鳲"=>"鸤",
+"鳳"=>"凤",
+"鳴"=>"鸣",
+"鳶"=>"鸢",
+"鳾"=>"䴓",
+"鴆"=>"鸩",
+"鴇"=>"鸨",
+"鴉"=>"鸦",
+"鴒"=>"鸰",
+"鴕"=>"鸵",
+"鴛"=>"鸳",
+"鴝"=>"鸲",
+"鴞"=>"鸮",
+"鴟"=>"鸱",
+"鴣"=>"鸪",
+"鴦"=>"鸯",
+"鴨"=>"鸭",
+"鴯"=>"鸸",
+"鴰"=>"鸹",
+"鴴"=>"鸻",
+"鴷"=>"䴕",
+"鴻"=>"鸿",
+"鴿"=>"鸽",
+"鵁"=>"䴔",
+"鵂"=>"鸺",
+"鵃"=>"鸼",
+"鵐"=>"鹀",
+"鵑"=>"鹃",
+"鵒"=>"鹆",
+"鵓"=>"鹁",
+"鵜"=>"鹈",
+"鵝"=>"鹅",
+"鵠"=>"鹄",
+"鵡"=>"鹉",
+"鵪"=>"鹌",
+"鵬"=>"鹏",
+"鵮"=>"鹐",
+"鵯"=>"鹎",
+"鵲"=>"鹊",
+"鵷"=>"鹓",
+"鵾"=>"鹍",
+"鶄"=>"䴖",
+"鶇"=>"鸫",
+"鶉"=>"鹑",
+"鶊"=>"鹒",
+"鶓"=>"鹋",
+"鶖"=>"鹙",
+"鶘"=>"鹕",
+"鶚"=>"鹗",
+"鶡"=>"鹖",
+"鶥"=>"鹛",
+"鶩"=>"鹜",
+"鶪"=>"䴗",
+"鶬"=>"鸧",
+"鶯"=>"莺",
+"鶲"=>"鹟",
+"鶴"=>"鹤",
+"鶹"=>"鹠",
+"鶺"=>"鹡",
+"鶻"=>"鹘",
+"鶼"=>"鹣",
+"鶿"=>"鹚",
+"鷀"=>"鹚",
+"鷁"=>"鹢",
+"鷂"=>"鹞",
+"鷄"=>"鸡",
+"鷈"=>"䴘",
+"鷊"=>"鹝",
+"鷓"=>"鹧",
+"鷖"=>"鹥",
+"鷗"=>"鸥",
+"鷙"=>"鸷",
+"鷚"=>"鹨",
+"鷥"=>"鸶",
+"鷦"=>"鹪",
+"鷫"=>"鹔",
+"鷯"=>"鹩",
+"鷲"=>"鹫",
+"鷳"=>"鹇",
+"鷸"=>"鹬",
+"鷹"=>"鹰",
+"鷺"=>"鹭",
+"鷽"=>"鸴",
+"鷿"=>"䴙",
+"鸇"=>"鹯",
+"鸌"=>"鹱",
+"鸏"=>"鹲",
+"鸕"=>"鸬",
+"鸘"=>"鹴",
+"鸚"=>"鹦",
+"鸛"=>"鹳",
+"鸝"=>"鹂",
+"鸞"=>"鸾",
+"鹵"=>"卤",
+"鹹"=>"咸",
+"鹺"=>"鹾",
+"鹽"=>"盐",
+"麗"=>"丽",
+"麥"=>"麦",
+"麩"=>"麸",
+"麯"=>"曲",
+"麵"=>"面",
+"麼"=>"么",
+"麽"=>"么",
+"黃"=>"黄",
+"黌"=>"黉",
+"點"=>"点",
+"黨"=>"党",
+"黲"=>"黪",
+"黴"=>"霉",
+"黶"=>"黡",
+"黷"=>"黩",
+"黽"=>"黾",
+"黿"=>"鼋",
+"鼉"=>"鼍",
+"鼕"=>"冬",
+"鼴"=>"鼹",
+"齊"=>"齐",
+"齋"=>"斋",
+"齎"=>"赍",
+"齏"=>"齑",
+"齒"=>"齿",
+"齔"=>"龀",
+"齕"=>"龁",
+"齗"=>"龂",
+"齙"=>"龅",
+"齜"=>"龇",
+"齟"=>"龃",
+"齠"=>"龆",
+"齡"=>"龄",
+"齣"=>"出",
+"齦"=>"龈",
+"齪"=>"龊",
+"齬"=>"龉",
+"齲"=>"龋",
+"齶"=>"腭",
+"齷"=>"龌",
+"龍"=>"龙",
+"龎"=>"厐",
+"龐"=>"庞",
+"龔"=>"龚",
+"龕"=>"龛",
+"龜"=>"龟",
+
+"幾畫" => "几画",
+"賣畫" => "卖画",
+"滷鹼" => "卤碱",
+"原畫" => "原画",
+"口鹼" => "口碱",
+"古畫" => "古画",
+"名畫" => "名画",
+"奇畫" => "奇画",
+"如畫" => "如画",
+"么 " => "幺 ",
+"么廝" => "幺厮",
+"么爹" => "幺爹",
+"弱鹼" => "弱碱",
+"彩畫" => "彩画",
+"所畫" => "所画",
+"扉畫" => "扉画",
+"教畫" => "教画",
+"楊么" => "杨幺",
+"水鹼" => "水碱",
+"洋鹼" => "洋碱",
+"炭畫" => "炭画",
+"畫一" => "画一",
+"畫上" => "画上",
+"畫下" => "画下",
+"畫中" => "画中",
+"畫供" => "画供",
+"畫兒" => "画儿",
+"畫具" => "画具",
+"畫出" => "画出",
+"畫史" => "画史",
+"畫品" => "画品",
+"畫商" => "画商",
+"畫圈" => "画圈",
+"畫境" => "画境",
+"畫工" => "画工",
+"畫帖" => "画帖",
+"畫幅" => "画幅",
+"畫意" => "画意",
+"畫成" => "画成",
+"畫景" => "画景",
+"畫本" => "画本",
+"畫架" => "画架",
+"畫框" => "画框",
+"畫法" => "画法",
+"畫王" => "画王",
+"畫界" => "画界",
+"畫符" => "画符",
+"畫紙" => "画纸",
+"畫線" => "画线",
+"畫航" => "画航",
+"畫舫" => "画舫",
+"畫虎" => "画虎",
+"畫論" => "画论",
+"畫譜" => "画谱",
+"畫象" => "画象",
+"畫質" => "画质",
+"畫貼" => "画贴",
+"畫軸" => "画轴",
+"畫頁" => "画页",
+"鹽鹼" => "盐碱",
+"鹼 " => "碱 ",
+"鹼基" => "碱基",
+"鹼度" => "碱度",
+"鹼水" => "碱水",
+"鹼熔" => "碱熔",
+"磁畫" => "磁画",
+"策畫" => "策画",
+"組畫" => "组画",
+"絹畫" => "绢画",
+"老么" => "老幺",
+"耐鹼" => "耐碱",
+"肉鹼" => "肉碱",
+"膠畫" => "胶画",
+"茶鹼" => "茶碱",
+"西畫" => "西画",
+"貼畫" => "贴画",
+"返鹼" => "返碱",
+"那麼" => "那麽",
+"鍾鍛" => "锺锻",
+"鍛鍾" => "锻锺",
+"雕畫" => "雕画",
+"鯰 " => "鲶 ",
+"麼 " => "麽 ",
+"三聯畫" => "三联画",
+"中國畫" => "中国画",
+"書畫 " => "书画 ",
+"書畫社" => "书画社",
+"五筆畫" => "五笔画",
+"作畫 " => "作画 ",
+"入畫 " => "入画 ",
+"寫生畫" => "写生画",
+"刻畫 " => "刻画 ",
+"動畫 " => "动画 ",
+"勾畫 " => "勾画 ",
+"單色畫" => "单色画",
+"卡通畫" => "卡通画",
+"國畫 " => "国画 ",
+"圖畫 " => "图画 ",
+"壁畫 " => "壁画 ",
+"字畫 " => "字画 ",
+"宣傳畫" => "宣传画",
+"工筆畫" => "工笔画",
+"年畫 " => "年画 ",
+"幽默畫" => "幽默画",
+"指畫 " => "指画 ",
+"描畫 " => "描画 ",
+"插畫 " => "插画 ",
+"擘畫 " => "擘画 ",
+"春畫 " => "春画 ",
+"木刻畫" => "木刻画",
+"機械畫" => "机械画",
+"比畫 " => "比画 ",
+"毛筆畫" => "毛笔画",
+"水粉畫" => "水粉画",
+"油畫 " => "油画 ",
+"海景畫" => "海景画",
+"漫畫 " => "漫画 ",
+"點畫 " => "点画 ",
+"版畫 " => "版画 ",
+"畫 " => "画 ",
+"畫像 " => "画像 ",
+"畫冊 " => "画册 ",
+"畫刊 " => "画刊 ",
+"畫匠 " => "画匠 ",
+"畫捲 " => "画卷 ",
+"畫圖 " => "画图 ",
+"畫壇 " => "画坛 ",
+"畫室 " => "画室 ",
+"畫家 " => "画家 ",
+"畫屏 " => "画屏 ",
+"畫展 " => "画展 ",
+"畫布 " => "画布 ",
+"畫師 " => "画师 ",
+"畫廊 " => "画廊 ",
+"畫報 " => "画报 ",
+"畫押 " => "画押 ",
+"畫板 " => "画板 ",
+"畫片 " => "画片 ",
+"畫畫 " => "画画 ",
+"畫皮 " => "画皮 ",
+"畫眉鳥" => "画眉鸟",
+"畫稿 " => "画稿 ",
+"畫筆 " => "画笔 ",
+"畫院 " => "画院 ",
+"畫集 " => "画集 ",
+"畫面 " => "画面 ",
+"筆畫 " => "笔画 ",
+"細密畫" => "细密画",
+"繪畫 " => "绘画 ",
+"自畫像" => "自画像",
+"蠟筆畫" => "蜡笔画",
+"裸體畫" => "裸体画",
+"西洋畫" => "西洋画",
+"透視畫" => "透视画",
+"銅版畫" => "铜版画",
+"鍾 " => "锺 ",
+"靜物畫" => "静物画",
+"餘 " => "馀 ",
+"記憶體"=>"内存",
+"預設"=>"默认",
+"預設"=>"缺省",
+"串列"=>"串行",
+"乙太網"=>"以太网",
+"點陣圖"=>"位图",
+"常式"=>"例程",
+"通道"=>"信道",
+"游標"=>"光标",
+"光碟"=>"光盘",
+"光碟機"=>"光驱",
+"全形"=>"全角",
+"共用"=>"共享",
+"相容"=>"兼容",
+"首碼"=>"前缀",
+"尾碼"=>"后缀",
+"載入"=>"加载",
+"半形"=>"半角",
+"變數"=>"变量",
+"雜訊"=>"噪声",
+"因數"=>"因子",
+"線上"=>"在线",
+"離線"=>"脱机",
+"功能變數名稱"=>"域名",
+"音效卡"=>"声卡",
+"字型大小"=>"字号",
+"字型檔"=>"字库",
+"欄位"=>"字段",
+"字元"=>"字符",
+"存檔"=>"存盘",
+"定址"=>"寻址",
+"章節附註"=>"尾注",
+"非同步"=>"异步",
+"匯流排"=>"总线",
+"括弧"=>"括号",
+"介面"=>"接口",
+"控制項"=>"控件",
+"許可權"=>"权限",
+"碟片"=>"盘片",
+"矽片"=>"硅片",
+"矽谷"=>"硅谷",
+"硬碟"=>"硬盘",
+"磁碟"=>"磁盘",
+"磁軌"=>"磁道",
+"程式控制"=>"程控",
+"埠"=>"端口",
+"運算元"=>"算子",
+"演算法"=>"算法",
+"晶片"=>"芯片",
+"晶元"=>"芯片",
+"片語"=>"词组",
+"解碼"=>"译码",
+"軟碟機"=>"软驱",
+"快閃記憶體"=>"闪存",
+"滑鼠"=>"鼠标",
+"進位"=>"进制",
+"互動式"=>"交互式",
+"模擬"=>"仿真",
+"優先順序"=>"优先级",
+"感測"=>"传感",
+"攜帶型"=>"便携式",
+"資訊理論"=>"信息论",
+"迴圈"=>"循环",
+"防寫"=>"写保护",
+"分散式"=>"分布式",
+"解析度"=>"分辨率",
+"程式"=>"程序",
+"伺服器"=>"服务器",
+"等於"=>"等于",
+"區域網"=>"局域网",
+"上傳"=>"上载",
+"電腦"=>"计算机",
+"巨集"=>"宏",
+"掃瞄器"=>"扫瞄仪",
+"寬頻"=>"宽带",
+"視窗"=>"窗口",
+"資料庫"=>"数据库",
+"西曆"=>"公历",
+"乳酪"=>"奶酪",
+"鉅賈"=>"巨商",
+"手電筒"=>"手电",
+"萬曆"=>"万历",
+"永曆"=>"永历",
+"辭彙"=>"词汇",
+"保全"=>"保安",
+"慣用"=>"习用",
+"母音"=>"元音",
+"自由球"=>"任意球",
+"頭槌"=>"头球",
+"進球"=>"入球",
+"顆進球"=>"粒入球",
+"射門"=>"打门",
+"蓋火鍋"=>"火锅盖帽",
+"印表機"=>"打印机",
+"打印機"=>"打印机",
+"位元組"=>"字节",
+"字節"=>"字节",
+"列印"=>"打印",
+"打印"=>"打印",
+"硬體"=>"硬件",
+"二極體"=>"二极管",
+"二極管"=>"二极管",
+"三極體"=>"三极管",
+"三極管"=>"三极管",
+"數位"=>"数码",
+"數碼"=>"数码",
+"軟體"=>"软件",
+"軟件"=>"软件",
+"網路"=>"网络",
+"網絡"=>"网络",
+"人工智慧"=>"人工智能",
+"太空梭"=>"航天飞机",
+"穿梭機"=>"航天飞机",
+"網際網路"=>"因特网",
+"互聯網"=>"因特网",
+"機械人"=>"机器人",
+"機器人"=>"机器人",
+"行動電話"=>"移动电话",
+"流動電話"=>"移动电话",
+"調制解調器"=>"调制解调器",
+"數據機"=>"调制解调器",
+"短訊"=>"短信",
+"簡訊"=>"短信",
+"烏茲別克"=>"乌兹别克斯坦",
+"查德"=>"乍得",
+"乍得"=>"乍得",
+"也門"=>"",
+"葉門"=>"也门",
+"伯利茲"=>"伯利兹",
+"貝里斯"=>"伯利兹",
+"維德角"=>"佛得角",
+"佛得角"=>"佛得角",
+"克羅地亞"=>"克罗地亚",
+"克羅埃西亞"=>"克罗地亚",
+"岡比亞"=>"冈比亚",
+"甘比亞"=>"冈比亚",
+"幾內亞比紹"=>"几内亚比绍",
+"幾內亞比索"=>"几内亚比绍",
+"列支敦斯登"=>"列支敦士登",
+"列支敦士登"=>"列支敦士登",
+"利比里亞"=>"利比里亚",
+"賴比瑞亞"=>"利比里亚",
+"加納"=>"加纳",
+"迦納"=>"加纳",
+"加彭"=>"加蓬",
+"加蓬"=>"加蓬",
+"博茨瓦納"=>"博茨瓦纳",
+"波札那"=>"博茨瓦纳",
+"卡塔爾"=>"卡塔尔",
+"卡達"=>"卡塔尔",
+"盧旺達"=>"卢旺达",
+"盧安達"=>"卢旺达",
+"危地馬拉"=>"危地马拉",
+"瓜地馬拉"=>"危地马拉",
+"厄瓜多爾"=>"厄瓜多尔",
+"厄瓜多"=>"厄瓜多尔",
+"厄立特里亞"=>"厄立特里亚",
+"厄利垂亞"=>"厄立特里亚",
+"吉布堤"=>"吉布提",
+"吉布地"=>"吉布提",
+"哈薩克"=>"哈萨克斯坦",
+"哥斯達黎加"=>"哥斯达黎加",
+"哥斯大黎加"=>"哥斯达黎加",
+"圖瓦盧"=>"图瓦卢",
+"吐瓦魯"=>"图瓦卢",
+"土庫曼"=>"土库曼斯坦",
+"聖盧西亞"=>"圣卢西亚",
+"聖露西亞"=>"圣卢西亚",
+"聖吉斯納域斯"=>"圣基茨和尼维斯",
+"聖克里斯多福及尼維斯"=>"圣基茨和尼维斯",
+"聖文森特和格林納丁斯"=>"圣文森特和格林纳丁斯",
+"聖文森及格瑞那丁"=>"圣文森特和格林纳丁斯",
+"聖馬力諾"=>"圣马力诺",
+"聖馬利諾"=>"圣马力诺",
+"圭亞那"=>"圭亚那",
+"蓋亞那"=>"圭亚那",
+"坦桑尼亞"=>"坦桑尼亚",
+"坦尚尼亞"=>"坦桑尼亚",
+"埃塞俄比亞"=>"埃塞俄比亚",
+"衣索比亞"=>"埃塞俄比亚",
+"吉里巴斯"=>"基里巴斯",
+"基里巴斯"=>"基里巴斯",
+"塔吉克"=>"塔吉克斯坦",
+"獅子山"=>"塞拉利昂",
+"塞拉利昂"=>"塞拉利昂",
+"塞普勒斯"=>"塞浦路斯",
+"塞浦路斯"=>"塞浦路斯",
+"塞舌爾"=>"塞舌尔",
+"塞席爾"=>"塞舌尔",
+"多明尼加共和國"=>"多米尼加",
+"多明尼加"=>"多米尼加",
+"多明尼加聯邦"=>"多米尼加联邦",
+"多米尼克"=>"多米尼加联邦",
+"安提瓜和巴布達"=>"安提瓜和巴布达",
+"安地卡及巴布達"=>"安提瓜和巴布达",
+"尼日利亞"=>"尼日利亚",
+"奈及利亞"=>"尼日利亚",
+"尼日爾"=>"尼日尔",
+"尼日"=>"尼日尔",
+"巴貝多"=>"巴巴多斯",
+"巴巴多斯"=>"巴巴多斯",
+"巴布亞新畿內亞"=>"巴布亚新几内亚",
+"巴布亞紐幾內亞"=>"巴布亚新几内亚",
+"布基納法索"=>"布基纳法索",
+"布吉納法索"=>"布基纳法索",
+"蒲隆地"=>"布隆迪",
+"布隆迪"=>"布隆迪",
+"希臘"=>"希腊",
+"帛琉"=>"帕劳",
+"義大利"=>"意大利",
+"意大利"=>"意大利",
+"所羅門群島"=>"所罗门群岛",
+"索羅門群島"=>"所罗门群岛",
+"汶萊"=>"文莱",
+"斯威士蘭"=>"斯威士兰",
+"史瓦濟蘭"=>"斯威士兰",
+"斯洛文尼亞"=>"斯洛文尼亚",
+"斯洛維尼亞"=>"斯洛文尼亚",
+"新西蘭"=>"新西兰",
+"紐西蘭"=>"新西兰",
+"北韓"=>"朝鲜",
+"格林納達"=>"格林纳达",
+"格瑞那達"=>"格林纳达",
+"格魯吉亞"=>"格鲁吉亚",
+"喬治亞"=>"格鲁吉亚",
+"梵蒂岡"=>"梵蒂冈",
+"教廷"=>"梵蒂冈",
+"毛里塔尼亞"=>"毛里塔尼亚",
+"茅利塔尼亞"=>"毛里塔尼亚",
+"毛里裘斯"=>"毛里求斯",
+"模里西斯"=>"毛里求斯",
+"沙地阿拉伯"=>"沙特阿拉伯",
+"沙烏地阿拉伯"=>"沙特阿拉伯",
+"波斯尼亞黑塞哥維那"=>"波斯尼亚和黑塞哥维那",
+"波士尼亞赫塞哥維納"=>"波斯尼亚和黑塞哥维那",
+"津巴布韋"=>"津巴布韦",
+"辛巴威"=>"津巴布韦",
+"宏都拉斯"=>"洪都拉斯",
+"洪都拉斯"=>"洪都拉斯",
+"特立尼達和多巴哥"=>"特立尼达和托巴哥",
+"千里達托貝哥"=>"特立尼达和托巴哥",
+"瑙魯"=>"瑙鲁",
+"諾魯"=>"瑙鲁",
+"瓦努阿圖"=>"瓦努阿图",
+"萬那杜"=>"瓦努阿图",
+"溫納圖"=>"瓦努阿图",
+"科摩羅"=>"科摩罗",
+"葛摩"=>"科摩罗",
+"象牙海岸"=>"科特迪瓦",
+"突尼西亞"=>"突尼斯",
+"索馬里"=>"索马里",
+"索馬利亞"=>"索马里",
+"老撾"=>"老挝",
+"寮國"=>"老挝",
+"肯雅"=>"肯尼亚",
+"肯亞"=>"肯尼亚",
+"蘇利南"=>"苏里南",
+"莫三比克"=>"莫桑比克",
+"莫桑比克"=>"莫桑比克",
+"萊索托"=>"莱索托",
+"賴索托"=>"莱索托",
+"貝寧"=>"贝宁",
+"貝南"=>"贝宁",
+"贊比亞"=>"赞比亚",
+"尚比亞"=>"赞比亚",
+"亞塞拜然"=>"阿塞拜疆",
+"阿塞拜疆"=>"阿塞拜疆",
+"阿拉伯聯合酋長國"=>"阿拉伯联合酋长国",
+"阿拉伯聯合大公國"=>"阿拉伯联合酋长国",
+"南韓"=>"韩国",
+"馬爾代夫"=>"马尔代夫",
+"馬爾地夫"=>"马尔代夫",
+"馬爾他"=>"马耳他",
+"馬里"=>"马里",
+"馬利"=>"马里",
+"即食麵"=>"方便面",
+"快速面"=>"方便面",
+"速食麵"=>"方便面",
+"泡麵"=>"方便面",
+"笨豬跳"=>"蹦极跳",
+"绑紧跳"=>"蹦极跳",
+"冷盤  "=>"凉菜",
+"冷菜"=>"凉菜",
+"散钱"=>"零钱",
+"谐星"=>"笑星    ",
+"夜学"=>"夜校",
+"华乐"=>"民乐",
+"中樂"=>"民乐",
+"住屋"=>"住房",
+"屋价"=>"房价",
+"的士"=>"出租车",
+"計程車"=>"出租车",
+"巴士"=>"公共汽车",
+"公車"=>"公共汽车",
+"單車"=>"自行车",
+"節慶"=>"节日",
+"芝士"=>"乾酪",
+"狗隻"=>"犬只",
+"士多啤梨"=>"草莓",
+"忌廉"=>"奶油",
+"桌球"=>"台球",
+"撞球"=>"台球",
+"雪糕"=>"冰淇淋",
+"衞生"=>"卫生",
+"衛生"=>"卫生",
+"賓士"=>"奔驰",
+"平治"=>"奔驰",
+"捷豹"=>"美洲虎",
+"積架"=>"美洲虎",
+"福斯"=>"大众",
+"福士"=>"大众",
+"雪鐵龍"=>"雪铁龙",
+"萬事得"=>"马自达",
+"馬自達"=>"马自达",
+"寶獅"=>"标志",
+"布殊"=>"布什",
+"布希"=>"布什",
+"柯林頓"=>"克林顿",
+"克林頓"=>"克林顿",
+"薩達姆"=>"萨达姆",
+"海珊"=>"萨达姆",
+"梵谷"=>"凡高",
+"大衛碧咸"=>"大卫·贝克汉姆",
+"米高奧雲"=>"迈克尔·欧文",
+"卡佩雅蒂"=>"珍妮弗·卡普里亚蒂",
+"沙芬"=>"马拉特·萨芬",
+"舒麥加"=>"迈克尔·舒马赫",
+"希特拉"=>"希特勒",
+"戴安娜"=>"狄安娜",
+"黛安娜"=>"狄安娜",
+"希拉"=>"赫拉",
+);
+
+$zh2HK=array(
+"打印机" => "打印機",
+"印表機" => "打印機",
+"字节" => "字節",
+"位元組" => "字節",
+"打印" => "打印",
+"列印" => "打印",
+"硬件" => "硬件",
+"硬體" => "硬件",
+"二极管" => "二極管",
+"二極體" => "二極管",
+"三极管" => "三極管",
+"三極體" => "三極管",
+"数码" => "數碼",
+"數位" => "數碼",
+"软件" => "軟件",
+"軟體" => "軟件",
+"网络" => "網絡",
+"網路" => "網絡",
+"人工智能" => "人工智能",
+"人工智慧" => "人工智能",
+"航天飞机" => "穿梭機",
+"太空梭" => "穿梭機",
+"因特网" => "互聯網",
+"網際網路" => "互聯網",
+"机器人" => "機械人",
+"機器人" => "機械人",
+"移动电话" => "流動電話",
+"行動電話" => "流動電話",
+"调制解调器" => "調制解調器",
+"數據機" => "調制解調器",
+"短信" => "短訊",
+"簡訊" => "短訊",
+"乍得" => "乍得",
+"查德" => "乍得",
+"也门" => "也門",
+"葉門" => "也門",
+"伯利兹" => "伯利茲",
+"貝里斯" => "伯利茲",
+"佛得角" => "佛得角",
+"維德角" => "佛得角",
+"克罗地亚" => "克羅地亞",
+"克羅埃西亞" => "克羅地亞",
+"冈比亚" => "岡比亞",
+"甘比亞" => "岡比亞",
+"几内亚比绍" => "幾內亞比紹",
+"幾內亞比索" => "幾內亞比紹",
+"列支敦士登" => "列支敦士登",
+"列支敦斯登" => "列支敦士登",
+"利比里亚" => "利比里亞",
+"賴比瑞亞" => "利比里亞",
+"加纳" => "加納",
+"迦納" => "加納",
+"加蓬" => "加蓬",
+"加彭" => "加蓬",
+"博茨瓦纳" => "博茨瓦納",
+"波札那" => "博茨瓦納",
+"卡塔尔" => "卡塔爾",
+"卡達" => "卡塔爾",
+"卢旺达" => "盧旺達",
+"盧安達" => "盧旺達",
+"危地马拉" => "危地馬拉",
+"瓜地馬拉" => "危地馬拉",
+"厄瓜多尔" => "厄瓜多爾",
+"厄瓜多" => "厄瓜多爾",
+"厄立特里亚" => "厄立特里亞",
+"厄利垂亞" => "厄立特里亞",
+"吉布提" => "吉布堤",
+"吉布地" => "吉布堤",
+"哥斯达黎加" => "哥斯達黎加",
+"哥斯大黎加" => "哥斯達黎加",
+"图瓦卢" => "圖瓦盧",
+"吐瓦魯" => "圖瓦盧",
+"圣卢西亚" => "聖盧西亞",
+"聖露西亞" => "聖盧西亞",
+"圣基茨和尼维斯" => "聖吉斯納域斯",
+"聖克里斯多福及尼維斯" => "聖吉斯納域斯",
+"圣文森特和格林纳丁斯" => "聖文森特和格林納丁斯",
+"聖文森及格瑞那丁" => "聖文森特和格林納丁斯",
+"圣马力诺" => "聖馬力諾",
+"聖馬利諾" => "聖馬力諾",
+"圭亚那" => "圭亞那",
+"蓋亞那" => "圭亞那",
+"坦桑尼亚" => "坦桑尼亞",
+"坦尚尼亞" => "坦桑尼亞",
+"埃塞俄比亚" => "埃塞俄比亞",
+"衣索比亞" => "埃塞俄比亞",
+"基里巴斯" => "基里巴斯",
+"吉里巴斯" => "基里巴斯",
+"獅子山" => "塞拉利昂",
+"塞普勒斯" => "塞浦路斯",
+"塞舌尔" => "塞舌爾",
+"塞席爾" => "塞舌爾",
+"多米尼加" => "多明尼加共和國",
+"多明尼加" => "多明尼加共和國",
+"多米尼加联邦" => "多明尼加聯邦",
+"多米尼克" => "多明尼加聯邦",
+"安提瓜和巴布达" => "安提瓜和巴布達",
+"安地卡及巴布達" => "安提瓜和巴布達",
+"尼日利亚" => "尼日利亞",
+"奈及利亞" => "尼日利亞",
+"尼日尔" => "尼日爾",
+"尼日" => "尼日爾",
+"巴巴多斯" => "巴巴多斯",
+"巴貝多" => "巴巴多斯",
+"巴布亚新几内亚" => "巴布亞新畿內亞",
+"巴布亞紐幾內亞" => "巴布亞新畿內亞",
+"布基纳法索" => "布基納法索",
+"布吉納法索" => "布基納法索",
+"布隆迪" => "布隆迪",
+"蒲隆地" => "布隆迪",
+"義大利" => "意大利",
+"所罗门群岛" => "所羅門群島",
+"索羅門群島" => "所羅門群島",
+"斯威士兰" => "斯威士蘭",
+"史瓦濟蘭" => "斯威士蘭",
+"斯洛文尼亚" => "斯洛文尼亞",
+"斯洛維尼亞" => "斯洛文尼亞",
+"新西兰" => "新西蘭",
+"紐西蘭" => "新西蘭",
+"格林纳达" => "格林納達",
+"格瑞那達" => "格林納達",
+"格鲁吉亚" => "格魯吉亞",
+"喬治亞" => "格魯吉亞",
+"梵蒂冈" => "梵蒂岡",
+"教廷" => "梵蒂岡",
+"毛里塔尼亚" => "毛里塔尼亞",
+"茅利塔尼亞" => "毛里塔尼亞",
+"毛里求斯" => "毛里裘斯",
+"模里西斯" => "毛里裘斯",
+"沙特阿拉伯" => "沙地阿拉伯",
+"沙烏地阿拉伯" => "沙地阿拉伯",
+"波斯尼亚和黑塞哥维那" => "波斯尼亞黑塞哥維那",
+"波士尼亞赫塞哥維納" => "波斯尼亞黑塞哥維那",
+"津巴布韦" => "津巴布韋",
+"辛巴威" => "津巴布韋",
+"洪都拉斯" => "洪都拉斯",
+"宏都拉斯" => "洪都拉斯",
+"特立尼达和托巴哥" => "特立尼達和多巴哥",
+"千里達托貝哥" => "特立尼達和多巴哥",
+"瑙鲁" => "瑙魯",
+"諾魯" => "瑙魯",
+"瓦努阿图" => "瓦努阿圖",
+"萬那杜" => "瓦努阿圖",
+"科摩罗" => "科摩羅",
+"葛摩" => "科摩羅",
+"索马里" => "索馬里",
+"索馬利亞" => "索馬里",
+"老挝" => "老撾",
+"寮國" => "老撾",
+"肯尼亚" => "肯雅",
+"肯亞" => "肯雅",
+"莫桑比克" => "莫桑比克",
+"莫三比克" => "莫桑比克",
+"莱索托" => "萊索托",
+"賴索托" => "萊索托",
+"贝宁" => "貝寧",
+"貝南" => "貝寧",
+"赞比亚" => "贊比亞",
+"尚比亞" => "贊比亞",
+"阿塞拜疆" => "阿塞拜疆",
+"亞塞拜然" => "阿塞拜疆",
+"阿拉伯联合酋长国" => "阿拉伯聯合酋長國",
+"阿拉伯聯合大公國" => "阿拉伯聯合酋長國",
+"马尔代夫" => "馬爾代夫",
+"馬爾地夫" => "馬爾代夫",
+"马里" => "馬里",
+"馬利" => "馬里",
+"方便面" => "即食麵",
+"快速面" => "即食麵",
+"速食麵" => "即食麵",
+"泡麵" => "即食麵",
+"土豆" => "薯仔",
+"华乐" => "中樂",
+"民乐" => "中樂",
+"計程車 " => "的士",
+"出租车" => "的士",
+"公車" => "巴士",
+"公共汽车" => "巴士",
+"自行车" => "單車",
+"节日" => "節慶",
+"犬只" => "狗隻",
+"台球" => "桌球",
+"撞球" => "桌球",
+"冰淇淋" => "雪糕",
+"冰淇淋" => "雪糕",
+"卫生" => "衞生",
+"衛生" => "衞生",
+"老人" => "長者",
+"賓士" => "平治",
+"捷豹" => "積架",
+"福斯" => "福士",
+"雪铁龙" => "先進",
+"雪鐵龍" => "先進",
+"沃尓沃" => "富豪",
+"马自达" => "萬事得",
+"馬自達" => "萬事得",
+"寶獅" => "標致",
+"布什" => "布殊",
+"布希" => "布殊",
+"克林顿" => "克林頓",
+"柯林頓" => "克林頓",
+"萨达姆" => "薩達姆",
+"海珊" => "薩達姆",
+"大卫·贝克汉姆" => "大衛碧咸",
+"迈克尔·欧文" => "米高奧雲",
+"珍妮弗·卡普里亚蒂" => "卡佩雅蒂",
+"马拉特·萨芬" => "沙芬",
+"迈克尔·舒马赫" => "舒麥加",
+"希特勒" => "希特拉",
+"狄安娜" => "戴安娜",
+"黛安娜" => "戴安娜",
+);
+
+$zh2SG=array(
+"方便面" => "快速面",
+"速食麵" => "快速面",
+"即食麵" => "快速面",
+"蹦极跳" => "绑紧跳",
+"笨豬跳" => "绑紧跳",
+"凉菜" => "冷菜",
+"冷盤" => "冷菜",
+"零钱" => "散钱",
+"散紙" => "散钱",
+"笑星" => "谐星",
+"夜校" => "夜学",
+"民乐" => "华乐",
+"住房" => "住屋",
+"房价" => "屋价",
+"泡麵" => "快速面",
+);
+?> \ No newline at end of file
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
new file mode 100644
index 00000000..4ef8ee4a
--- /dev/null
+++ b/includes/cbt/CBTCompiler.php
@@ -0,0 +1,369 @@
+<?php
+
+/**
+ * This file contains functions to convert callback templates to other languages.
+ * The template should first be pre-processed with CBTProcessor to remove static
+ * sections.
+ */
+
+
+require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
+
+/**
+ * Push a value onto the stack
+ * Argument 1: value
+ */
+define( 'CBT_PUSH', 1 );
+
+/**
+ * Pop, concatenate argument, push
+ * Argument 1: value
+ */
+define( 'CBT_CAT', 2 );
+
+/**
+ * Concatenate where the argument is on the stack, instead of immediate
+ */
+define( 'CBT_CATS', 3 );
+
+/**
+ * Call a function, push the return value onto the stack and put it in the cache
+ * Argument 1: argument count
+ *
+ * The arguments to the function are on the stack
+ */
+define( 'CBT_CALL', 4 );
+
+/**
+ * Pop, htmlspecialchars, push
+ */
+define( 'CBT_HX', 5 );
+
+class CBTOp {
+ var $opcode;
+ var $arg1;
+ var $arg2;
+
+ function CBTOp( $opcode, $arg1, $arg2 ) {
+ $this->opcode = $opcode;
+ $this->arg1 = $arg1;
+ $this->arg2 = $arg2;
+ }
+
+ function name() {
+ $opcodeNames = array(
+ CBT_PUSH => 'PUSH',
+ CBT_CAT => 'CAT',
+ CBT_CATS => 'CATS',
+ CBT_CALL => 'CALL',
+ CBT_HX => 'HX',
+ );
+ return $opcodeNames[$this->opcode];
+ }
+};
+
+class CBTCompiler {
+ var $mOps = array();
+ var $mCode;
+
+ function CBTCompiler( $text ) {
+ $this->mText = $text;
+ }
+
+ /**
+ * Compile the text.
+ * Returns true on success, error message on failure
+ */
+ function compile() {
+ $fname = 'CBTProcessor::compile';
+ $this->mLastError = false;
+ $this->mOps = array();
+
+ $this->doText( 0, strlen( $this->mText ) );
+
+ if ( $this->mLastError !== false ) {
+ $pos = $this->mErrorPos;
+
+ // Find the line number at which the error occurred
+ $startLine = 0;
+ $endLine = 0;
+ $line = 0;
+ do {
+ if ( $endLine ) {
+ $startLine = $endLine + 1;
+ }
+ $endLine = strpos( $this->mText, "\n", $startLine );
+ ++$line;
+ } while ( $endLine !== false && $endLine < $pos );
+
+ $text = "Template error at line $line: $this->mLastError\n<pre>\n";
+
+ $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
+ $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
+ } else {
+ $text = true;
+ }
+
+ return $text;
+ }
+
+ /** Shortcut for doOpenText( $start, $end, false */
+ function doText( $start, $end ) {
+ return $this->doOpenText( $start, $end, false );
+ }
+
+ function phpQuote( $text ) {
+ return "'" . strtr( $text, array( "\\" => "\\\\", "'" => "\\'" ) ) . "'";
+ }
+
+ function op( $opcode, $arg1 = null, $arg2 = null) {
+ return new CBTOp( $opcode, $arg1, $arg2 );
+ }
+
+ /**
+ * Recursive workhorse for text mode.
+ *
+ * Processes text mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
+ * closing brace will flag an error, if $needClosing is true, the lack
+ * of a closing brace will flag an error.
+ *
+ * The parameter $p is advanced to the position after the closing brace,
+ * or after the end. A CBTValue is returned.
+ *
+ * @private
+ */
+ function doOpenText( &$p, $end, $needClosing = true ) {
+ $in =& $this->mText;
+ $start = $p;
+ $atStart = true;
+
+ $foundClosing = false;
+ while ( $p < $end ) {
+ $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
+ $pToken = $p + $matchLength;
+
+ if ( $pToken >= $end ) {
+ // No more braces, output remainder
+ if ( $atStart ) {
+ $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p ) );
+ $atStart = false;
+ } else {
+ $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p ) );
+ }
+ $p = $end;
+ break;
+ }
+
+ // Output the text before the brace
+ if ( $atStart ) {
+ $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $matchLength ) );
+ $atStart = false;
+ } else {
+ $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) );
+ }
+
+ // Advance the pointer
+ $p = $pToken + 1;
+
+ // Check for closing brace
+ if ( $in[$pToken] == '}' ) {
+ $foundClosing = true;
+ break;
+ }
+
+ // Handle the "{fn}" special case
+ if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
+ $this->doOpenFunction( $p, $end );
+ if ( $p < $end && $in[$p] == '"' ) {
+ $this->mOps[] = $this->op( CBT_HX );
+ }
+ } else {
+ $this->doOpenFunction( $p, $end );
+ }
+ if ( $atStart ) {
+ $atStart = false;
+ } else {
+ $this->mOps[] = $this->op( CBT_CATS );
+ }
+ }
+ if ( $foundClosing && !$needClosing ) {
+ $this->error( 'Errant closing brace', $p );
+ } elseif ( !$foundClosing && $needClosing ) {
+ $this->error( 'Unclosed text section', $start );
+ } else {
+ if ( $atStart ) {
+ $this->mOps[] = $this->op( CBT_PUSH, '' );
+ }
+ }
+ }
+
+ /**
+ * Recursive workhorse for function mode.
+ *
+ * Processes function mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
+ * closing brace will flag an error, if $needClosing is true, the lack
+ * of a closing brace will flag an error.
+ *
+ * The parameter $p is advanced to the position after the closing brace,
+ * or after the end. A CBTValue is returned.
+ *
+ * @private
+ */
+ function doOpenFunction( &$p, $end, $needClosing = true ) {
+ $in =& $this->mText;
+ $start = $p;
+ $argCount = 0;
+
+ $foundClosing = false;
+ while ( $p < $end ) {
+ $char = $in[$p];
+ if ( $char == '{' ) {
+ // Switch to text mode
+ ++$p;
+ $tokenStart = $p;
+ $this->doOpenText( $p, $end );
+ ++$argCount;
+ } elseif ( $char == '}' ) {
+ // Block end
+ ++$p;
+ $foundClosing = true;
+ break;
+ } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
+ // Whitespace
+ // Consume the rest of the whitespace
+ $p += strspn( $in, CBT_WHITE, $p, $end - $p );
+ } else {
+ // Token, find the end of it
+ $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
+ $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $tokenLength ) );
+
+ // Execute the token as a function if it's not the function name
+ if ( $argCount ) {
+ $this->mOps[] = $this->op( CBT_CALL, 1 );
+ }
+
+ $p += $tokenLength;
+ ++$argCount;
+ }
+ }
+ if ( !$foundClosing && $needClosing ) {
+ $this->error( 'Unclosed function', $start );
+ return '';
+ }
+
+ $this->mOps[] = $this->op( CBT_CALL, $argCount );
+ }
+
+ /**
+ * Set a flag indicating that an error has been found.
+ */
+ function error( $text, $pos = false ) {
+ $this->mLastError = $text;
+ if ( $pos === false ) {
+ $this->mErrorPos = $this->mCurrentPos;
+ } else {
+ $this->mErrorPos = $pos;
+ }
+ }
+
+ function getLastError() {
+ return $this->mLastError;
+ }
+
+ function opsToString() {
+ $s = '';
+ foreach( $this->mOps as $op ) {
+ $s .= $op->name();
+ if ( !is_null( $op->arg1 ) ) {
+ $s .= ' ' . var_export( $op->arg1, true );
+ }
+ if ( !is_null( $op->arg2 ) ) {
+ $s .= ' ' . var_export( $op->arg2, true );
+ }
+ $s .= "\n";
+ }
+ return $s;
+ }
+
+ function generatePHP( $functionObj ) {
+ $fname = 'CBTCompiler::generatePHP';
+ wfProfileIn( $fname );
+ $stack = array();
+
+ foreach( $this->mOps as $index => $op ) {
+ switch( $op->opcode ) {
+ case CBT_PUSH:
+ $stack[] = $this->phpQuote( $op->arg1 );
+ break;
+ case CBT_CAT:
+ $val = array_pop( $stack );
+ array_push( $stack, "$val . " . $this->phpQuote( $op->arg1 ) );
+ break;
+ case CBT_CATS:
+ $right = array_pop( $stack );
+ $left = array_pop( $stack );
+ array_push( $stack, "$left . $right" );
+ break;
+ case CBT_CALL:
+ $args = array_slice( $stack, count( $stack ) - $op->arg1, $op->arg1 );
+ $stack = array_slice( $stack, 0, count( $stack ) - $op->arg1 );
+
+ // Some special optimised expansions
+ if ( $op->arg1 == 0 ) {
+ $result = '';
+ } else {
+ $func = array_shift( $args );
+ if ( substr( $func, 0, 1 ) == "'" && substr( $func, -1 ) == "'" ) {
+ $func = substr( $func, 1, strlen( $func ) - 2 );
+ if ( $func == "if" ) {
+ if ( $op->arg1 < 3 ) {
+ // This should have been caught during processing
+ return "Not enough arguments to if";
+ } elseif ( $op->arg1 == 3 ) {
+ $result = "(({$args[0]} != '') ? ({$args[1]}) : '')";
+ } else {
+ $result = "(({$args[0]} != '') ? ({$args[1]}) : ({$args[2]}))";
+ }
+ } elseif ( $func == "true" ) {
+ $result = "true";
+ } elseif( $func == "lbrace" || $func == "{" ) {
+ $result = "{";
+ } elseif( $func == "rbrace" || $func == "}" ) {
+ $result = "}";
+ } elseif ( $func == "escape" || $func == "~" ) {
+ $result = "htmlspecialchars({$args[0]})";
+ } else {
+ // Known function name
+ $result = "{$functionObj}->{$func}(" . implode( ', ', $args ) . ')';
+ }
+ } else {
+ // Unknown function name
+ $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )';
+ }
+ }
+ array_push( $stack, $result );
+ break;
+ case CBT_HX:
+ $val = array_pop( $stack );
+ array_push( $stack, "htmlspecialchars( $val )" );
+ break;
+ default:
+ return "Unknown opcode {$op->opcode}\n";
+ }
+ }
+ wfProfileOut( $fname );
+ if ( count( $stack ) !== 1 ) {
+ return "Error, stack count incorrect\n";
+ }
+ return '
+ global $cbtExecutingGenerated;
+ ++$cbtExecutingGenerated;
+ $output = ' . $stack[0] . ';
+ --$cbtExecutingGenerated;
+ return $output;
+ ';
+ }
+}
+?>
diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php
new file mode 100644
index 00000000..0c34204e
--- /dev/null
+++ b/includes/cbt/CBTProcessor.php
@@ -0,0 +1,540 @@
+<?php
+
+/**
+ * PHP version of the callback template processor
+ * This is currently used as a test rig and is likely to be used for
+ * compatibility purposes later, where the C++ extension is not available.
+ */
+
+define( 'CBT_WHITE', " \t\r\n" );
+define( 'CBT_BRACE', '{}' );
+define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
+define( 'CBT_DEBUG', 0 );
+
+$GLOBALS['cbtExecutingGenerated'] = 0;
+
+/**
+ * Attempting to be a MediaWiki-independent module
+ */
+if ( !function_exists( 'wfProfileIn' ) ) {
+ function wfProfileIn() {}
+}
+if ( !function_exists( 'wfProfileOut' ) ) {
+ function wfProfileOut() {}
+}
+
+/**
+ * Escape text for inclusion in template
+ */
+function cbt_escape( $text ) {
+ return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
+}
+
+/**
+ * Create a CBTValue
+ */
+function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
+ global $cbtExecutingGenerated;
+ if ( $cbtExecutingGenerated ) {
+ return $text;
+ } else {
+ return new CBTValue( $text, $deps, $isTemplate );
+ }
+}
+
+/**
+ * A dependency-tracking value class
+ * Callback functions should return one of these, unless they have
+ * no dependencies in which case they can return a string.
+ */
+class CBTValue {
+ var $mText, $mDeps, $mIsTemplate;
+
+ /**
+ * Create a new value
+ * @param $text String: , default ''.
+ * @param $deps Array: what this value depends on
+ * @param $isTemplate Bool: whether the result needs compilation/execution, default 'false'.
+ */
+ function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
+ $this->mText = $text;
+ if ( !is_array( $deps ) ) {
+ $this->mDeps = array( $deps ) ;
+ } else {
+ $this->mDeps = $deps;
+ }
+ $this->mIsTemplate = $isTemplate;
+ }
+
+ /** Concatenate two values, merging their dependencies */
+ function cat( $val ) {
+ if ( is_object( $val ) ) {
+ $this->addDeps( $val );
+ $this->mText .= $val->mText;
+ } else {
+ $this->mText .= $val;
+ }
+ }
+
+ /** Add the dependencies of another value to this one */
+ function addDeps( $values ) {
+ if ( !is_array( $values ) ) {
+ $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
+ } else {
+ foreach ( $values as $val ) {
+ if ( !is_object( $val ) ) {
+ var_dump( debug_backtrace() );
+ exit;
+ }
+ $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
+ }
+ }
+ }
+
+ /** Remove a list of dependencies */
+ function removeDeps( $deps ) {
+ $this->mDeps = array_diff( $this->mDeps, $deps );
+ }
+
+ function setText( $text ) {
+ $this->mText = $text;
+ }
+
+ function getText() {
+ return $this->mText;
+ }
+
+ function getDeps() {
+ return $this->mDeps;
+ }
+
+ /** If the value is a template, execute it */
+ function execute( &$processor ) {
+ if ( $this->mIsTemplate ) {
+ $myProcessor = new CBTProcessor( $this->mText, $processor->mFunctionObj, $processor->mIgnorableDeps );
+ $myProcessor->mCompiling = $processor->mCompiling;
+ $val = $myProcessor->doText( 0, strlen( $this->mText ) );
+ if ( $myProcessor->getLastError() ) {
+ $processor->error( $myProcessor->getLastError() );
+ $this->mText = '';
+ } else {
+ $this->mText = $val->mText;
+ $this->addDeps( $val );
+ }
+ if ( !$processor->mCompiling ) {
+ $this->mIsTemplate = false;
+ }
+ }
+ }
+
+ /** If the value is plain text, escape it for inclusion in a template */
+ function templateEscape() {
+ if ( !$this->mIsTemplate ) {
+ $this->mText = cbt_escape( $this->mText );
+ }
+ }
+
+ /** Return true if the value has no dependencies */
+ function isStatic() {
+ return count( $this->mDeps ) == 0;
+ }
+}
+
+/**
+ * Template processor, for compilation and execution
+ */
+class CBTProcessor {
+ var $mText, # The text being processed
+ $mFunctionObj, # The object containing callback functions
+ $mCompiling = false, # True if compiling to a template, false if executing to text
+ $mIgnorableDeps = array(), # Dependency names which should be treated as static
+ $mFunctionCache = array(), # A cache of function results keyed by argument hash
+ $mLastError = false, # Last error message or false for no error
+ $mErrorPos = 0, # Last error position
+
+ /** Built-in functions */
+ $mBuiltins = array(
+ 'if' => 'bi_if',
+ 'true' => 'bi_true',
+ '[' => 'bi_lbrace',
+ 'lbrace' => 'bi_lbrace',
+ ']' => 'bi_rbrace',
+ 'rbrace' => 'bi_rbrace',
+ 'escape' => 'bi_escape',
+ '~' => 'bi_escape',
+ );
+
+ /**
+ * Create a template processor for a given text, callback object and static dependency list
+ */
+ function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
+ $this->mText = $text;
+ $this->mFunctionObj = $functionObj;
+ $this->mIgnorableDeps = $ignorableDeps;
+ }
+
+ /**
+ * Execute the template.
+ * If $compile is true, produces an optimised template where functions with static
+ * dependencies have been replaced by their return values.
+ */
+ function execute( $compile = false ) {
+ $fname = 'CBTProcessor::execute';
+ wfProfileIn( $fname );
+ $this->mCompiling = $compile;
+ $this->mLastError = false;
+ $val = $this->doText( 0, strlen( $this->mText ) );
+ $text = $val->getText();
+ if ( $this->mLastError !== false ) {
+ $pos = $this->mErrorPos;
+
+ // Find the line number at which the error occurred
+ $startLine = 0;
+ $endLine = 0;
+ $line = 0;
+ do {
+ if ( $endLine ) {
+ $startLine = $endLine + 1;
+ }
+ $endLine = strpos( $this->mText, "\n", $startLine );
+ ++$line;
+ } while ( $endLine !== false && $endLine < $pos );
+
+ $text = "Template error at line $line: $this->mLastError\n<pre>\n";
+
+ $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
+ $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
+ }
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /** Shortcut for execute(true) */
+ function compile() {
+ $fname = 'CBTProcessor::compile';
+ wfProfileIn( $fname );
+ $s = $this->execute( true );
+ wfProfileOut( $fname );
+ return $s;
+ }
+
+ /** Shortcut for doOpenText( $start, $end, false */
+ function doText( $start, $end ) {
+ return $this->doOpenText( $start, $end, false );
+ }
+
+ /**
+ * Escape text for a template if we are producing a template. Do nothing
+ * if we are producing plain text.
+ */
+ function templateEscape( $text ) {
+ if ( $this->mCompiling ) {
+ return cbt_escape( $text );
+ } else {
+ return $text;
+ }
+ }
+
+ /**
+ * Recursive workhorse for text mode.
+ *
+ * Processes text mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
+ * closing brace will flag an error, if $needClosing is true, the lack
+ * of a closing brace will flag an error.
+ *
+ * The parameter $p is advanced to the position after the closing brace,
+ * or after the end. A CBTValue is returned.
+ *
+ * @private
+ */
+ function doOpenText( &$p, $end, $needClosing = true ) {
+ $fname = 'CBTProcessor::doOpenText';
+ wfProfileIn( $fname );
+ $in =& $this->mText;
+ $start = $p;
+ $ret = new CBTValue( '', array(), $this->mCompiling );
+
+ $foundClosing = false;
+ while ( $p < $end ) {
+ $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
+ $pToken = $p + $matchLength;
+
+ if ( $pToken >= $end ) {
+ // No more braces, output remainder
+ $ret->cat( substr( $in, $p ) );
+ $p = $end;
+ break;
+ }
+
+ // Output the text before the brace
+ $ret->cat( substr( $in, $p, $matchLength ) );
+
+ // Advance the pointer
+ $p = $pToken + 1;
+
+ // Check for closing brace
+ if ( $in[$pToken] == '}' ) {
+ $foundClosing = true;
+ break;
+ }
+
+ // Handle the "{fn}" special case
+ if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
+ wfProfileOut( $fname );
+ $val = $this->doOpenFunction( $p, $end );
+ wfProfileIn( $fname );
+ if ( $p < $end && $in[$p] == '"' ) {
+ $val->setText( htmlspecialchars( $val->getText() ) );
+ }
+ $ret->cat( $val );
+ } else {
+ // Process the function mode component
+ wfProfileOut( $fname );
+ $ret->cat( $this->doOpenFunction( $p, $end ) );
+ wfProfileIn( $fname );
+ }
+ }
+ if ( $foundClosing && !$needClosing ) {
+ $this->error( 'Errant closing brace', $p );
+ } elseif ( !$foundClosing && $needClosing ) {
+ $this->error( 'Unclosed text section', $start );
+ }
+ wfProfileOut( $fname );
+ return $ret;
+ }
+
+ /**
+ * Recursive workhorse for function mode.
+ *
+ * Processes function mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
+ * closing brace will flag an error, if $needClosing is true, the lack
+ * of a closing brace will flag an error.
+ *
+ * The parameter $p is advanced to the position after the closing brace,
+ * or after the end. A CBTValue is returned.
+ *
+ * @private
+ */
+ function doOpenFunction( &$p, $end, $needClosing = true ) {
+ $in =& $this->mText;
+ $start = $p;
+ $tokens = array();
+ $unexecutedTokens = array();
+
+ $foundClosing = false;
+ while ( $p < $end ) {
+ $char = $in[$p];
+ if ( $char == '{' ) {
+ // Switch to text mode
+ ++$p;
+ $tokenStart = $p;
+ $token = $this->doOpenText( $p, $end );
+ $tokens[] = $token;
+ $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
+ } elseif ( $char == '}' ) {
+ // Block end
+ ++$p;
+ $foundClosing = true;
+ break;
+ } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
+ // Whitespace
+ // Consume the rest of the whitespace
+ $p += strspn( $in, CBT_WHITE, $p, $end - $p );
+ } else {
+ // Token, find the end of it
+ $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
+ $token = new CBTValue( substr( $in, $p, $tokenLength ) );
+ // Execute the token as a function if it's not the function name
+ if ( count( $tokens ) ) {
+ $tokens[] = $this->doFunction( array( $token ), $p );
+ } else {
+ $tokens[] = $token;
+ }
+ $unexecutedTokens[] = $token->getText();
+
+ $p += $tokenLength;
+ }
+ }
+ if ( !$foundClosing && $needClosing ) {
+ $this->error( 'Unclosed function', $start );
+ return '';
+ }
+
+ $val = $this->doFunction( $tokens, $start );
+ if ( $this->mCompiling && !$val->isStatic() ) {
+ $compiled = '';
+ $first = true;
+ foreach( $tokens as $i => $token ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $compiled .= ' ';
+ }
+ if ( $token->isStatic() ) {
+ if ( $i !== 0 ) {
+ $compiled .= '{' . $token->getText() . '}';
+ } else {
+ $compiled .= $token->getText();
+ }
+ } else {
+ $compiled .= $unexecutedTokens[$i];
+ }
+ }
+
+ // The dynamic parts of the string are still represented as functions, and
+ // function invocations have no dependencies. Thus the compiled result has
+ // no dependencies.
+ $val = new CBTValue( "{{$compiled}}", array(), true );
+ }
+ return $val;
+ }
+
+ /**
+ * Execute a function, caching and returning the result value.
+ * $tokens is an array of CBTValue objects. $tokens[0] is the function
+ * name, the others are arguments. $p is the string position, and is used
+ * for error messages only.
+ */
+ function doFunction( $tokens, $p ) {
+ if ( count( $tokens ) == 0 ) {
+ return new CBTValue;
+ }
+ $fname = 'CBTProcessor::doFunction';
+ wfProfileIn( $fname );
+
+ $ret = new CBTValue;
+
+ // All functions implicitly depend on their arguments, and the function name
+ // While this is not strictly necessary for all functions, it's true almost
+ // all the time and so convenient to do automatically.
+ $ret->addDeps( $tokens );
+
+ $this->mCurrentPos = $p;
+ $func = array_shift( $tokens );
+ $func = $func->getText();
+
+ // Extract the text component from all the tokens
+ // And convert any templates to plain text
+ $textArgs = array();
+ foreach ( $tokens as $token ) {
+ $token->execute( $this );
+ $textArgs[] = $token->getText();
+ }
+
+ // Try the local cache
+ $cacheKey = $func . "\n" . implode( "\n", $textArgs );
+ if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
+ $val = $this->mFunctionCache[$cacheKey];
+ } elseif ( isset( $this->mBuiltins[$func] ) ) {
+ $func = $this->mBuiltins[$func];
+ $val = call_user_func_array( array( &$this, $func ), $tokens );
+ $this->mFunctionCache[$cacheKey] = $val;
+ } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
+ $profName = get_class( $this->mFunctionObj ) . '::' . $func;
+ wfProfileIn( "$fname-callback" );
+ wfProfileIn( $profName );
+ $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
+ wfProfileOut( $profName );
+ wfProfileOut( "$fname-callback" );
+ $this->mFunctionCache[$cacheKey] = $val;
+ } else {
+ $this->error( "Call of undefined function \"$func\"", $p );
+ $val = new CBTValue;
+ }
+ if ( !is_object( $val ) ) {
+ $val = new CBTValue((string)$val);
+ }
+
+ if ( CBT_DEBUG ) {
+ $unexpanded = $val;
+ }
+
+ // If the output was a template, execute it
+ $val->execute( $this );
+
+ if ( $this->mCompiling ) {
+ // Escape any braces so that the output will be a valid template
+ $val->templateEscape();
+ }
+ $val->removeDeps( $this->mIgnorableDeps );
+ $ret->addDeps( $val );
+ $ret->setText( $val->getText() );
+
+ if ( CBT_DEBUG ) {
+ wfDebug( "doFunction $func args = "
+ . var_export( $tokens, true )
+ . "unexpanded return = "
+ . var_export( $unexpanded, true )
+ . "expanded return = "
+ . var_export( $ret, true )
+ );
+ }
+
+ wfProfileOut( $fname );
+ return $ret;
+ }
+
+ /**
+ * Set a flag indicating that an error has been found.
+ */
+ function error( $text, $pos = false ) {
+ $this->mLastError = $text;
+ if ( $pos === false ) {
+ $this->mErrorPos = $this->mCurrentPos;
+ } else {
+ $this->mErrorPos = $pos;
+ }
+ }
+
+ function getLastError() {
+ return $this->mLastError;
+ }
+
+ /** 'if' built-in function */
+ function bi_if( $condition, $trueBlock, $falseBlock = null ) {
+ if ( is_null( $condition ) ) {
+ $this->error( "Missing condition in if" );
+ return '';
+ }
+
+ if ( $condition->getText() != '' ) {
+ return new CBTValue( $trueBlock->getText(),
+ array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
+ $trueBlock->mIsTemplate );
+ } else {
+ if ( !is_null( $falseBlock ) ) {
+ return new CBTValue( $falseBlock->getText(),
+ array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
+ $falseBlock->mIsTemplate );
+ } else {
+ return new CBTValue( '', $condition->getDeps() );
+ }
+ }
+ }
+
+ /** 'true' built-in function */
+ function bi_true() {
+ return "true";
+ }
+
+ /** left brace built-in */
+ function bi_lbrace() {
+ return '{';
+ }
+
+ /** right brace built-in */
+ function bi_rbrace() {
+ return '}';
+ }
+
+ /**
+ * escape built-in.
+ * Escape text for inclusion in an HTML attribute
+ */
+ function bi_escape( $val ) {
+ return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
+ }
+}
+?>
diff --git a/includes/cbt/README b/includes/cbt/README
new file mode 100644
index 00000000..cffcef2f
--- /dev/null
+++ b/includes/cbt/README
@@ -0,0 +1,108 @@
+Overview
+--------
+
+CBT (callback-based templates) is an experimental system for improving skin
+rendering time in MediaWiki and similar applications. The fundamental concept is
+a template language which contains tags which pull text from PHP callbacks.
+These PHP callbacks do not simply return text, they also return a description of
+the dependencies -- the global data upon which the returned text depends. This
+allows a compiler to produce a template optimised for a certain context. For
+example, a user-dependent template can be produced, with the username replaced
+by static text, as well as all user preference dependent text.
+
+This was an experimental project to prove the concept -- to explore possible
+efficiency gains and techniques. TemplateProcessor was the first element of this
+experiment. It is a class written in PHP which parses a template, and produces
+either an optimised template with dependencies removed, or the output text
+itself. I found that even with a heavily optimised template, this processor was
+not fast enough to match the speed of the original MonoBook.
+
+To improve the efficiency, I wrote TemplateCompiler, which takes a template,
+preferably pre-optimised by TemplateProcessor, and generates PHP code from it.
+The generated code is a single expression, concatenating static text and
+callback results. This approach turned out to be very efficient, making
+significant time savings compared to the original MonoBook.
+
+Despite this success, the code has been shelved for the time being. There were
+a number of unresolved implementation problems, and I felt that there were more
+pressing priorities for MediaWiki development than solving them and bringing
+this module to completion. I also believe that more research is needed into
+other possible template architectures. There is nothing fundamentally wrong with
+the CBT concept, and I would encourage others to continue its development.
+
+The problems I saw were:
+
+* Extensibility. Can non-Wikimedia installations easily extend and modify CBT
+ skins? Patching seems to be necessary, is this acceptable? MediaWiki
+ extensions are another problem. Unless the interfaces allow them to return
+ dependencies, any hooks will have to be marked dynamic and thus inefficient.
+
+* Cache invalidation. This is a simple implementation issue, although it would
+ require extensive modification to the MediaWiki core.
+
+* Syntax. The syntax is minimalistic and easy to parse, but can be quite ugly.
+ Will generations of MediaWiki users curse my name?
+
+* Security. The code produced by TemplateCompiler is best stored in memcached
+ and executed with eval(). This allows anyone with access to the memcached port
+ to run code as the apache user.
+
+
+Template syntax
+---------------
+
+There are two modes: text mode and function mode. The brace characters "{"
+and "}" are the only reserved characters. Either one of them will switch from
+text mode to function mode wherever they appear, no exceptions.
+
+In text mode, all characters are passed through to the output. In function
+mode, text is split into tokens, delimited either by whitespace or by
+matching pairs of braces. The first token is taken to be a function name. The
+other tokens are first processed in function mode themselves, then they are
+passed to the named function as parameters. The return value of the function
+is passed through to the output.
+
+Example:
+ {escape {"hello"}}
+
+First brace switches to function mode. The function name is escape, the first
+and only parameter is {"hello"}. This parameter is executed. The braces around
+the parameter cause the parser to switch to text mode, thus the string "hello",
+including the quotes, is passed back and used as an argument to the escape
+function.
+
+Example:
+ {if title {<h1>{title}</h1>}}
+
+The function name is "if". The first parameter is the result of calling the
+function "title". The second parameter is a level 1 HTML heading containing
+the result of the function "title". "if" is a built-in function which will
+return the second parameter only if the first is non-blank, so the effect of
+this is to return a heading element only if a title exists.
+
+As a shortcut for generation of HTML attributes, if a function mode segment is
+surrounded by double quotes, quote characters in the return value will be
+escaped. This only applies if the quote character immediately precedes the
+opening brace, and immediately follows the closing brace, with no whitespace.
+
+User callback functions are defined by passing a function object to the
+template processor. Function names appearing in the text are first checked
+against built-in function names, then against the method names in the function
+object. The function object forms a sandbox for execution of the template, so
+security-conscious users may wish to avoid including functions that allow
+arbitrary filesystem access or code execution.
+
+The callback function will receive its parameters as strings. If the
+result of the function depends only on the arguments, and certain things
+understood to be "static", such as the source code, then the callback function
+should return a string. If the result depends on other things, then the function
+should call cbt_value() to get a return value:
+
+ return cbt_value( $text, $deps );
+
+where $deps is an array of string tokens, each one naming a dependency. As a
+shortcut, if there is only one dependency, $deps may be a string.
+
+
+---------------------
+Tim Starling 2006
diff --git a/includes/memcached-client.php b/includes/memcached-client.php
new file mode 100644
index 00000000..697509e8
--- /dev/null
+++ b/includes/memcached-client.php
@@ -0,0 +1,1060 @@
+<?php
+//
+// +---------------------------------------------------------------------------+
+// | memcached client, PHP |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net> |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | Author: Ryan T. Dean <rtdean@cytherianage.net> |
+// | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. |
+// | Permission granted by Brad Fitzpatrick for relicense of ported Perl |
+// | client logic under 2-clause BSD license. |
+// +---------------------------------------------------------------------------+
+//
+// $TCAnet$
+//
+
+/**
+ * This is the PHP client for memcached - a distributed memory cache daemon.
+ * More information is available at http://www.danga.com/memcached/
+ *
+ * Usage example:
+ *
+ * require_once 'memcached.php';
+ *
+ * $mc = new memcached(array(
+ * 'servers' => array('127.0.0.1:10000',
+ * array('192.0.0.1:10010', 2),
+ * '127.0.0.1:10020'),
+ * 'debug' => false,
+ * 'compress_threshold' => 10240,
+ * 'persistant' => true));
+ *
+ * $mc->add('key', array('some', 'array'));
+ * $mc->replace('key', 'some random string');
+ * $val = $mc->get('key');
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @package memcached-client
+ * @version 0.1.2
+ */
+
+// {{{ requirements
+// }}}
+
+// {{{ constants
+// {{{ flags
+
+/**
+ * Flag: indicates data is serialized
+ */
+define("MEMCACHE_SERIALIZED", 1<<0);
+
+/**
+ * Flag: indicates data is compressed
+ */
+define("MEMCACHE_COMPRESSED", 1<<1);
+
+// }}}
+
+/**
+ * Minimum savings to store data compressed
+ */
+define("COMPRESSION_SAVINGS", 0.20);
+
+// }}}
+
+// {{{ class memcached
+/**
+ * memcached client class implemented using (p)fsockopen()
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @package memcached-client
+ */
+class memcached
+{
+ // {{{ properties
+ // {{{ public
+
+ /**
+ * Command statistics
+ *
+ * @var array
+ * @access public
+ */
+ var $stats;
+
+ // }}}
+ // {{{ private
+
+ /**
+ * Cached Sockets that are connected
+ *
+ * @var array
+ * @access private
+ */
+ var $_cache_sock;
+
+ /**
+ * Current debug status; 0 - none to 9 - profiling
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_debug;
+
+ /**
+ * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
+ *
+ * @var array
+ * @access private
+ */
+ var $_host_dead;
+
+ /**
+ * Is compression available?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_have_zlib;
+
+ /**
+ * Do we want to use compression?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_compress_enable;
+
+ /**
+ * At how many bytes should we compress?
+ *
+ * @var interger
+ * @access private
+ */
+ var $_compress_threshold;
+
+ /**
+ * Are we using persistant links?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_persistant;
+
+ /**
+ * If only using one server; contains ip:port to connect to
+ *
+ * @var string
+ * @access private
+ */
+ var $_single_sock;
+
+ /**
+ * Array containing ip:port or array(ip:port, weight)
+ *
+ * @var array
+ * @access private
+ */
+ var $_servers;
+
+ /**
+ * Our bit buckets
+ *
+ * @var array
+ * @access private
+ */
+ var $_buckets;
+
+ /**
+ * Total # of bit buckets we have
+ *
+ * @var interger
+ * @access private
+ */
+ var $_bucketcount;
+
+ /**
+ * # of total servers we have
+ *
+ * @var interger
+ * @access private
+ */
+ var $_active;
+
+ /**
+ * Stream timeout in seconds. Applies for example to fread()
+ *
+ * @var integer
+ * @access private
+ */
+ var $_timeout_seconds;
+
+ /**
+ * Stream timeout in microseconds
+ *
+ * @var integer
+ * @access private
+ */
+ var $_timeout_microseconds;
+
+ // }}}
+ // }}}
+ // {{{ methods
+ // {{{ public functions
+ // {{{ memcached()
+
+ /**
+ * Memcache initializer
+ *
+ * @param array $args Associative array of settings
+ *
+ * @return mixed
+ * @access public
+ */
+ function memcached ($args)
+ {
+ $this->set_servers(@$args['servers']);
+ $this->_debug = @$args['debug'];
+ $this->stats = array();
+ $this->_compress_threshold = @$args['compress_threshold'];
+ $this->_persistant = array_key_exists('persistant', $args) ? (@$args['persistant']) : false;
+ $this->_compress_enable = true;
+ $this->_have_zlib = function_exists("gzcompress");
+
+ $this->_cache_sock = array();
+ $this->_host_dead = array();
+
+ $this->_timeout_seconds = 1;
+ $this->_timeout_microseconds = 0;
+ }
+
+ // }}}
+ // {{{ add()
+
+ /**
+ * Adds a key/value to the memcache server if one isn't already set with
+ * that key
+ *
+ * @param string $key Key to set with data
+ * @param mixed $val Value to store
+ * @param interger $exp (optional) Time to expire data at
+ *
+ * @return boolean
+ * @access public
+ */
+ function add ($key, $val, $exp = 0)
+ {
+ return $this->_set('add', $key, $val, $exp);
+ }
+
+ // }}}
+ // {{{ decr()
+
+ /**
+ * Decriment a value stored on the memcache server
+ *
+ * @param string $key Key to decriment
+ * @param interger $amt (optional) Amount to decriment
+ *
+ * @return mixed FALSE on failure, value on success
+ * @access public
+ */
+ function decr ($key, $amt=1)
+ {
+ return $this->_incrdecr('decr', $key, $amt);
+ }
+
+ // }}}
+ // {{{ delete()
+
+ /**
+ * Deletes a key from the server, optionally after $time
+ *
+ * @param string $key Key to delete
+ * @param interger $time (optional) How long to wait before deleting
+ *
+ * @return boolean TRUE on success, FALSE on failure
+ * @access public
+ */
+ function delete ($key, $time = 0)
+ {
+ if (!$this->_active)
+ return false;
+
+ $sock = $this->get_sock($key);
+ if (!is_resource($sock))
+ return false;
+
+ $key = is_array($key) ? $key[1] : $key;
+
+ @$this->stats['delete']++;
+ $cmd = "delete $key $time\r\n";
+ if(!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
+ {
+ $this->_dead_sock($sock);
+ return false;
+ }
+ $res = trim(fgets($sock));
+
+ if ($this->_debug)
+ $this->_debugprint(sprintf("MemCache: delete %s (%s)\n", $key, $res));
+
+ if ($res == "DELETED")
+ return true;
+ return false;
+ }
+
+ // }}}
+ // {{{ disconnect_all()
+
+ /**
+ * Disconnects all connected sockets
+ *
+ * @access public
+ */
+ function disconnect_all ()
+ {
+ foreach ($this->_cache_sock as $sock)
+ fclose($sock);
+
+ $this->_cache_sock = array();
+ }
+
+ // }}}
+ // {{{ enable_compress()
+
+ /**
+ * Enable / Disable compression
+ *
+ * @param boolean $enable TRUE to enable, FALSE to disable
+ *
+ * @access public
+ */
+ function enable_compress ($enable)
+ {
+ $this->_compress_enable = $enable;
+ }
+
+ // }}}
+ // {{{ forget_dead_hosts()
+
+ /**
+ * Forget about all of the dead hosts
+ *
+ * @access public
+ */
+ function forget_dead_hosts ()
+ {
+ $this->_host_dead = array();
+ }
+
+ // }}}
+ // {{{ get()
+
+ /**
+ * Retrieves the value associated with the key from the memcache server
+ *
+ * @param string $key Key to retrieve
+ *
+ * @return mixed
+ * @access public
+ */
+ function get ($key)
+ {
+ $fname = 'memcached::get';
+ wfProfileIn( $fname );
+
+ if (!$this->_active) {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ $sock = $this->get_sock($key);
+
+ if (!is_resource($sock)) {
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ @$this->stats['get']++;
+
+ $cmd = "get $key\r\n";
+ if (!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
+ {
+ $this->_dead_sock($sock);
+ wfProfileOut( $fname );
+ return false;
+ }
+
+ $val = array();
+ $this->_load_items($sock, $val);
+
+ if ($this->_debug)
+ foreach ($val as $k => $v)
+ $this->_debugprint(@sprintf("MemCache: sock %s got %s => %s\r\n", serialize($sock), $k, $v));
+
+ wfProfileOut( $fname );
+ return @$val[$key];
+ }
+
+ // }}}
+ // {{{ get_multi()
+
+ /**
+ * Get multiple keys from the server(s)
+ *
+ * @param array $keys Keys to retrieve
+ *
+ * @return array
+ * @access public
+ */
+ function get_multi ($keys)
+ {
+ if (!$this->_active)
+ return false;
+
+ $this->stats['get_multi']++;
+
+ foreach ($keys as $key)
+ {
+ $sock = $this->get_sock($key);
+ if (!is_resource($sock)) continue;
+ $key = is_array($key) ? $key[1] : $key;
+ if (!isset($sock_keys[$sock]))
+ {
+ $sock_keys[$sock] = array();
+ $socks[] = $sock;
+ }
+ $sock_keys[$sock][] = $key;
+ }
+
+ // Send out the requests
+ foreach ($socks as $sock)
+ {
+ $cmd = "get";
+ foreach ($sock_keys[$sock] as $key)
+ {
+ $cmd .= " ". $key;
+ }
+ $cmd .= "\r\n";
+
+ if ($this->_safe_fwrite($sock, $cmd, strlen($cmd)))
+ {
+ $gather[] = $sock;
+ } else
+ {
+ $this->_dead_sock($sock);
+ }
+ }
+
+ // Parse responses
+ $val = array();
+ foreach ($gather as $sock)
+ {
+ $this->_load_items($sock, $val);
+ }
+
+ if ($this->_debug)
+ foreach ($val as $k => $v)
+ $this->_debugprint(sprintf("MemCache: got %s => %s\r\n", $k, $v));
+
+ return $val;
+ }
+
+ // }}}
+ // {{{ incr()
+
+ /**
+ * Increments $key (optionally) by $amt
+ *
+ * @param string $key Key to increment
+ * @param interger $amt (optional) amount to increment
+ *
+ * @return interger New key value?
+ * @access public
+ */
+ function incr ($key, $amt=1)
+ {
+ return $this->_incrdecr('incr', $key, $amt);
+ }
+
+ // }}}
+ // {{{ replace()
+
+ /**
+ * Overwrites an existing value for key; only works if key is already set
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to store
+ * @param interger $exp (optional) Experiation time
+ *
+ * @return boolean
+ * @access public
+ */
+ function replace ($key, $value, $exp=0)
+ {
+ return $this->_set('replace', $key, $value, $exp);
+ }
+
+ // }}}
+ // {{{ run_command()
+
+ /**
+ * Passes through $cmd to the memcache server connected by $sock; returns
+ * output as an array (null array if no output)
+ *
+ * NOTE: due to a possible bug in how PHP reads while using fgets(), each
+ * line may not be terminated by a \r\n. More specifically, my testing
+ * has shown that, on FreeBSD at least, each line is terminated only
+ * with a \n. This is with the PHP flag auto_detect_line_endings set
+ * to falase (the default).
+ *
+ * @param resource $sock Socket to send command on
+ * @param string $cmd Command to run
+ *
+ * @return array Output array
+ * @access public
+ */
+ function run_command ($sock, $cmd)
+ {
+ if (!is_resource($sock))
+ return array();
+
+ if (!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
+ return array();
+
+ while (true)
+ {
+ $res = fgets($sock);
+ $ret[] = $res;
+ if (preg_match('/^END/', $res))
+ break;
+ if (strlen($res) == 0)
+ break;
+ }
+ return $ret;
+ }
+
+ // }}}
+ // {{{ set()
+
+ /**
+ * Unconditionally sets a key to a given value in the memcache. Returns true
+ * if set successfully.
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to set
+ * @param interger $exp (optional) Experiation time
+ *
+ * @return boolean TRUE on success
+ * @access public
+ */
+ function set ($key, $value, $exp=0)
+ {
+ return $this->_set('set', $key, $value, $exp);
+ }
+
+ // }}}
+ // {{{ set_compress_threshold()
+
+ /**
+ * Sets the compression threshold
+ *
+ * @param interger $thresh Threshold to compress if larger than
+ *
+ * @access public
+ */
+ function set_compress_threshold ($thresh)
+ {
+ $this->_compress_threshold = $thresh;
+ }
+
+ // }}}
+ // {{{ set_debug()
+
+ /**
+ * Sets the debug flag
+ *
+ * @param boolean $dbg TRUE for debugging, FALSE otherwise
+ *
+ * @access public
+ *
+ * @see memcahced::memcached
+ */
+ function set_debug ($dbg)
+ {
+ $this->_debug = $dbg;
+ }
+
+ // }}}
+ // {{{ set_servers()
+
+ /**
+ * Sets the server list to distribute key gets and puts between
+ *
+ * @param array $list Array of servers to connect to
+ *
+ * @access public
+ *
+ * @see memcached::memcached()
+ */
+ function set_servers ($list)
+ {
+ $this->_servers = $list;
+ $this->_active = count($list);
+ $this->_buckets = null;
+ $this->_bucketcount = 0;
+
+ $this->_single_sock = null;
+ if ($this->_active == 1)
+ $this->_single_sock = $this->_servers[0];
+ }
+
+ /**
+ * Sets the timeout for new connections
+ *
+ * @param integer $seconds Number of seconds
+ * @param integer $microseconds Number of microseconds
+ *
+ * @access public
+ */
+ function set_timeout ($seconds, $microseconds)
+ {
+ $this->_timeout_seconds = $seconds;
+ $this->_timeout_microseconds = $microseconds;
+ }
+
+ // }}}
+ // }}}
+ // {{{ private methods
+ // {{{ _close_sock()
+
+ /**
+ * Close the specified socket
+ *
+ * @param string $sock Socket to close
+ *
+ * @access private
+ */
+ function _close_sock ($sock)
+ {
+ $host = array_search($sock, $this->_cache_sock);
+ fclose($this->_cache_sock[$host]);
+ unset($this->_cache_sock[$host]);
+ }
+
+ // }}}
+ // {{{ _connect_sock()
+
+ /**
+ * Connects $sock to $host, timing out after $timeout
+ *
+ * @param interger $sock Socket to connect
+ * @param string $host Host:IP to connect to
+ * @param float $timeout (optional) Timeout value, defaults to 0.25s
+ *
+ * @return boolean
+ * @access private
+ */
+ function _connect_sock (&$sock, $host, $timeout = 0.25)
+ {
+ list ($ip, $port) = explode(":", $host);
+ if ($this->_persistant == 1)
+ {
+ $sock = @pfsockopen($ip, $port, $errno, $errstr, $timeout);
+ } else
+ {
+ $sock = @fsockopen($ip, $port, $errno, $errstr, $timeout);
+ }
+
+ if (!$sock) {
+ if ($this->_debug)
+ $this->_debugprint( "Error connecting to $host: $errstr\n" );
+ return false;
+ }
+
+ // Initialise timeout
+ stream_set_timeout($sock, $this->_timeout_seconds, $this->_timeout_microseconds);
+
+ return true;
+ }
+
+ // }}}
+ // {{{ _dead_sock()
+
+ /**
+ * Marks a host as dead until 30-40 seconds in the future
+ *
+ * @param string $sock Socket to mark as dead
+ *
+ * @access private
+ */
+ function _dead_sock ($sock)
+ {
+ $host = array_search($sock, $this->_cache_sock);
+ @list ($ip, $port) = explode(":", $host);
+ $this->_host_dead[$ip] = time() + 30 + intval(rand(0, 10));
+ $this->_host_dead[$host] = $this->_host_dead[$ip];
+ unset($this->_cache_sock[$host]);
+ }
+
+ // }}}
+ // {{{ get_sock()
+
+ /**
+ * get_sock
+ *
+ * @param string $key Key to retrieve value for;
+ *
+ * @return mixed resource on success, false on failure
+ * @access private
+ */
+ function get_sock ($key)
+ {
+ if (!$this->_active)
+ return false;
+
+ if ($this->_single_sock !== null) {
+ $this->_flush_read_buffer($this->_single_sock);
+ return $this->sock_to_host($this->_single_sock);
+ }
+
+ $hv = is_array($key) ? intval($key[0]) : $this->_hashfunc($key);
+
+ if ($this->_buckets === null)
+ {
+ foreach ($this->_servers as $v)
+ {
+ if (is_array($v))
+ {
+ for ($i=0; $i<$v[1]; $i++)
+ $bu[] = $v[0];
+ } else
+ {
+ $bu[] = $v;
+ }
+ }
+ $this->_buckets = $bu;
+ $this->_bucketcount = count($bu);
+ }
+
+ $realkey = is_array($key) ? $key[1] : $key;
+ for ($tries = 0; $tries<20; $tries++)
+ {
+ $host = $this->_buckets[$hv % $this->_bucketcount];
+ $sock = $this->sock_to_host($host);
+ if (is_resource($sock)) {
+ $this->_flush_read_buffer($sock);
+ return $sock;
+ }
+ $hv += $this->_hashfunc($tries . $realkey);
+ }
+
+ return false;
+ }
+
+ // }}}
+ // {{{ _hashfunc()
+
+ /**
+ * Creates a hash interger based on the $key
+ *
+ * @param string $key Key to hash
+ *
+ * @return interger Hash value
+ * @access private
+ */
+ function _hashfunc ($key)
+ {
+ # Hash function must on [0,0x7ffffff]
+ # We take the first 31 bits of the MD5 hash, which unlike the hash
+ # function used in a previous version of this client, works
+ return hexdec(substr(md5($key),0,8)) & 0x7fffffff;
+ }
+
+ // }}}
+ // {{{ _incrdecr()
+
+ /**
+ * Perform increment/decriment on $key
+ *
+ * @param string $cmd Command to perform
+ * @param string $key Key to perform it on
+ * @param interger $amt Amount to adjust
+ *
+ * @return interger New value of $key
+ * @access private
+ */
+ function _incrdecr ($cmd, $key, $amt=1)
+ {
+ if (!$this->_active)
+ return null;
+
+ $sock = $this->get_sock($key);
+ if (!is_resource($sock))
+ return null;
+
+ $key = is_array($key) ? $key[1] : $key;
+ @$this->stats[$cmd]++;
+ if (!$this->_safe_fwrite($sock, "$cmd $key $amt\r\n"))
+ return $this->_dead_sock($sock);
+
+ stream_set_timeout($sock, 1, 0);
+ $line = fgets($sock);
+ if (!preg_match('/^(\d+)/', $line, $match))
+ return null;
+ return $match[1];
+ }
+
+ // }}}
+ // {{{ _load_items()
+
+ /**
+ * Load items into $ret from $sock
+ *
+ * @param resource $sock Socket to read from
+ * @param array $ret Returned values
+ *
+ * @access private
+ */
+ function _load_items ($sock, &$ret)
+ {
+ while (1)
+ {
+ $decl = fgets($sock);
+ if ($decl == "END\r\n")
+ {
+ return true;
+ } elseif (preg_match('/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match))
+ {
+ list($rkey, $flags, $len) = array($match[1], $match[2], $match[3]);
+ $bneed = $len+2;
+ $offset = 0;
+
+ while ($bneed > 0)
+ {
+ $data = fread($sock, $bneed);
+ $n = strlen($data);
+ if ($n == 0)
+ break;
+ $offset += $n;
+ $bneed -= $n;
+ @$ret[$rkey] .= $data;
+ }
+
+ if ($offset != $len+2)
+ {
+ // Something is borked!
+ if ($this->_debug)
+ $this->_debugprint(sprintf("Something is borked! key %s expecting %d got %d length\n", $rkey, $len+2, $offset));
+
+ unset($ret[$rkey]);
+ $this->_close_sock($sock);
+ return false;
+ }
+
+ if ($this->_have_zlib && $flags & MEMCACHE_COMPRESSED)
+ $ret[$rkey] = gzuncompress($ret[$rkey]);
+
+ $ret[$rkey] = rtrim($ret[$rkey]);
+
+ if ($flags & MEMCACHE_SERIALIZED)
+ $ret[$rkey] = unserialize($ret[$rkey]);
+
+ } else
+ {
+ $this->_debugprint("Error parsing memcached response\n");
+ return 0;
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _set()
+
+ /**
+ * Performs the requested storage operation to the memcache server
+ *
+ * @param string $cmd Command to perform
+ * @param string $key Key to act on
+ * @param mixed $val What we need to store
+ * @param interger $exp When it should expire
+ *
+ * @return boolean
+ * @access private
+ */
+ function _set ($cmd, $key, $val, $exp)
+ {
+ if (!$this->_active)
+ return false;
+
+ $sock = $this->get_sock($key);
+ if (!is_resource($sock))
+ return false;
+
+ @$this->stats[$cmd]++;
+
+ $flags = 0;
+
+ if (!is_scalar($val))
+ {
+ $val = serialize($val);
+ $flags |= MEMCACHE_SERIALIZED;
+ if ($this->_debug)
+ $this->_debugprint(sprintf("client: serializing data as it is not scalar\n"));
+ }
+
+ $len = strlen($val);
+
+ if ($this->_have_zlib && $this->_compress_enable &&
+ $this->_compress_threshold && $len >= $this->_compress_threshold)
+ {
+ $c_val = gzcompress($val, 9);
+ $c_len = strlen($c_val);
+
+ if ($c_len < $len*(1 - COMPRESSION_SAVINGS))
+ {
+ if ($this->_debug)
+ $this->_debugprint(sprintf("client: compressing data; was %d bytes is now %d bytes\n", $len, $c_len));
+ $val = $c_val;
+ $len = $c_len;
+ $flags |= MEMCACHE_COMPRESSED;
+ }
+ }
+ if (!$this->_safe_fwrite($sock, "$cmd $key $flags $exp $len\r\n$val\r\n"))
+ return $this->_dead_sock($sock);
+
+ $line = trim(fgets($sock));
+
+ if ($this->_debug)
+ {
+ if ($flags & MEMCACHE_COMPRESSED)
+ $val = 'compressed data';
+ $this->_debugprint(sprintf("MemCache: %s %s => %s (%s)\n", $cmd, $key, $val, $line));
+ }
+ if ($line == "STORED")
+ return true;
+ return false;
+ }
+
+ // }}}
+ // {{{ sock_to_host()
+
+ /**
+ * Returns the socket for the host
+ *
+ * @param string $host Host:IP to get socket for
+ *
+ * @return mixed IO Stream or false
+ * @access private
+ */
+ function sock_to_host ($host)
+ {
+ if (isset($this->_cache_sock[$host]))
+ return $this->_cache_sock[$host];
+
+ $now = time();
+ list ($ip, $port) = explode (":", $host);
+ if (isset($this->_host_dead[$host]) && $this->_host_dead[$host] > $now ||
+ isset($this->_host_dead[$ip]) && $this->_host_dead[$ip] > $now)
+ return null;
+
+ if (!$this->_connect_sock($sock, $host))
+ return $this->_dead_sock($host);
+
+ // Do not buffer writes
+ stream_set_write_buffer($sock, 0);
+
+ $this->_cache_sock[$host] = $sock;
+
+ return $this->_cache_sock[$host];
+ }
+
+ function _debugprint($str){
+ print($str);
+ }
+
+ /**
+ * Write to a stream, timing out after the correct amount of time
+ *
+ * @return bool false on failure, true on success
+ */
+ /*
+ function _safe_fwrite($f, $buf, $len = false) {
+ stream_set_blocking($f, 0);
+
+ if ($len === false) {
+ wfDebug("Writing " . strlen( $buf ) . " bytes\n");
+ $bytesWritten = fwrite($f, $buf);
+ } else {
+ wfDebug("Writing $len bytes\n");
+ $bytesWritten = fwrite($f, $buf, $len);
+ }
+ $n = stream_select($r=NULL, $w = array($f), $e = NULL, 10, 0);
+ # $this->_timeout_seconds, $this->_timeout_microseconds);
+
+ wfDebug("stream_select returned $n\n");
+ stream_set_blocking($f, 1);
+ return $n == 1;
+ return $bytesWritten;
+ }*/
+
+ /**
+ * Original behaviour
+ */
+ function _safe_fwrite($f, $buf, $len = false) {
+ if ($len === false) {
+ $bytesWritten = fwrite($f, $buf);
+ } else {
+ $bytesWritten = fwrite($f, $buf, $len);
+ }
+ return $bytesWritten;
+ }
+
+ /**
+ * Flush the read buffer of a stream
+ */
+ function _flush_read_buffer($f) {
+ if (!is_resource($f)) {
+ return;
+ }
+ $n = stream_select($r=array($f), $w = NULL, $e = NULL, 0, 0);
+ while ($n == 1 && !feof($f)) {
+ fread($f, 1024);
+ $n = stream_select($r=array($f), $w = NULL, $e = NULL, 0, 0);
+ }
+ }
+
+ // }}}
+ // }}}
+ // }}}
+}
+
+// vim: sts=3 sw=3 et
+
+// }}}
+?>
diff --git a/includes/mime.info b/includes/mime.info
new file mode 100644
index 00000000..9b05f089
--- /dev/null
+++ b/includes/mime.info
@@ -0,0 +1,76 @@
+#Mime type info file.
+#the first mime type in each line is the "main" mime type,
+#the others are aliases for this type
+#the media type is given in upper case and square brackets,
+#like [BITMAP], and must indicate a media type as defined by
+#the MEDIATYPE_xxx constants in Defines.php
+
+
+image/gif [BITMAP]
+image/png [BITMAP]
+image/ief [BITMAP]
+image/jpeg [BITMAP]
+image/xbm [BITMAP]
+image/tiff [BITMAP]
+image/x-icon [BITMAP]
+image/x-rgb [BITMAP]
+image/x-portable-pixmap [BITMAP]
+image/x-portable-graymap image/x-portable-greymap [BITMAP]
+image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP]
+image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP]
+
+image/svg image/svg+xml application/svg+xml application/svg [DRAWING]
+application/postscript [DRAWING]
+application/x-latex [DRAWING]
+application/x-tex [DRAWING]
+
+
+audio/mp3 audio/mpeg3 audio/mpeg [AUDIO]
+audio/wav audio/x-wav audio/wave [AUDIO]
+audio/midi audio/mid [AUDIO]
+audio/basic [AUDIO]
+audio/x-aiff [AUDIO]
+audio/x-pn-realaudio [AUDIO]
+audio/x-realaudio [AUDIO]
+
+video/mpeg application/mpeg [VIDEO]
+video/ogg [VIDEO]
+video/x-sgi-video [VIDEO]
+
+application/ogg application/x-ogg audio/ogg audio/x-ogg video/ogg video/x-ogg [MULTIMEDIA]
+
+application/x-shockwave-flash [MULTIMEDIA]
+audio/x-pn-realaudio-plugin [MULTIMEDIA]
+model/iges [MULTIMEDIA]
+model/mesh [MULTIMEDIA]
+model/vrml [MULTIMEDIA]
+video/quicktime [MULTIMEDIA]
+video/x-msvideo [MULTIMEDIA]
+
+text/plain [TEXT]
+text/html application/xhtml+xml [TEXT]
+application/xml text/xml [TEXT]
+text [TEXT]
+
+application/zip application/x-zip [ARCHIVE]
+application/x-gzip [ARCHIVE]
+application/x-bzip [ARCHIVE]
+application/x-tar [ARCHIVE]
+application/x-stuffit [ARCHIVE]
+
+
+text/javascript application/x-javascript application/x-ecmascript text/ecmascript [EXECUTABLE]
+application/x-bash [EXECUTABLE]
+application/x-sh [EXECUTABLE]
+application/x-csh [EXECUTABLE]
+application/x-tcsh [EXECUTABLE]
+application/x-tcl [EXECUTABLE]
+application/x-perl [EXECUTABLE]
+application/x-python [EXECUTABLE]
+
+application/pdf application/acrobat [OFFICE]
+application/msword [OFFICE]
+application/vnd.ms-excel [OFFICE]
+application/vnd.ms-powerpoint [OFFICE]
+application/x-director [OFFICE]
+text/rtf [OFFICE]
diff --git a/includes/mime.types b/includes/mime.types
new file mode 100644
index 00000000..3a7fa39c
--- /dev/null
+++ b/includes/mime.types
@@ -0,0 +1,117 @@
+application/andrew-inset ez
+application/mac-binhex40 hqx
+application/mac-compactpro cpt
+application/mathml+xml mathml
+application/msword doc
+application/octet-stream bin dms lha lzh exe class so dll
+application/oda oda
+application/ogg ogg ogm
+application/pdf pdf
+application/postscript ai eps ps
+application/rdf+xml rdf
+application/smil smi smil
+application/srgs gram
+application/srgs+xml grxml
+application/vnd.mif mif
+application/vnd.ms-excel xls
+application/vnd.ms-powerpoint ppt
+application/vnd.wap.wbxml wbxml
+application/vnd.wap.wmlc wmlc
+application/vnd.wap.wmlscriptc wmlsc
+application/voicexml+xml vxml
+application/x-bcpio bcpio
+application/x-bzip gz bz2
+application/x-cdlink vcd
+application/x-chess-pgn pgn
+application/x-cpio cpio
+application/x-csh csh
+application/x-director dcr dir dxr
+application/x-dvi dvi
+application/x-futuresplash spl
+application/x-gtar gtar tar
+application/x-gzip gz
+application/x-hdf hdf
+application/x-jar jar
+application/x-javascript js
+application/x-koan skp skd skt skm
+application/x-latex latex
+application/x-netcdf nc cdf
+application/x-sh sh
+application/x-shar shar
+application/x-shockwave-flash swf
+application/x-stuffit sit
+application/x-sv4cpio sv4cpio
+application/x-sv4crc sv4crc
+application/x-tar tar
+application/x-tcl tcl
+application/x-tex tex
+application/x-texinfo texinfo texi
+application/x-troff t tr roff
+application/x-troff-man man
+application/x-troff-me me
+application/x-troff-ms ms
+application/x-ustar ustar
+application/x-wais-source src
+application/x-xpinstall xpi
+application/xhtml+xml xhtml xht
+application/xslt+xml xslt
+application/xml xml xsl
+application/xml-dtd dtd
+application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw
+audio/basic au snd
+audio/midi mid midi kar
+audio/mpeg mpga mp2 mp3
+audio/ogg ogg
+audio/x-aiff aif aiff aifc
+audio/x-mpegurl m3u
+audio/x-ogg ogg
+audio/x-pn-realaudio ram rm
+audio/x-pn-realaudio-plugin rpm
+audio/x-realaudio ra
+audio/x-wav wav
+chemical/x-pdb pdb
+chemical/x-xyz xyz
+image/bmp bmp
+image/cgm cgm
+image/gif gif
+image/ief ief
+image/jpeg jpeg jpg jpe
+image/png png
+image/svg+xml svg
+image/tiff tiff tif
+image/vnd.djvu djvu djv
+image/vnd.wap.wbmp wbmp
+image/x-cmu-raster ras
+image/x-icon ico
+image/x-portable-anymap pnm
+image/x-portable-bitmap pbm
+image/x-portable-graymap pgm
+image/x-portable-pixmap ppm
+image/x-rgb rgb
+image/x-photoshop psd
+image/x-xbitmap xbm
+image/x-xpixmap xpm
+image/x-xwindowdump xwd
+model/iges igs iges
+model/mesh msh mesh silo
+model/vrml wrl vrml
+text/calendar ics ifb
+text/css css
+text/html html htm
+text/plain txt
+text/richtext rtx
+text/rtf rtf
+text/sgml sgml sgm
+text/tab-separated-values tsv
+text/vnd.wap.wml wml
+text/vnd.wap.wmlscript wmls
+text/xml xml xsl xslt rss rdf
+text/x-setext etx
+video/mpeg mpeg mpg mpe
+video/ogg ogm ogg
+video/quicktime qt mov
+video/vnd.mpegurl mxu
+video/x-msvideo avi
+video/x-ogg ogm ogg
+video/x-sgi-movie movie
+x-conference/x-cooltalk ice \ No newline at end of file
diff --git a/includes/normal/CleanUpTest.php b/includes/normal/CleanUpTest.php
new file mode 100644
index 00000000..4e147cfd
--- /dev/null
+++ b/includes/normal/CleanUpTest.php
@@ -0,0 +1,423 @@
+<?php
+# 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
+
+/**
+ * Additional tests for UtfNormal::cleanUp() function, inclusion
+ * regression checks for known problems.
+ *
+ * Requires PHPUnit.
+ *
+ * @package UtfNormal
+ * @private
+ */
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+/** */
+if( isset( $_SERVER['argv'] ) && in_array( '--icu', $_SERVER['argv'] ) ) {
+ dl( 'php_utfnormal.so' );
+}
+
+#ini_set( 'memory_limit', '40M' );
+
+require_once( 'PHPUnit.php' );
+require_once( 'UtfNormal.php' );
+
+/**
+ * @package UtfNormal
+ */
+class CleanUpTest extends PHPUnit_TestCase {
+ /**
+ * @param $name String: FIXME
+ */
+ function CleanUpTest( $name ) {
+ $this->PHPUnit_TestCase( $name );
+ }
+
+ /** @todo document */
+ function setUp() {
+ }
+
+ /** @todo document */
+ function tearDown() {
+ }
+
+ /** @todo document */
+ function testAscii() {
+ $text = 'This is plain ASCII text.';
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ function testNull() {
+ $text = "a \x00 null";
+ $expect = "a \xef\xbf\xbd null";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testLatin() {
+ $text = "L'\xc3\xa9cole";
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ function testLatinNormal() {
+ $text = "L'e\xcc\x81cole";
+ $expect = "L'\xc3\xa9cole";
+ $this->assertEquals( $expect, UtfNormal::cleanUp( $text ) );
+ }
+
+ /**
+ * This test is *very* expensive!
+ * @todo document
+ */
+ function XtestAllChars() {
+ $rep = UTF8_REPLACEMENT;
+ global $utfCanonicalComp, $utfCanonicalDecomp;
+ for( $i = 0x0; $i < UNICODE_MAX; $i++ ) {
+ $char = codepointToUtf8( $i );
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%04X", $i );
+ if( $i % 0x1000 == 0 ) echo "U+$x\n";
+ if( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ($i > 0x001f && $i < UNICODE_SURROGATE_FIRST) ||
+ ($i > UNICODE_SURROGATE_LAST && $i < 0xfffe ) ||
+ ($i > 0xffff && $i <= UNICODE_MAX ) ) {
+ if( isset( $utfCanonicalComp[$char] ) || isset( $utfCanonicalDecomp[$char] ) ) {
+ $comp = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $comp ),
+ bin2hex( $clean ),
+ "U+$x should be decomposed" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "U+$x should be intact" );
+ }
+ } else {
+ $this->assertEquals( bin2hex( $rep ), bin2hex( $clean ), $x );
+ }
+ }
+ }
+
+ /** @todo document */
+ function testAllBytes() {
+ $this->doTestBytes( '', '' );
+ $this->doTestBytes( 'x', '' );
+ $this->doTestBytes( '', 'x' );
+ $this->doTestBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestBytes( $head, $tail ) {
+ for( $i = 0x0; $i < 256; $i++ ) {
+ $char = $head . chr( $i ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X", $i );
+ if( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ($i > 0x001f && $i < 0x80) ) {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "ASCII byte $x should be intact" );
+ if( $char != $clean ) return;
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden byte $x should be rejected" );
+ if( $norm != $clean ) return;
+ }
+ }
+ }
+
+ /** @todo document */
+ function testDoubleBytes() {
+ $this->doTestDoubleBytes( '', '' );
+ $this->doTestDoubleBytes( 'x', '' );
+ $this->doTestDoubleBytes( '', 'x' );
+ $this->doTestDoubleBytes( 'x', 'x' );
+ }
+
+ /**
+ * @todo document
+ */
+ function doTestDoubleBytes( $head, $tail ) {
+ for( $first = 0xc0; $first < 0x100; $first++ ) {
+ for( $second = 0x80; $second < 0x100; $second++ ) {
+ $char = $head . chr( $first ) . chr( $second ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X", $first, $second );
+ if( $first > 0xc1 &&
+ $first < 0xe0 &&
+ $second < 0xc0 ) {
+ $norm = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Pair $x should be intact" );
+ if( $norm != $clean ) return;
+ } elseif( $first > 0xfd || $second > 0xbf ) {
+ # fe and ff are not legal head bytes -- expect two replacement chars
+ $norm = $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if( $norm != $clean ) return;
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if( $norm != $clean ) return;
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ function testTripleBytes() {
+ $this->doTestTripleBytes( '', '' );
+ $this->doTestTripleBytes( 'x', '' );
+ $this->doTestTripleBytes( '', 'x' );
+ $this->doTestTripleBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestTripleBytes( $head, $tail ) {
+ for( $first = 0xc0; $first < 0x100; $first++ ) {
+ for( $second = 0x80; $second < 0x100; $second++ ) {
+ #for( $third = 0x80; $third < 0x100; $third++ ) {
+ for( $third = 0x80; $third < 0x81; $third++ ) {
+ $char = $head . chr( $first ) . chr( $second ) . chr( $third ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X,%02X", $first, $second, $third );
+ if( $first >= 0xe0 &&
+ $first < 0xf0 &&
+ $second < 0xc0 &&
+ $third < 0xc0 ) {
+ if( $first == 0xe0 && $second < 0xa0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Overlong triplet $x should be rejected" );
+ } elseif( $first == 0xed &&
+ ( chr( $first ) . chr( $second ) . chr( $third )) >= UTF8_SURROGATE_FIRST ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Surrogate triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $char ) ),
+ bin2hex( $clean ),
+ "Triplet $x should be intact" );
+ }
+ } elseif( $first > 0xc1 && $first < 0xe0 && $second < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $head . chr( $first ) . chr( $second ) ) . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Valid 2-byte $x + broken tail" );
+ } elseif( $second > 0xc1 && $second < 0xe0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UtfNormal::NFC( chr( $second ) . chr( $third ) . $tail ) ),
+ bin2hex( $clean ),
+ "Broken head + valid 2-byte $x" );
+ } elseif( ( $first > 0xfd || $second > 0xfd ) &&
+ ( ( $second > 0xbf && $third > 0xbf ) ||
+ ( $second < 0xc0 && $third < 0xc0 ) ||
+ ( $second > 0xfd ) ||
+ ( $third > 0xfd ) ) ) {
+ # fe and ff are not legal head bytes -- expect three replacement chars
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } elseif( $first > 0xc2 && $second < 0xc0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ }
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ function testChunkRegression() {
+ # Check for regression against a chunking bug
+ $text = "\x46\x55\xb8" .
+ "\xdc\x96" .
+ "\xee" .
+ "\xe7" .
+ "\x44" .
+ "\xaa" .
+ "\x2f\x25";
+ $expect = "\x46\x55\xef\xbf\xbd" .
+ "\xdc\x96" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x44" .
+ "\xef\xbf\xbd" .
+ "\x2f\x25";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testInterposeRegression() {
+ $text = "\x4e\x30" .
+ "\xb1" . # bad tail
+ "\x3a" .
+ "\x92" . # bad tail
+ "\x62\x3a" .
+ "\x84" . # bad tail
+ "\x43" .
+ "\xc6" . # bad head
+ "\x3f" .
+ "\x92" . # bad tail
+ "\xad" . # bad tail
+ "\x7d" .
+ "\xd9\x95";
+
+ $expect = "\x4e\x30" .
+ "\xef\xbf\xbd" .
+ "\x3a" .
+ "\xef\xbf\xbd" .
+ "\x62\x3a" .
+ "\xef\xbf\xbd" .
+ "\x43" .
+ "\xef\xbf\xbd" .
+ "\x3f" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x7d" .
+ "\xd9\x95";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testOverlongRegression() {
+ $text = "\x67" .
+ "\x1a" . # forbidden ascii
+ "\xea" . # bad head
+ "\xc1\xa6" . # overlong sequence
+ "\xad" . # bad tail
+ "\x1c" . # forbidden ascii
+ "\xb0" . # bad tail
+ "\x3c" .
+ "\x9e"; # bad tail
+ $expect = "\x67" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x3c" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testSurrogateRegression() {
+ $text = "\xed\xb4\x96" . # surrogate 0xDD16
+ "\x83" . # bad tail
+ "\xb4" . # bad tail
+ "\xac"; # bad head
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testBomRegression() {
+ $text = "\xef\xbf\xbe" . # U+FFFE, illegal char
+ "\xb2" . # bad tail
+ "\xef" . # bad head
+ "\x59";
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x59";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testForbiddenRegression() {
+ $text = "\xef\xbf\xbf"; # U+FFFF, illegal char
+ $expect = "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testHangulRegression() {
+ $text = "\xed\x9c\xaf" . # Hangul char
+ "\xe1\x87\x81"; # followed by another final jamo
+ $expect = $text; # Should *not* change.
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+}
+
+
+$suite =& new PHPUnit_TestSuite( 'CleanUpTest' );
+$result = PHPUnit::run( $suite );
+echo $result->toString();
+
+if( !$result->wasSuccessful() ) {
+ exit( -1 );
+}
+exit( 0 );
+?>
diff --git a/includes/normal/Makefile b/includes/normal/Makefile
new file mode 100644
index 00000000..fcdf2380
--- /dev/null
+++ b/includes/normal/Makefile
@@ -0,0 +1,72 @@
+.PHONY : all test testutf8 testclean icutest bench icubench clean distclean
+
+FETCH=wget
+#FETCH=fetch
+BASE=http://www.unicode.org/Public/UNIDATA
+PHP=php
+#PHP=php-cli
+
+all : UtfNormalData.inc
+
+UtfNormalData.inc : UtfNormalGenerate.php UtfNormalUtil.php UnicodeData.txt CompositionExclusions.txt NormalizationCorrections.txt DerivedNormalizationProps.txt
+ $(PHP) UtfNormalGenerate.php
+
+test : testutf8 testclean UtfNormalTest.php UtfNormalData.inc NormalizationTest.txt
+ $(PHP) UtfNormalTest.php
+
+testutf8 : Utf8Test.php UTF-8-test.txt
+ $(PHP) Utf8Test.php
+
+testclean : CleanUpTest.php
+ $(PHP) CleanUpTest.php
+
+bench : UtfNormalData.inc testdata/washington.txt testdata/berlin.txt testdata/tokyo.txt testdata/sociology.txt testdata/bulgakov.txt
+ $(PHP) UtfNormalBench.php
+
+icutest : UtfNormalData.inc NormalizationTest.txt
+ $(PHP) Utf8Test.php --icu
+ $(PHP) CleanUpTest.php --icu
+ $(PHP) UtfNormalTest.php --icu
+
+icubench : UtfNormalData.inc testdata/washington.txt testdata/berlin.txt testdata/tokyo.txt testdata/sociology.txt testdata/bulgakov.txt
+ $(PHP) UtfNormalBench.php --icu
+
+clean :
+ rm -f UtfNormalData.inc
+
+distclean : clean
+ rm -f CompositionExclusions.txt NormalizationTest.txt NormalizationCorrections.txt UnicodeData.txt DerivedNormalizationProps.txt
+
+# The Unicode data files...
+CompositionExclusions.txt :
+ $(FETCH) $(BASE)/CompositionExclusions.txt
+
+NormalizationTest.txt :
+ $(FETCH) $(BASE)/NormalizationTest.txt
+
+NormalizationCorrections.txt :
+ $(FETCH) $(BASE)/NormalizationCorrections.txt
+
+DerivedNormalizationProps.txt :
+ $(FETCH) $(BASE)/DerivedNormalizationProps.txt
+
+UnicodeData.txt :
+ $(FETCH) $(BASE)/UnicodeData.txt
+
+UTF-8-test.txt :
+ $(FETCH) http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+
+testdata/berlin.txt :
+ mkdir -p testdata && wget -U MediaWiki/test -O testdata/berlin.txt "http://de.wikipedia.org/w/wiki.phtml?title=Berlin&oldid=2775712&action=raw"
+
+testdata/washington.txt :
+ mkdir -p testdata && wget -U MediaWiki/test -O testdata/washington.txt "http://en.wikipedia.org/w/wiki.phtml?title=Washington%2C_DC&oldid=6370218&action=raw"
+
+testdata/tokyo.txt :
+ mkdir -p testdata && wget -U MediaWiki/test -O testdata/tokyo.txt "http://ja.wikipedia.org/w/wiki.phtml?title=%E6%9D%B1%E4%BA%AC%E9%83%BD&oldid=940880&action=raw"
+
+testdata/sociology.txt :
+ mkdir -p testdata && wget -U MediaWiki/test -O testdata/sociology.txt "http://ko.wikipedia.org/w/wiki.phtml?title=%EC%82%AC%ED%9A%8C%ED%95%99&oldid=16409&action=raw"
+
+testdata/bulgakov.txt :
+ mkdir -p testdata && wget -U MediaWiki/test -O testdata/bulgakov.txt "http://ru.wikipedia.org/w/wiki.phtml?title=%D0%91%D1%83%D0%BB%D0%B3%D0%B0%D0%BA%D0%BE%D0%B2%2C_%D0%A1%D0%B5%D1%80%D0%B3%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87&oldid=17704&action=raw"
diff --git a/includes/normal/README b/includes/normal/README
new file mode 100644
index 00000000..f8207a1b
--- /dev/null
+++ b/includes/normal/README
@@ -0,0 +1,55 @@
+This directory contains some Unicode normalization routines. These routines
+are meant to be reusable in other projects, so I'm not tying them to the
+MediaWiki utility functions.
+
+The main function to care about is UtfNormal::toNFC(); this will convert
+a given UTF-8 string to Normalization Form C if it's not already such.
+The function assumes that the input string is already valid UTF-8; if there
+are corrupt characters this may produce erroneous results.
+
+To also check for illegal characters, use UtfNormal::cleanUp(). This will
+strip illegal UTF-8 sequences and characters that are illegal in XML, and
+if necessary convert to normalization form C.
+
+Performance is kind of stinky in absolute terms, though it should be speedy
+on pure ASCII text. ;) On text that can be determined quickly to already be
+in NFC it's not too awful but it can quickly get uncomfortably slow,
+particularly for Korean text (the hangul decomposition/composition code is
+extra slow).
+
+
+== Regenerating data tables ==
+
+UtfNormalData.inc and UtfNormalDataK.inc are generated from the Unicode
+Character Database by the script UtfNormalGenerate.php. On a *nix system
+'make' should fetch the necessary files and regenerate it if the scripts
+have been changed or you remove it.
+
+
+== Testing ==
+
+'make test' will run the conformance test (UtfNormalTest.php), fetching the
+data from from the net if necessary. If it reports failure, something is
+going wrong!
+
+
+== Benchmarks ==
+
+Run 'make bench' to download some sample texts from Wikipedia and run some
+cheap benchmarks of some of the functions. Take all numbers with large
+grains of salt.
+
+
+== PHP module extension ==
+
+There's an experimental PHP extension module which wraps the ICU library's
+normalization functions. This is *MUCH* faster than doing this work in pure
+PHP code. This is in the 'normal' directory in MediaWiki's CVS extensions
+module. It is known to work with PHP 4.3.8 and 5.0.2 on Linux/x86 but hasn't
+been thoroughly tested on other configurations.
+
+If the php_normal.so module is loaded in php.ini, the normalization functions
+will automatically use it. If you can't (or don't want to) load it in php.ini,
+you may be able to load it using the dl() function before include()ing or
+require()ing UtfNormal.php, and it will be picked up.
+
diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php
new file mode 100644
index 00000000..3a5a407b
--- /dev/null
+++ b/includes/normal/RandomTest.php
@@ -0,0 +1,107 @@
+<?php
+# 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
+
+/**
+ * Test feeds random 16-byte strings to both the pure PHP and ICU-based
+ * UtfNormal::cleanUp() code paths, and checks to see if there's a
+ * difference. Will run forever until it finds one or you kill it.
+ *
+ * @package UtfNormal
+ * @access private
+ */
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+/** */
+require_once( 'UtfNormal.php' );
+require_once( '../DifferenceEngine.php' );
+
+dl('php_utfnormal.so' );
+
+# mt_srand( 99999 );
+
+function randomString( $length, $nullOk, $ascii = false ) {
+ $out = '';
+ for( $i = 0; $i < $length; $i++ )
+ $out .= chr( mt_rand( $nullOk ? 0 : 1, $ascii ? 127 : 255 ) );
+ return $out;
+}
+
+/* Duplicate of the cleanUp() path for ICU usage */
+function donorm( $str ) {
+ # We exclude a few chars that ICU would not.
+ $str = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', UTF8_REPLACEMENT, $str );
+ $str = str_replace( UTF8_FFFE, UTF8_REPLACEMENT, $str );
+ $str = str_replace( UTF8_FFFF, UTF8_REPLACEMENT, $str );
+
+ # UnicodeString constructor fails if the string ends with a head byte.
+ # Add a junk char at the end, we'll strip it off
+ return rtrim( utf8_normalize( $str . "\x01", UNORM_NFC ), "\x01" );
+}
+
+function wfMsg($x) {
+ return $x;
+}
+
+function showDiffs( $a, $b ) {
+ $ota = explode( "\n", str_replace( "\r\n", "\n", $a ) );
+ $nta = explode( "\n", str_replace( "\r\n", "\n", $b ) );
+
+ $diffs =& new Diff( $ota, $nta );
+ $formatter =& new TableDiffFormatter();
+ $funky = $formatter->format( $diffs );
+ preg_match_all( '/<span class="diffchange">(.*?)<\/span>/', $funky, $matches );
+ foreach( $matches[1] as $bit ) {
+ $hex = bin2hex( $bit );
+ echo "\t$hex\n";
+ }
+}
+
+$size = 16;
+$n = 0;
+while( true ) {
+ $n++;
+ echo "$n\n";
+
+ $str = randomString( $size, true);
+ $clean = UtfNormal::cleanUp( $str );
+ $norm = donorm( $str );
+
+ echo strlen( $clean ) . ", " . strlen( $norm );
+ if( $clean == $norm ) {
+ echo " (match)\n";
+ } else {
+ echo " (FAIL)\n";
+ echo "\traw: " . bin2hex( $str ) . "\n" .
+ "\tphp: " . bin2hex( $clean ) . "\n" .
+ "\ticu: " . bin2hex( $norm ) . "\n";
+ echo "\n\tdiffs:\n";
+ showDiffs( $clean, $norm );
+ die();
+ }
+
+
+ $str = '';
+ $clean = '';
+ $norm = '';
+}
+
+?> \ No newline at end of file
diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php
new file mode 100644
index 00000000..71069598
--- /dev/null
+++ b/includes/normal/Utf8Test.php
@@ -0,0 +1,151 @@
+<?php
+# 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
+
+/**
+ * Runs the UTF-8 decoder test at:
+ * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ *
+ * @package UtfNormal
+ * @access private
+ */
+
+/** */
+require_once 'UtfNormalUtil.php';
+require_once 'UtfNormal.php';
+mb_internal_encoding( "utf-8" );
+
+#$verbose = true;
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+$in = fopen( "UTF-8-test.txt", "rt" );
+if( !$in ) {
+ print "Couldn't open UTF-8-test.txt -- can't run tests.\n";
+ print "If necessary, manually download this file. It can be obtained at\n";
+ print "http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt";
+ exit(-1);
+}
+
+$columns = 0;
+while( false !== ( $line = fgets( $in ) ) ) {
+ if( preg_match( '/^(Here come the tests:\s*)\|$/', $line, $matches ) ) {
+ $columns = strpos( $line, '|' );
+ break;
+ }
+}
+
+if( !$columns ) {
+ print "Something seems to be wrong; couldn't extract line length.\n";
+ print "Check that UTF-8-test.txt was downloaded correctly from\n";
+ print "http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt";
+ exit(-1);
+}
+
+# print "$columns\n";
+
+$ignore = array(
+ # These two lines actually seem to be corrupt
+ '2.1.1', '2.2.1' );
+
+$exceptions = array(
+ # Tests that should mark invalid characters due to using long
+ # sequences beyond what is now considered legal.
+ '2.1.5', '2.1.6', '2.2.4', '2.2.5', '2.2.6', '2.3.5',
+
+ # Literal 0xffff, which is illegal
+ '2.2.3' );
+
+$longTests = array(
+ # These tests span multiple lines
+ '3.1.9', '3.2.1', '3.2.2', '3.2.3', '3.2.4', '3.2.5',
+ '3.4' );
+
+# These tests are not in proper subsections
+$sectionTests = array( '3.4' );
+
+$section = NULL;
+$test = '';
+$failed = 0;
+$success = 0;
+$total = 0;
+while( false !== ( $line = fgets( $in ) ) ) {
+ if( preg_match( '/^(\d+)\s+(.*?)\s*\|/', $line, $matches ) ) {
+ $section = $matches[1];
+ print $line;
+ continue;
+ }
+ if( preg_match( '/^(\d+\.\d+\.\d+)\s*/', $line, $matches ) ) {
+ $test = $matches[1];
+
+ if( in_array( $test, $ignore ) ) {
+ continue;
+ }
+ if( in_array( $test, $longTests ) ) {
+ $line = fgets( $in );
+ for( $line = fgets( $in ); !preg_match( '/^\s+\|/', $line ); $line = fgets( $in ) ) {
+ testLine( $test, $line, $total, $success, $failed );
+ }
+ } else {
+ testLine( $test, $line, $total, $success, $failed );
+ }
+ }
+}
+
+if( $failed ) {
+ echo "\nFailed $failed tests.\n";
+ echo "UTF-8 DECODER TEST FAILED\n";
+ exit (-1);
+}
+
+echo "UTF-8 DECODER TEST SUCCESS!\n";
+exit (0);
+
+
+function testLine( $test, $line, &$total, &$success, &$failed ) {
+ $stripped = $line;
+ UtfNormal::quickisNFCVerify( $stripped );
+
+ $same = ( $line == $stripped );
+ $len = mb_strlen( substr( $stripped, 0, strpos( $stripped, '|' ) ) );
+ if( $len == 0 ) {
+ $len = strlen( substr( $stripped, 0, strpos( $stripped, '|' ) ) );
+ }
+
+ global $columns;
+ $ok = $same ^ ($test >= 3 );
+
+ global $exceptions;
+ $ok ^= in_array( $test, $exceptions );
+
+ $ok &= ($columns == $len);
+
+ $total++;
+ if( $ok ) {
+ $success++;
+ } else {
+ $failed++;
+ }
+ global $verbose;
+ if( $verbose || !$ok ) {
+ print str_replace( "\n", "$len\n", $stripped );
+ }
+}
+
+?>
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
new file mode 100644
index 00000000..d8641993
--- /dev/null
+++ b/includes/normal/UtfNormal.php
@@ -0,0 +1,792 @@
+<?php
+# 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
+
+/**
+ * Unicode normalization routines for working with UTF-8 strings.
+ * Currently assumes that input strings are valid UTF-8!
+ *
+ * Not as fast as I'd like, but should be usable for most purposes.
+ * UtfNormal::toNFC() will bail early if given ASCII text or text
+ * it can quickly deterimine is already normalized.
+ *
+ * All functions can be called static.
+ *
+ * See description of forms at http://www.unicode.org/reports/tr15/
+ *
+ * @package UtfNormal
+ */
+
+/** */
+require_once 'UtfNormalUtil.php';
+
+global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp;
+$utfCombiningClass = NULL;
+$utfCanonicalComp = NULL;
+$utfCanonicalDecomp = NULL;
+
+# Load compatibility decompositions on demand if they are needed.
+global $utfCompatibilityDecomp;
+$utfCompatibilityDecomp = NULL;
+
+define( 'UNICODE_HANGUL_FIRST', 0xac00 );
+define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
+
+define( 'UNICODE_HANGUL_LBASE', 0x1100 );
+define( 'UNICODE_HANGUL_VBASE', 0x1161 );
+define( 'UNICODE_HANGUL_TBASE', 0x11a7 );
+
+define( 'UNICODE_HANGUL_LCOUNT', 19 );
+define( 'UNICODE_HANGUL_VCOUNT', 21 );
+define( 'UNICODE_HANGUL_TCOUNT', 28 );
+define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT );
+
+define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 );
+define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 );
+define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 );
+
+define( 'UNICODE_SURROGATE_FIRST', 0xd800 );
+define( 'UNICODE_SURROGATE_LAST', 0xdfff );
+define( 'UNICODE_MAX', 0x10ffff );
+define( 'UNICODE_REPLACEMENT', 0xfffd );
+
+
+define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ );
+define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ );
+
+define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ );
+define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ );
+define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ );
+
+define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ );
+define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ );
+define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ );
+
+define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ );
+define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ );
+define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ );
+define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ );
+#define( 'UTF8_REPLACEMENT', '!' );
+
+define( 'UTF8_OVERLONG_A', "\xc1\xbf" );
+define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" );
+define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" );
+
+# These two ranges are illegal
+define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ );
+define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ );
+define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ );
+define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
+
+define( 'UTF8_HEAD', false );
+define( 'UTF8_TAIL', true );
+
+
+/**
+ * For using the ICU wrapper
+ */
+define( 'UNORM_NONE', 1 );
+define( 'UNORM_NFD', 2 );
+define( 'UNORM_NFKD', 3 );
+define( 'UNORM_NFC', 4 );
+define( 'UNORM_DEFAULT', UNORM_NFC );
+define( 'UNORM_NFKC', 5 );
+define( 'UNORM_FCD', 6 );
+
+define( 'NORMALIZE_ICU', function_exists( 'utf8_normalize' ) );
+
+/**
+ *
+ * @package MediaWiki
+ */
+class UtfNormal {
+ /**
+ * The ultimate convenience function! Clean up invalid UTF-8 sequences,
+ * and convert to normal form C, canonical composition.
+ *
+ * Fast return for pure ASCII strings; some lesser optimizations for
+ * strings containing only known-good characters. Not as fast as toNFC().
+ *
+ * @param string $string a UTF-8 string
+ * @return string a clean, shiny, normalized UTF-8 string
+ */
+ function cleanUp( $string ) {
+ if( NORMALIZE_ICU ) {
+ # We exclude a few chars that ICU would not.
+ $string = preg_replace(
+ '/[\x00-\x08\x0b\x0c\x0e-\x1f]/',
+ UTF8_REPLACEMENT,
+ $string );
+ $string = str_replace( UTF8_FFFE, UTF8_REPLACEMENT, $string );
+ $string = str_replace( UTF8_FFFF, UTF8_REPLACEMENT, $string );
+
+ # UnicodeString constructor fails if the string ends with a
+ # head byte. Add a junk char at the end, we'll strip it off.
+ return rtrim( utf8_normalize( $string . "\x01", UNORM_NFC ), "\x01" );
+ } elseif( UtfNormal::quickIsNFCVerify( $string ) ) {
+ # Side effect -- $string has had UTF-8 errors cleaned up.
+ return $string;
+ } else {
+ return UtfNormal::NFC( $string );
+ }
+ }
+
+ /**
+ * Convert a UTF-8 string to normal form C, canonical composition.
+ * Fast return for pure ASCII strings; some lesser optimizations for
+ * strings containing only known-good characters.
+ *
+ * @param string $string a valid UTF-8 string. Input is not validated.
+ * @return string a UTF-8 string in normal form C
+ */
+ function toNFC( $string ) {
+ if( NORMALIZE_ICU )
+ return utf8_normalize( $string, UNORM_NFC );
+ elseif( UtfNormal::quickIsNFC( $string ) )
+ return $string;
+ else
+ return UtfNormal::NFC( $string );
+ }
+
+ /**
+ * Convert a UTF-8 string to normal form D, canonical decomposition.
+ * Fast return for pure ASCII strings.
+ *
+ * @param string $string a valid UTF-8 string. Input is not validated.
+ * @return string a UTF-8 string in normal form D
+ */
+ function toNFD( $string ) {
+ if( NORMALIZE_ICU )
+ return utf8_normalize( $string, UNORM_NFD );
+ elseif( preg_match( '/[\x80-\xff]/', $string ) )
+ return UtfNormal::NFD( $string );
+ else
+ return $string;
+ }
+
+ /**
+ * Convert a UTF-8 string to normal form KC, compatibility composition.
+ * This may cause irreversible information loss, use judiciously.
+ * Fast return for pure ASCII strings.
+ *
+ * @param string $string a valid UTF-8 string. Input is not validated.
+ * @return string a UTF-8 string in normal form KC
+ */
+ function toNFKC( $string ) {
+ if( NORMALIZE_ICU )
+ return utf8_normalize( $string, UNORM_NFKC );
+ elseif( preg_match( '/[\x80-\xff]/', $string ) )
+ return UtfNormal::NFKC( $string );
+ else
+ return $string;
+ }
+
+ /**
+ * Convert a UTF-8 string to normal form KD, compatibility decomposition.
+ * This may cause irreversible information loss, use judiciously.
+ * Fast return for pure ASCII strings.
+ *
+ * @param string $string a valid UTF-8 string. Input is not validated.
+ * @return string a UTF-8 string in normal form KD
+ */
+ function toNFKD( $string ) {
+ if( NORMALIZE_ICU )
+ return utf8_normalize( $string, UNORM_NFKD );
+ elseif( preg_match( '/[\x80-\xff]/', $string ) )
+ return UtfNormal::NFKD( $string );
+ else
+ return $string;
+ }
+
+ /**
+ * Load the basic composition data if necessary
+ * @private
+ */
+ function loadData() {
+ # fixme : are $utfCanonicalComp, $utfCanonicalDecomp really used?
+ global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp;
+ if( !isset( $utfCombiningClass ) ) {
+ require_once( 'UtfNormalData.inc' );
+ }
+ }
+
+ /**
+ * Returns true if the string is _definitely_ in NFC.
+ * Returns false if not or uncertain.
+ * @param string $string a valid UTF-8 string. Input is not validated.
+ * @return bool
+ */
+ function quickIsNFC( $string ) {
+ # ASCII is always valid NFC!
+ # If it's pure ASCII, let it through.
+ if( !preg_match( '/[\x80-\xff]/', $string ) ) return true;
+
+ UtfNormal::loadData();
+ global $utfCheckNFC, $utfCombiningClass;
+ $len = strlen( $string );
+ for( $i = 0; $i < $len; $i++ ) {
+ $c = $string{$i};
+ $n = ord( $c );
+ if( $n < 0x80 ) {
+ continue;
+ } elseif( $n >= 0xf0 ) {
+ $c = substr( $string, $i, 4 );
+ $i += 3;
+ } elseif( $n >= 0xe0 ) {
+ $c = substr( $string, $i, 3 );
+ $i += 2;
+ } elseif( $n >= 0xc0 ) {
+ $c = substr( $string, $i, 2 );
+ $i++;
+ }
+ if( isset( $utfCheckNFC[$c] ) ) {
+ # If it's NO or MAYBE, bail and do the slow check.
+ return false;
+ }
+ if( isset( $utfCombiningClass[$c] ) ) {
+ # Combining character? We might have to do sorting, at least.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the string is _definitely_ in NFC.
+ * Returns false if not or uncertain.
+ * @param string $string a UTF-8 string, altered on output to be valid UTF-8 safe for XML.
+ */
+ function quickIsNFCVerify( &$string ) {
+ # Screen out some characters that eg won't be allowed in XML
+ $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', UTF8_REPLACEMENT, $string );
+
+ # ASCII is always valid NFC!
+ # If we're only ever given plain ASCII, we can avoid the overhead
+ # of initializing the decomposition tables by skipping out early.
+ if( !preg_match( '/[\x80-\xff]/', $string ) ) return true;
+
+ static $checkit = null, $tailBytes = null, $utfCheckOrCombining = null;
+ if( !isset( $checkit ) ) {
+ # Load/build some scary lookup tables...
+ UtfNormal::loadData();
+ global $utfCheckNFC, $utfCombiningClass;
+
+ $utfCheckOrCombining = array_merge( $utfCheckNFC, $utfCombiningClass );
+
+ # Head bytes for sequences which we should do further validity checks
+ $checkit = array_flip( array_map( 'chr',
+ array( 0xc0, 0xc1, 0xe0, 0xed, 0xef,
+ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
+ 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff ) ) );
+
+ # Each UTF-8 head byte is followed by a certain
+ # number of tail bytes.
+ $tailBytes = array();
+ for( $n = 0; $n < 256; $n++ ) {
+ if( $n < 0xc0 ) {
+ $remaining = 0;
+ } elseif( $n < 0xe0 ) {
+ $remaining = 1;
+ } elseif( $n < 0xf0 ) {
+ $remaining = 2;
+ } elseif( $n < 0xf8 ) {
+ $remaining = 3;
+ } elseif( $n < 0xfc ) {
+ $remaining = 4;
+ } elseif( $n < 0xfe ) {
+ $remaining = 5;
+ } else {
+ $remaining = 0;
+ }
+ $tailBytes[chr($n)] = $remaining;
+ }
+ }
+
+ # Chop the text into pure-ASCII and non-ASCII areas;
+ # large ASCII parts can be handled much more quickly.
+ # Don't chop up Unicode areas for punctuation, though,
+ # that wastes energy.
+ preg_match_all(
+ '/([\x00-\x7f]+|[\x80-\xff][\x00-\x40\x5b-\x5f\x7b-\xff]*)/',
+ $string, $matches );
+
+ $looksNormal = true;
+ $base = 0;
+ $replace = array();
+ foreach( $matches[1] as $str ) {
+ $chunk = strlen( $str );
+
+ if( $str{0} < "\x80" ) {
+ # ASCII chunk: guaranteed to be valid UTF-8
+ # and in normal form C, so skip over it.
+ $base += $chunk;
+ continue;
+ }
+
+ # We'll have to examine the chunk byte by byte to ensure
+ # that it consists of valid UTF-8 sequences, and to see
+ # if any of them might not be normalized.
+ #
+ # Since PHP is not the fastest language on earth, some of
+ # this code is a little ugly with inner loop optimizations.
+
+ $head = '';
+ $len = $chunk + 1; # Counting down is faster. I'm *so* sorry.
+
+ for( $i = -1; --$len; ) {
+ if( $remaining = $tailBytes[$c = $str{++$i}] ) {
+ # UTF-8 head byte!
+ $sequence = $head = $c;
+ do {
+ # Look for the defined number of tail bytes...
+ if( --$len && ( $c = $str{++$i} ) >= "\x80" && $c < "\xc0" ) {
+ # Legal tail bytes are nice.
+ $sequence .= $c;
+ } else {
+ if( 0 == $len ) {
+ # Premature end of string!
+ # Drop a replacement character into output to
+ # represent the invalid UTF-8 sequence.
+ $replace[] = array( UTF8_REPLACEMENT,
+ $base + $i + 1 - strlen( $sequence ),
+ strlen( $sequence ) );
+ break 2;
+ } else {
+ # Illegal tail byte; abandon the sequence.
+ $replace[] = array( UTF8_REPLACEMENT,
+ $base + $i - strlen( $sequence ),
+ strlen( $sequence ) );
+ # Back up and reprocess this byte; it may itself
+ # be a legal ASCII or UTF-8 sequence head.
+ --$i;
+ ++$len;
+ continue 2;
+ }
+ }
+ } while( --$remaining );
+
+ if( isset( $checkit[$head] ) ) {
+ # Do some more detailed validity checks, for
+ # invalid characters and illegal sequences.
+ if( $head == "\xed" ) {
+ # 0xed is relatively frequent in Korean, which
+ # abuts the surrogate area, so we're doing
+ # this check separately to speed things up.
+
+ if( $sequence >= UTF8_SURROGATE_FIRST ) {
+ # Surrogates are legal only in UTF-16 code.
+ # They are totally forbidden here in UTF-8
+ # utopia.
+ $replace[] = array( UTF8_REPLACEMENT,
+ $base + $i + 1 - strlen( $sequence ),
+ strlen( $sequence ) );
+ $head = '';
+ continue;
+ }
+ } else {
+ # Slower, but rarer checks...
+ $n = ord( $head );
+ if(
+ # "Overlong sequences" are those that are syntactically
+ # correct but use more UTF-8 bytes than are necessary to
+ # encode a character. Naïve string comparisons can be
+ # tricked into failing to see a match for an ASCII
+ # character, for instance, which can be a security hole
+ # if blacklist checks are being used.
+ ($n < 0xc2 && $sequence <= UTF8_OVERLONG_A)
+ || ($n == 0xe0 && $sequence <= UTF8_OVERLONG_B)
+ || ($n == 0xf0 && $sequence <= UTF8_OVERLONG_C)
+
+ # U+FFFE and U+FFFF are explicitly forbidden in Unicode.
+ || ($n == 0xef &&
+ ($sequence == UTF8_FFFE)
+ || ($sequence == UTF8_FFFF) )
+
+ # Unicode has been limited to 21 bits; longer
+ # sequences are not allowed.
+ || ($n >= 0xf0 && $sequence > UTF8_MAX) ) {
+
+ $replace[] = array( UTF8_REPLACEMENT,
+ $base + $i + 1 - strlen( $sequence ),
+ strlen( $sequence ) );
+ $head = '';
+ continue;
+ }
+ }
+ }
+
+ if( isset( $utfCheckOrCombining[$sequence] ) ) {
+ # If it's NO or MAYBE, we'll have to rip
+ # the string apart and put it back together.
+ # That's going to be mighty slow.
+ $looksNormal = false;
+ }
+
+ # The sequence is legal!
+ $head = '';
+ } elseif( $c < "\x80" ) {
+ # ASCII byte.
+ $head = '';
+ } elseif( $c < "\xc0" ) {
+ # Illegal tail bytes
+ if( $head == '' ) {
+ # Out of the blue!
+ $replace[] = array( UTF8_REPLACEMENT, $base + $i, 1 );
+ } else {
+ # Don't add if we're continuing a broken sequence;
+ # we already put a replacement character when we looked
+ # at the broken sequence.
+ $replace[] = array( '', $base + $i, 1 );
+ }
+ } else {
+ # Miscellaneous freaks.
+ $replace[] = array( UTF8_REPLACEMENT, $base + $i, 1 );
+ $head = '';
+ }
+ }
+ $base += $chunk;
+ }
+ if( count( $replace ) ) {
+ # There were illegal UTF-8 sequences we need to fix up.
+ $out = '';
+ $last = 0;
+ foreach( $replace as $rep ) {
+ list( $replacement, $start, $length ) = $rep;
+ if( $last < $start ) {
+ $out .= substr( $string, $last, $start - $last );
+ }
+ $out .= $replacement;
+ $last = $start + $length;
+ }
+ if( $last < strlen( $string ) ) {
+ $out .= substr( $string, $last );
+ }
+ $string = $out;
+ }
+ return $looksNormal;
+ }
+
+ # These take a string and run the normalization on them, without
+ # checking for validity or any optimization etc. Input must be
+ # VALID UTF-8!
+ /**
+ * @param string $string
+ * @return string
+ * @private
+ */
+ function NFC( $string ) {
+ return UtfNormal::fastCompose( UtfNormal::NFD( $string ) );
+ }
+
+ /**
+ * @param string $string
+ * @return string
+ * @private
+ */
+ function NFD( $string ) {
+ UtfNormal::loadData();
+ global $utfCanonicalDecomp;
+ return UtfNormal::fastCombiningSort(
+ UtfNormal::fastDecompose( $string, $utfCanonicalDecomp ) );
+ }
+
+ /**
+ * @param string $string
+ * @return string
+ * @private
+ */
+ function NFKC( $string ) {
+ return UtfNormal::fastCompose( UtfNormal::NFKD( $string ) );
+ }
+
+ /**
+ * @param string $string
+ * @return string
+ * @private
+ */
+ function NFKD( $string ) {
+ global $utfCompatibilityDecomp;
+ if( !isset( $utfCompatibilityDecomp ) ) {
+ require_once( 'UtfNormalDataK.inc' );
+ }
+ return UtfNormal::fastCombiningSort(
+ UtfNormal::fastDecompose( $string, $utfCompatibilityDecomp ) );
+ }
+
+
+ /**
+ * Perform decomposition of a UTF-8 string into either D or KD form
+ * (depending on which decomposition map is passed to us).
+ * Input is assumed to be *valid* UTF-8. Invalid code will break.
+ * @private
+ * @param string $string Valid UTF-8 string
+ * @param array $map hash of expanded decomposition map
+ * @return string a UTF-8 string decomposed, not yet normalized (needs sorting)
+ */
+ function fastDecompose( $string, &$map ) {
+ UtfNormal::loadData();
+ $len = strlen( $string );
+ $out = '';
+ for( $i = 0; $i < $len; $i++ ) {
+ $c = $string{$i};
+ $n = ord( $c );
+ if( $n < 0x80 ) {
+ # ASCII chars never decompose
+ # THEY ARE IMMORTAL
+ $out .= $c;
+ continue;
+ } elseif( $n >= 0xf0 ) {
+ $c = substr( $string, $i, 4 );
+ $i += 3;
+ } elseif( $n >= 0xe0 ) {
+ $c = substr( $string, $i, 3 );
+ $i += 2;
+ } elseif( $n >= 0xc0 ) {
+ $c = substr( $string, $i, 2 );
+ $i++;
+ }
+ if( isset( $map[$c] ) ) {
+ $out .= $map[$c];
+ continue;
+ } else {
+ if( $c >= UTF8_HANGUL_FIRST && $c <= UTF8_HANGUL_LAST ) {
+ # Decompose a hangul syllable into jamo;
+ # hardcoded for three-byte UTF-8 sequence.
+ # A lookup table would be slightly faster,
+ # but adds a lot of memory & disk needs.
+ #
+ $index = ( (ord( $c{0} ) & 0x0f) << 12
+ | (ord( $c{1} ) & 0x3f) << 6
+ | (ord( $c{2} ) & 0x3f) )
+ - UNICODE_HANGUL_FIRST;
+ $l = intval( $index / UNICODE_HANGUL_NCOUNT );
+ $v = intval( ($index % UNICODE_HANGUL_NCOUNT) / UNICODE_HANGUL_TCOUNT);
+ $t = $index % UNICODE_HANGUL_TCOUNT;
+ $out .= "\xe1\x84" . chr( 0x80 + $l ) . "\xe1\x85" . chr( 0xa1 + $v );
+ if( $t >= 25 ) {
+ $out .= "\xe1\x87" . chr( 0x80 + $t - 25 );
+ } elseif( $t ) {
+ $out .= "\xe1\x86" . chr( 0xa7 + $t );
+ }
+ continue;
+ }
+ }
+ $out .= $c;
+ }
+ return $out;
+ }
+
+ /**
+ * Sorts combining characters into canonical order. This is the
+ * final step in creating decomposed normal forms D and KD.
+ * @private
+ * @param string $string a valid, decomposed UTF-8 string. Input is not validated.
+ * @return string a UTF-8 string with combining characters sorted in canonical order
+ */
+ function fastCombiningSort( $string ) {
+ UtfNormal::loadData();
+ global $utfCombiningClass;
+ $len = strlen( $string );
+ $out = '';
+ $combiners = array();
+ $lastClass = -1;
+ for( $i = 0; $i < $len; $i++ ) {
+ $c = $string{$i};
+ $n = ord( $c );
+ if( $n >= 0x80 ) {
+ if( $n >= 0xf0 ) {
+ $c = substr( $string, $i, 4 );
+ $i += 3;
+ } elseif( $n >= 0xe0 ) {
+ $c = substr( $string, $i, 3 );
+ $i += 2;
+ } elseif( $n >= 0xc0 ) {
+ $c = substr( $string, $i, 2 );
+ $i++;
+ }
+ if( isset( $utfCombiningClass[$c] ) ) {
+ $lastClass = $utfCombiningClass[$c];
+ @$combiners[$lastClass] .= $c;
+ continue;
+ }
+ }
+ if( $lastClass ) {
+ ksort( $combiners );
+ $out .= implode( '', $combiners );
+ $combiners = array();
+ }
+ $out .= $c;
+ $lastClass = 0;
+ }
+ if( $lastClass ) {
+ ksort( $combiners );
+ $out .= implode( '', $combiners );
+ }
+ return $out;
+ }
+
+ /**
+ * Produces canonically composed sequences, i.e. normal form C or KC.
+ *
+ * @private
+ * @param string $string a valid UTF-8 string in sorted normal form D or KD. Input is not validated.
+ * @return string a UTF-8 string with canonical precomposed characters used where possible
+ */
+ function fastCompose( $string ) {
+ UtfNormal::loadData();
+ global $utfCanonicalComp, $utfCombiningClass;
+ $len = strlen( $string );
+ $out = '';
+ $lastClass = -1;
+ $lastHangul = 0;
+ $startChar = '';
+ $combining = '';
+ $x1 = ord(substr(UTF8_HANGUL_VBASE,0,1));
+ $x2 = ord(substr(UTF8_HANGUL_TEND,0,1));
+ for( $i = 0; $i < $len; $i++ ) {
+ $c = $string{$i};
+ $n = ord( $c );
+ if( $n < 0x80 ) {
+ # No combining characters here...
+ $out .= $startChar;
+ $out .= $combining;
+ $startChar = $c;
+ $combining = '';
+ $lastClass = 0;
+ continue;
+ } elseif( $n >= 0xf0 ) {
+ $c = substr( $string, $i, 4 );
+ $i += 3;
+ } elseif( $n >= 0xe0 ) {
+ $c = substr( $string, $i, 3 );
+ $i += 2;
+ } elseif( $n >= 0xc0 ) {
+ $c = substr( $string, $i, 2 );
+ $i++;
+ }
+ $pair = $startChar . $c;
+ if( $n > 0x80 ) {
+ if( isset( $utfCombiningClass[$c] ) ) {
+ # A combining char; see what we can do with it
+ $class = $utfCombiningClass[$c];
+ if( !empty( $startChar ) &&
+ $lastClass < $class &&
+ $class > 0 &&
+ isset( $utfCanonicalComp[$pair] ) ) {
+ $startChar = $utfCanonicalComp[$pair];
+ $class = 0;
+ } else {
+ $combining .= $c;
+ }
+ $lastClass = $class;
+ $lastHangul = 0;
+ continue;
+ }
+ }
+ # New start char
+ if( $lastClass == 0 ) {
+ if( isset( $utfCanonicalComp[$pair] ) ) {
+ $startChar = $utfCanonicalComp[$pair];
+ $lastHangul = 0;
+ continue;
+ }
+ if( $n >= $x1 && $n <= $x2 ) {
+ # WARNING: Hangul code is painfully slow.
+ # I apologize for this ugly, ugly code; however
+ # performance is even more teh suck if we call
+ # out to nice clean functions. Lookup tables are
+ # marginally faster, but require a lot of space.
+ #
+ if( $c >= UTF8_HANGUL_VBASE &&
+ $c <= UTF8_HANGUL_VEND &&
+ $startChar >= UTF8_HANGUL_LBASE &&
+ $startChar <= UTF8_HANGUL_LEND ) {
+ #
+ #$lIndex = utf8ToCodepoint( $startChar ) - UNICODE_HANGUL_LBASE;
+ #$vIndex = utf8ToCodepoint( $c ) - UNICODE_HANGUL_VBASE;
+ $lIndex = ord( $startChar{2} ) - 0x80;
+ $vIndex = ord( $c{2} ) - 0xa1;
+
+ $hangulPoint = UNICODE_HANGUL_FIRST +
+ UNICODE_HANGUL_TCOUNT *
+ (UNICODE_HANGUL_VCOUNT * $lIndex + $vIndex);
+
+ # Hardcode the limited-range UTF-8 conversion:
+ $startChar = chr( $hangulPoint >> 12 & 0x0f | 0xe0 ) .
+ chr( $hangulPoint >> 6 & 0x3f | 0x80 ) .
+ chr( $hangulPoint & 0x3f | 0x80 );
+ $lastHangul = 0;
+ continue;
+ } elseif( $c >= UTF8_HANGUL_TBASE &&
+ $c <= UTF8_HANGUL_TEND &&
+ $startChar >= UTF8_HANGUL_FIRST &&
+ $startChar <= UTF8_HANGUL_LAST &&
+ !$lastHangul ) {
+ # $tIndex = utf8ToCodepoint( $c ) - UNICODE_HANGUL_TBASE;
+ $tIndex = ord( $c{2} ) - 0xa7;
+ if( $tIndex < 0 ) $tIndex = ord( $c{2} ) - 0x80 + (0x11c0 - 0x11a7);
+
+ # Increment the code point by $tIndex, without
+ # the function overhead of decoding and recoding UTF-8
+ #
+ $tail = ord( $startChar{2} ) + $tIndex;
+ if( $tail > 0xbf ) {
+ $tail -= 0x40;
+ $mid = ord( $startChar{1} ) + 1;
+ if( $mid > 0xbf ) {
+ $startChar{0} = chr( ord( $startChar{0} ) + 1 );
+ $mid -= 0x40;
+ }
+ $startChar{1} = chr( $mid );
+ }
+ $startChar{2} = chr( $tail );
+
+ # If there's another jamo char after this, *don't* try to merge it.
+ $lastHangul = 1;
+ continue;
+ }
+ }
+ }
+ $out .= $startChar;
+ $out .= $combining;
+ $startChar = $c;
+ $combining = '';
+ $lastClass = 0;
+ $lastHangul = 0;
+ }
+ $out .= $startChar . $combining;
+ return $out;
+ }
+
+ /**
+ * This is just used for the benchmark, comparing how long it takes to
+ * interate through a string without really doing anything of substance.
+ * @param string $string
+ * @return string
+ */
+ function placebo( $string ) {
+ $len = strlen( $string );
+ $out = '';
+ for( $i = 0; $i < $len; $i++ ) {
+ $out .= $string{$i};
+ }
+ return $out;
+ }
+}
+
+?>
diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php
new file mode 100644
index 00000000..a5eb267e
--- /dev/null
+++ b/includes/normal/UtfNormalBench.php
@@ -0,0 +1,107 @@
+<?php
+# 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
+
+/**
+ * Approximate benchmark for some basic operations.
+ *
+ * @package UtfNormal
+ * @access private
+ */
+
+/** */
+if( isset( $_SERVER['argv'] ) && in_array( '--icu', $_SERVER['argv'] ) ) {
+ dl( 'php_utfnormal.so' );
+}
+
+require_once 'UtfNormalUtil.php';
+require_once 'UtfNormal.php';
+
+define( 'BENCH_CYCLES', 5 );
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+$testfiles = array(
+ 'testdata/washington.txt' => 'English text',
+ 'testdata/berlin.txt' => 'German text',
+ 'testdata/bulgakov.txt' => 'Russian text',
+ 'testdata/tokyo.txt' => 'Japanese text',
+ 'testdata/sociology.txt' => 'Korean text'
+);
+$normalizer = new UtfNormal;
+UtfNormal::loadData();
+foreach( $testfiles as $file => $desc ) {
+ benchmarkTest( $normalizer, $file, $desc );
+}
+
+# -------
+
+function benchmarkTest( &$u, $filename, $desc ) {
+ print "Testing $filename ($desc)...\n";
+ $data = file_get_contents( $filename );
+ $forms = array(
+# 'placebo',
+ 'cleanUp',
+ 'toNFC',
+# 'toNFKC',
+# 'toNFD', 'toNFKD',
+ 'NFC',
+# 'NFKC',
+# 'NFD', 'NFKD',
+ array( 'fastDecompose', 'fastCombiningSort', 'fastCompose' ),
+# 'quickIsNFC', 'quickIsNFCVerify',
+ );
+ foreach( $forms as $form ) {
+ if( is_array( $form ) ) {
+ $str = $data;
+ foreach( $form as $step ) {
+ $str = benchmarkForm( $u, $str, $step );
+ }
+ } else {
+ benchmarkForm( $u, $data, $form );
+ }
+ }
+}
+
+function benchTime(){
+ $st = explode( ' ', microtime() );
+ return (float)$st[0] + (float)$st[1];
+}
+
+function benchmarkForm( &$u, &$data, $form ) {
+ global $utfCanonicalDecomp;
+ #$start = benchTime();
+ for( $i = 0; $i < BENCH_CYCLES; $i++ ) {
+ $start = benchTime();
+ $out = $u->$form( $data, $utfCanonicalDecomp );
+ $deltas[] = (benchTime() - $start);
+ }
+ #$delta = (benchTime() - $start) / BENCH_CYCLES;
+ sort( $deltas );
+ $delta = $deltas[0]; # Take shortest time
+
+ $rate = intval( strlen( $data ) / $delta );
+ $same = (0 == strcmp( $data, $out ) );
+
+ printf( " %20s %6.1fms %8d bytes/s (%s)\n", $form, $delta*1000.0, $rate, ($same ? 'no change' : 'changed' ) );
+ return $out;
+}
+
+?>
diff --git a/includes/normal/UtfNormalData.inc b/includes/normal/UtfNormalData.inc
new file mode 100644
index 00000000..6216d1a3
--- /dev/null
+++ b/includes/normal/UtfNormalData.inc
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This file was automatically generated -- do not edit!
+ * Run UtfNormalGenerate.php to create this file again (make clean && make)
+ * @package MediaWiki
+ */
+/** */
+global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp, $utfCheckNFC;
+$utfCombiningClass = unserialize( 'a:384:{s:2:"̀";i:230;s:2:"́";i:230;s:2:"̂";i:230;s:2:"̃";i:230;s:2:"̄";i:230;s:2:"̅";i:230;s:2:"̆";i:230;s:2:"̇";i:230;s:2:"̈";i:230;s:2:"̉";i:230;s:2:"̊";i:230;s:2:"̋";i:230;s:2:"̌";i:230;s:2:"̍";i:230;s:2:"̎";i:230;s:2:"̏";i:230;s:2:"̐";i:230;s:2:"̑";i:230;s:2:"̒";i:230;s:2:"̓";i:230;s:2:"̔";i:230;s:2:"̕";i:232;s:2:"̖";i:220;s:2:"̗";i:220;s:2:"̘";i:220;s:2:"̙";i:220;s:2:"̚";i:232;s:2:"̛";i:216;s:2:"̜";i:220;s:2:"̝";i:220;s:2:"̞";i:220;s:2:"̟";i:220;s:2:"̠";i:220;s:2:"̡";i:202;s:2:"̢";i:202;s:2:"̣";i:220;s:2:"̤";i:220;s:2:"̥";i:220;s:2:"̦";i:220;s:2:"̧";i:202;s:2:"̨";i:202;s:2:"̩";i:220;s:2:"̪";i:220;s:2:"̫";i:220;s:2:"̬";i:220;s:2:"̭";i:220;s:2:"̮";i:220;s:2:"̯";i:220;s:2:"̰";i:220;s:2:"̱";i:220;s:2:"̲";i:220;s:2:"̳";i:220;s:2:"̴";i:1;s:2:"̵";i:1;s:2:"̶";i:1;s:2:"̷";i:1;s:2:"̸";i:1;s:2:"̹";i:220;s:2:"̺";i:220;s:2:"̻";i:220;s:2:"̼";i:220;s:2:"̽";i:230;s:2:"̾";i:230;s:2:"̿";i:230;s:2:"̀";i:230;s:2:"́";i:230;s:2:"͂";i:230;s:2:"̓";i:230;s:2:"̈́";i:230;s:2:"ͅ";i:240;s:2:"͆";i:230;s:2:"͇";i:220;s:2:"͈";i:220;s:2:"͉";i:220;s:2:"͊";i:230;s:2:"͋";i:230;s:2:"͌";i:230;s:2:"͍";i:220;s:2:"͎";i:220;s:2:"͐";i:230;s:2:"͑";i:230;s:2:"͒";i:230;s:2:"͓";i:220;s:2:"͔";i:220;s:2:"͕";i:220;s:2:"͖";i:220;s:2:"͗";i:230;s:2:"͘";i:232;s:2:"͙";i:220;s:2:"͚";i:220;s:2:"͛";i:230;s:2:"͜";i:233;s:2:"͝";i:234;s:2:"͞";i:234;s:2:"͟";i:233;s:2:"͠";i:234;s:2:"͡";i:234;s:2:"͢";i:233;s:2:"ͣ";i:230;s:2:"ͤ";i:230;s:2:"ͥ";i:230;s:2:"ͦ";i:230;s:2:"ͧ";i:230;s:2:"ͨ";i:230;s:2:"ͩ";i:230;s:2:"ͪ";i:230;s:2:"ͫ";i:230;s:2:"ͬ";i:230;s:2:"ͭ";i:230;s:2:"ͮ";i:230;s:2:"ͯ";i:230;s:2:"҃";i:230;s:2:"҄";i:230;s:2:"҅";i:230;s:2:"҆";i:230;s:2:"֑";i:220;s:2:"֒";i:230;s:2:"֓";i:230;s:2:"֔";i:230;s:2:"֕";i:230;s:2:"֖";i:220;s:2:"֗";i:230;s:2:"֘";i:230;s:2:"֙";i:230;s:2:"֚";i:222;s:2:"֛";i:220;s:2:"֜";i:230;s:2:"֝";i:230;s:2:"֞";i:230;s:2:"֟";i:230;s:2:"֠";i:230;s:2:"֡";i:230;s:2:"֢";i:220;s:2:"֣";i:220;s:2:"֤";i:220;s:2:"֥";i:220;s:2:"֦";i:220;s:2:"֧";i:220;s:2:"֨";i:230;s:2:"֩";i:230;s:2:"֪";i:220;s:2:"֫";i:230;s:2:"֬";i:230;s:2:"֭";i:222;s:2:"֮";i:228;s:2:"֯";i:230;s:2:"ְ";i:10;s:2:"ֱ";i:11;s:2:"ֲ";i:12;s:2:"ֳ";i:13;s:2:"ִ";i:14;s:2:"ֵ";i:15;s:2:"ֶ";i:16;s:2:"ַ";i:17;s:2:"ָ";i:18;s:2:"ֹ";i:19;s:2:"ֻ";i:20;s:2:"ּ";i:21;s:2:"ֽ";i:22;s:2:"ֿ";i:23;s:2:"ׁ";i:24;s:2:"ׂ";i:25;s:2:"ׄ";i:230;s:2:"ׅ";i:220;s:2:"ׇ";i:18;s:2:"ؐ";i:230;s:2:"ؑ";i:230;s:2:"ؒ";i:230;s:2:"ؓ";i:230;s:2:"ؔ";i:230;s:2:"ؕ";i:230;s:2:"ً";i:27;s:2:"ٌ";i:28;s:2:"ٍ";i:29;s:2:"َ";i:30;s:2:"ُ";i:31;s:2:"ِ";i:32;s:2:"ّ";i:33;s:2:"ْ";i:34;s:2:"ٓ";i:230;s:2:"ٔ";i:230;s:2:"ٕ";i:220;s:2:"ٖ";i:220;s:2:"ٗ";i:230;s:2:"٘";i:230;s:2:"ٙ";i:230;s:2:"ٚ";i:230;s:2:"ٛ";i:230;s:2:"ٜ";i:220;s:2:"ٝ";i:230;s:2:"ٞ";i:230;s:2:"ٰ";i:35;s:2:"ۖ";i:230;s:2:"ۗ";i:230;s:2:"ۘ";i:230;s:2:"ۙ";i:230;s:2:"ۚ";i:230;s:2:"ۛ";i:230;s:2:"ۜ";i:230;s:2:"۟";i:230;s:2:"۠";i:230;s:2:"ۡ";i:230;s:2:"ۢ";i:230;s:2:"ۣ";i:220;s:2:"ۤ";i:230;s:2:"ۧ";i:230;s:2:"ۨ";i:230;s:2:"۪";i:220;s:2:"۫";i:230;s:2:"۬";i:230;s:2:"ۭ";i:220;s:2:"ܑ";i:36;s:2:"ܰ";i:230;s:2:"ܱ";i:220;s:2:"ܲ";i:230;s:2:"ܳ";i:230;s:2:"ܴ";i:220;s:2:"ܵ";i:230;s:2:"ܶ";i:230;s:2:"ܷ";i:220;s:2:"ܸ";i:220;s:2:"ܹ";i:220;s:2:"ܺ";i:230;s:2:"ܻ";i:220;s:2:"ܼ";i:220;s:2:"ܽ";i:230;s:2:"ܾ";i:220;s:2:"ܿ";i:230;s:2:"݀";i:230;s:2:"݁";i:230;s:2:"݂";i:220;s:2:"݃";i:230;s:2:"݄";i:220;s:2:"݅";i:230;s:2:"݆";i:220;s:2:"݇";i:230;s:2:"݈";i:220;s:2:"݉";i:230;s:2:"݊";i:230;s:3:"़";i:7;s:3:"्";i:9;s:3:"॑";i:230;s:3:"॒";i:220;s:3:"॓";i:230;s:3:"॔";i:230;s:3:"়";i:7;s:3:"্";i:9;s:3:"਼";i:7;s:3:"੍";i:9;s:3:"઼";i:7;s:3:"્";i:9;s:3:"଼";i:7;s:3:"୍";i:9;s:3:"்";i:9;s:3:"్";i:9;s:3:"ౕ";i:84;s:3:"ౖ";i:91;s:3:"಼";i:7;s:3:"್";i:9;s:3:"്";i:9;s:3:"්";i:9;s:3:"ุ";i:103;s:3:"ู";i:103;s:3:"ฺ";i:9;s:3:"่";i:107;s:3:"้";i:107;s:3:"๊";i:107;s:3:"๋";i:107;s:3:"ຸ";i:118;s:3:"ູ";i:118;s:3:"່";i:122;s:3:"້";i:122;s:3:"໊";i:122;s:3:"໋";i:122;s:3:"༘";i:220;s:3:"༙";i:220;s:3:"༵";i:220;s:3:"༷";i:220;s:3:"༹";i:216;s:3:"ཱ";i:129;s:3:"ི";i:130;s:3:"ུ";i:132;s:3:"ེ";i:130;s:3:"ཻ";i:130;s:3:"ོ";i:130;s:3:"ཽ";i:130;s:3:"ྀ";i:130;s:3:"ྂ";i:230;s:3:"ྃ";i:230;s:3:"྄";i:9;s:3:"྆";i:230;s:3:"྇";i:230;s:3:"࿆";i:220;s:3:"့";i:7;s:3:"္";i:9;s:3:"፟";i:230;s:3:"᜔";i:9;s:3:"᜴";i:9;s:3:"្";i:9;s:3:"៝";i:230;s:3:"ᢩ";i:228;s:3:"᤹";i:222;s:3:"᤺";i:230;s:3:"᤻";i:220;s:3:"ᨗ";i:230;s:3:"ᨘ";i:220;s:3:"᷀";i:230;s:3:"᷁";i:230;s:3:"᷂";i:220;s:3:"᷃";i:230;s:3:"⃐";i:230;s:3:"⃑";i:230;s:3:"⃒";i:1;s:3:"⃓";i:1;s:3:"⃔";i:230;s:3:"⃕";i:230;s:3:"⃖";i:230;s:3:"⃗";i:230;s:3:"⃘";i:1;s:3:"⃙";i:1;s:3:"⃚";i:1;s:3:"⃛";i:230;s:3:"⃜";i:230;s:3:"⃡";i:230;s:3:"⃥";i:1;s:3:"⃦";i:1;s:3:"⃧";i:230;s:3:"⃨";i:220;s:3:"⃩";i:230;s:3:"⃪";i:1;s:3:"⃫";i:1;s:3:"〪";i:218;s:3:"〫";i:228;s:3:"〬";i:232;s:3:"〭";i:222;s:3:"〮";i:224;s:3:"〯";i:224;s:3:"゙";i:8;s:3:"゚";i:8;s:3:"꠆";i:9;s:3:"ﬞ";i:26;s:3:"︠";i:230;s:3:"︡";i:230;s:3:"︢";i:230;s:3:"︣";i:230;s:4:"𐨍";i:220;s:4:"𐨏";i:230;s:4:"𐨸";i:230;s:4:"𐨹";i:1;s:4:"𐨺";i:220;s:4:"𐨿";i:9;s:4:"𝅥";i:216;s:4:"𝅦";i:216;s:4:"𝅧";i:1;s:4:"𝅨";i:1;s:4:"𝅩";i:1;s:4:"𝅭";i:226;s:4:"𝅮";i:216;s:4:"𝅯";i:216;s:4:"𝅰";i:216;s:4:"𝅱";i:216;s:4:"𝅲";i:216;s:4:"𝅻";i:220;s:4:"𝅼";i:220;s:4:"𝅽";i:220;s:4:"𝅾";i:220;s:4:"𝅿";i:220;s:4:"𝆀";i:220;s:4:"𝆁";i:220;s:4:"𝆂";i:220;s:4:"𝆅";i:230;s:4:"𝆆";i:230;s:4:"𝆇";i:230;s:4:"𝆈";i:230;s:4:"𝆉";i:230;s:4:"𝆊";i:220;s:4:"𝆋";i:220;s:4:"𝆪";i:230;s:4:"𝆫";i:230;s:4:"𝆬";i:230;s:4:"𝆭";i:230;s:4:"𝉂";i:230;s:4:"𝉃";i:230;s:4:"𝉄";i:230;}' );
+$utfCanonicalComp = unserialize( 'a:1851:{s:3:"À";s:2:"À";s:3:"Á";s:2:"Á";s:3:"Â";s:2:"Â";s:3:"Ã";s:2:"Ã";s:3:"Ä";s:2:"Ä";s:3:"Å";s:2:"Å";s:3:"Ç";s:2:"Ç";s:3:"È";s:2:"È";s:3:"É";s:2:"É";s:3:"Ê";s:2:"Ê";s:3:"Ë";s:2:"Ë";s:3:"Ì";s:2:"Ì";s:3:"Í";s:2:"Í";s:3:"Î";s:2:"Î";s:3:"Ï";s:2:"Ï";s:3:"Ñ";s:2:"Ñ";s:3:"Ò";s:2:"Ò";s:3:"Ó";s:2:"Ó";s:3:"Ô";s:2:"Ô";s:3:"Õ";s:2:"Õ";s:3:"Ö";s:2:"Ö";s:3:"Ù";s:2:"Ù";s:3:"Ú";s:2:"Ú";s:3:"Û";s:2:"Û";s:3:"Ü";s:2:"Ü";s:3:"Ý";s:2:"Ý";s:3:"à";s:2:"à";s:3:"á";s:2:"á";s:3:"â";s:2:"â";s:3:"ã";s:2:"ã";s:3:"ä";s:2:"ä";s:3:"å";s:2:"å";s:3:"ç";s:2:"ç";s:3:"è";s:2:"è";s:3:"é";s:2:"é";s:3:"ê";s:2:"ê";s:3:"ë";s:2:"ë";s:3:"ì";s:2:"ì";s:3:"í";s:2:"í";s:3:"î";s:2:"î";s:3:"ï";s:2:"ï";s:3:"ñ";s:2:"ñ";s:3:"ò";s:2:"ò";s:3:"ó";s:2:"ó";s:3:"ô";s:2:"ô";s:3:"õ";s:2:"õ";s:3:"ö";s:2:"ö";s:3:"ù";s:2:"ù";s:3:"ú";s:2:"ú";s:3:"û";s:2:"û";s:3:"ü";s:2:"ü";s:3:"ý";s:2:"ý";s:3:"ÿ";s:2:"ÿ";s:3:"Ā";s:2:"Ā";s:3:"ā";s:2:"ā";s:3:"Ă";s:2:"Ă";s:3:"ă";s:2:"ă";s:3:"Ą";s:2:"Ą";s:3:"ą";s:2:"ą";s:3:"Ć";s:2:"Ć";s:3:"ć";s:2:"ć";s:3:"Ĉ";s:2:"Ĉ";s:3:"ĉ";s:2:"ĉ";s:3:"Ċ";s:2:"Ċ";s:3:"ċ";s:2:"ċ";s:3:"Č";s:2:"Č";s:3:"č";s:2:"č";s:3:"Ď";s:2:"Ď";s:3:"ď";s:2:"ď";s:3:"Ē";s:2:"Ē";s:3:"ē";s:2:"ē";s:3:"Ĕ";s:2:"Ĕ";s:3:"ĕ";s:2:"ĕ";s:3:"Ė";s:2:"Ė";s:3:"ė";s:2:"ė";s:3:"Ę";s:2:"Ę";s:3:"ę";s:2:"ę";s:3:"Ě";s:2:"Ě";s:3:"ě";s:2:"ě";s:3:"Ĝ";s:2:"Ĝ";s:3:"ĝ";s:2:"ĝ";s:3:"Ğ";s:2:"Ğ";s:3:"ğ";s:2:"ğ";s:3:"Ġ";s:2:"Ġ";s:3:"ġ";s:2:"ġ";s:3:"Ģ";s:2:"Ģ";s:3:"ģ";s:2:"ģ";s:3:"Ĥ";s:2:"Ĥ";s:3:"ĥ";s:2:"ĥ";s:3:"Ĩ";s:2:"Ĩ";s:3:"ĩ";s:2:"ĩ";s:3:"Ī";s:2:"Ī";s:3:"ī";s:2:"ī";s:3:"Ĭ";s:2:"Ĭ";s:3:"ĭ";s:2:"ĭ";s:3:"Į";s:2:"Į";s:3:"į";s:2:"į";s:3:"İ";s:2:"İ";s:3:"Ĵ";s:2:"Ĵ";s:3:"ĵ";s:2:"ĵ";s:3:"Ķ";s:2:"Ķ";s:3:"ķ";s:2:"ķ";s:3:"Ĺ";s:2:"Ĺ";s:3:"ĺ";s:2:"ĺ";s:3:"Ļ";s:2:"Ļ";s:3:"ļ";s:2:"ļ";s:3:"Ľ";s:2:"Ľ";s:3:"ľ";s:2:"ľ";s:3:"Ń";s:2:"Ń";s:3:"ń";s:2:"ń";s:3:"Ņ";s:2:"Ņ";s:3:"ņ";s:2:"ņ";s:3:"Ň";s:2:"Ň";s:3:"ň";s:2:"ň";s:3:"Ō";s:2:"Ō";s:3:"ō";s:2:"ō";s:3:"Ŏ";s:2:"Ŏ";s:3:"ŏ";s:2:"ŏ";s:3:"Ő";s:2:"Ő";s:3:"ő";s:2:"ő";s:3:"Ŕ";s:2:"Ŕ";s:3:"ŕ";s:2:"ŕ";s:3:"Ŗ";s:2:"Ŗ";s:3:"ŗ";s:2:"ŗ";s:3:"Ř";s:2:"Ř";s:3:"ř";s:2:"ř";s:3:"Ś";s:2:"Ś";s:3:"ś";s:2:"ś";s:3:"Ŝ";s:2:"Ŝ";s:3:"ŝ";s:2:"ŝ";s:3:"Ş";s:2:"Ş";s:3:"ş";s:2:"ş";s:3:"Š";s:2:"Š";s:3:"š";s:2:"š";s:3:"Ţ";s:2:"Ţ";s:3:"ţ";s:2:"ţ";s:3:"Ť";s:2:"Ť";s:3:"ť";s:2:"ť";s:3:"Ũ";s:2:"Ũ";s:3:"ũ";s:2:"ũ";s:3:"Ū";s:2:"Ū";s:3:"ū";s:2:"ū";s:3:"Ŭ";s:2:"Ŭ";s:3:"ŭ";s:2:"ŭ";s:3:"Ů";s:2:"Ů";s:3:"ů";s:2:"ů";s:3:"Ű";s:2:"Ű";s:3:"ű";s:2:"ű";s:3:"Ų";s:2:"Ų";s:3:"ų";s:2:"ų";s:3:"Ŵ";s:2:"Ŵ";s:3:"ŵ";s:2:"ŵ";s:3:"Ŷ";s:2:"Ŷ";s:3:"ŷ";s:2:"ŷ";s:3:"Ÿ";s:2:"Ÿ";s:3:"Ź";s:2:"Ź";s:3:"ź";s:2:"ź";s:3:"Ż";s:2:"Ż";s:3:"ż";s:2:"ż";s:3:"Ž";s:2:"Ž";s:3:"ž";s:2:"ž";s:3:"Ơ";s:2:"Ơ";s:3:"ơ";s:2:"ơ";s:3:"Ư";s:2:"Ư";s:3:"ư";s:2:"ư";s:3:"Ǎ";s:2:"Ǎ";s:3:"ǎ";s:2:"ǎ";s:3:"Ǐ";s:2:"Ǐ";s:3:"ǐ";s:2:"ǐ";s:3:"Ǒ";s:2:"Ǒ";s:3:"ǒ";s:2:"ǒ";s:3:"Ǔ";s:2:"Ǔ";s:3:"ǔ";s:2:"ǔ";s:4:"Ǖ";s:2:"Ǖ";s:4:"ǖ";s:2:"ǖ";s:4:"Ǘ";s:2:"Ǘ";s:4:"ǘ";s:2:"ǘ";s:4:"Ǚ";s:2:"Ǚ";s:4:"ǚ";s:2:"ǚ";s:4:"Ǜ";s:2:"Ǜ";s:4:"ǜ";s:2:"ǜ";s:4:"Ǟ";s:2:"Ǟ";s:4:"ǟ";s:2:"ǟ";s:4:"Ǡ";s:2:"Ǡ";s:4:"ǡ";s:2:"ǡ";s:4:"Ǣ";s:2:"Ǣ";s:4:"ǣ";s:2:"ǣ";s:3:"Ǧ";s:2:"Ǧ";s:3:"ǧ";s:2:"ǧ";s:3:"Ǩ";s:2:"Ǩ";s:3:"ǩ";s:2:"ǩ";s:3:"Ǫ";s:2:"Ǫ";s:3:"ǫ";s:2:"ǫ";s:4:"Ǭ";s:2:"Ǭ";s:4:"ǭ";s:2:"ǭ";s:4:"Ǯ";s:2:"Ǯ";s:4:"ǯ";s:2:"ǯ";s:3:"ǰ";s:2:"ǰ";s:3:"Ǵ";s:2:"Ǵ";s:3:"ǵ";s:2:"ǵ";s:3:"Ǹ";s:2:"Ǹ";s:3:"ǹ";s:2:"ǹ";s:4:"Ǻ";s:2:"Ǻ";s:4:"ǻ";s:2:"ǻ";s:4:"Ǽ";s:2:"Ǽ";s:4:"ǽ";s:2:"ǽ";s:4:"Ǿ";s:2:"Ǿ";s:4:"ǿ";s:2:"ǿ";s:3:"Ȁ";s:2:"Ȁ";s:3:"ȁ";s:2:"ȁ";s:3:"Ȃ";s:2:"Ȃ";s:3:"ȃ";s:2:"ȃ";s:3:"Ȅ";s:2:"Ȅ";s:3:"ȅ";s:2:"ȅ";s:3:"Ȇ";s:2:"Ȇ";s:3:"ȇ";s:2:"ȇ";s:3:"Ȉ";s:2:"Ȉ";s:3:"ȉ";s:2:"ȉ";s:3:"Ȋ";s:2:"Ȋ";s:3:"ȋ";s:2:"ȋ";s:3:"Ȍ";s:2:"Ȍ";s:3:"ȍ";s:2:"ȍ";s:3:"Ȏ";s:2:"Ȏ";s:3:"ȏ";s:2:"ȏ";s:3:"Ȑ";s:2:"Ȑ";s:3:"ȑ";s:2:"ȑ";s:3:"Ȓ";s:2:"Ȓ";s:3:"ȓ";s:2:"ȓ";s:3:"Ȕ";s:2:"Ȕ";s:3:"ȕ";s:2:"ȕ";s:3:"Ȗ";s:2:"Ȗ";s:3:"ȗ";s:2:"ȗ";s:3:"Ș";s:2:"Ș";s:3:"ș";s:2:"ș";s:3:"Ț";s:2:"Ț";s:3:"ț";s:2:"ț";s:3:"Ȟ";s:2:"Ȟ";s:3:"ȟ";s:2:"ȟ";s:3:"Ȧ";s:2:"Ȧ";s:3:"ȧ";s:2:"ȧ";s:3:"Ȩ";s:2:"Ȩ";s:3:"ȩ";s:2:"ȩ";s:4:"Ȫ";s:2:"Ȫ";s:4:"ȫ";s:2:"ȫ";s:4:"Ȭ";s:2:"Ȭ";s:4:"ȭ";s:2:"ȭ";s:3:"Ȯ";s:2:"Ȯ";s:3:"ȯ";s:2:"ȯ";s:4:"Ȱ";s:2:"Ȱ";s:4:"ȱ";s:2:"ȱ";s:3:"Ȳ";s:2:"Ȳ";s:3:"ȳ";s:2:"ȳ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:4:"̈́";s:2:"̈́";s:2:"ʹ";s:2:"ʹ";s:1:";";s:2:";";s:4:"΅";s:2:"΅";s:4:"Ά";s:2:"Ά";s:2:"·";s:2:"·";s:4:"Έ";s:2:"Έ";s:4:"Ή";s:2:"Ή";s:4:"Ί";s:2:"Ί";s:4:"Ό";s:2:"Ό";s:4:"Ύ";s:2:"Ύ";s:4:"Ώ";s:2:"Ώ";s:4:"ΐ";s:2:"ΐ";s:4:"Ϊ";s:2:"Ϊ";s:4:"Ϋ";s:2:"Ϋ";s:4:"ά";s:2:"ά";s:4:"έ";s:2:"έ";s:4:"ή";s:2:"ή";s:4:"ί";s:2:"ί";s:4:"ΰ";s:2:"ΰ";s:4:"ϊ";s:2:"ϊ";s:4:"ϋ";s:2:"ϋ";s:4:"ό";s:2:"ό";s:4:"ύ";s:2:"ύ";s:4:"ώ";s:2:"ώ";s:4:"ϓ";s:2:"ϓ";s:4:"ϔ";s:2:"ϔ";s:4:"Ѐ";s:2:"Ѐ";s:4:"Ё";s:2:"Ё";s:4:"Ѓ";s:2:"Ѓ";s:4:"Ї";s:2:"Ї";s:4:"Ќ";s:2:"Ќ";s:4:"Ѝ";s:2:"Ѝ";s:4:"Ў";s:2:"Ў";s:4:"Й";s:2:"Й";s:4:"й";s:2:"й";s:4:"ѐ";s:2:"ѐ";s:4:"ё";s:2:"ё";s:4:"ѓ";s:2:"ѓ";s:4:"ї";s:2:"ї";s:4:"ќ";s:2:"ќ";s:4:"ѝ";s:2:"ѝ";s:4:"ў";s:2:"ў";s:4:"Ѷ";s:2:"Ѷ";s:4:"ѷ";s:2:"ѷ";s:4:"Ӂ";s:2:"Ӂ";s:4:"ӂ";s:2:"ӂ";s:4:"Ӑ";s:2:"Ӑ";s:4:"ӑ";s:2:"ӑ";s:4:"Ӓ";s:2:"Ӓ";s:4:"ӓ";s:2:"ӓ";s:4:"Ӗ";s:2:"Ӗ";s:4:"ӗ";s:2:"ӗ";s:4:"Ӛ";s:2:"Ӛ";s:4:"ӛ";s:2:"ӛ";s:4:"Ӝ";s:2:"Ӝ";s:4:"ӝ";s:2:"ӝ";s:4:"Ӟ";s:2:"Ӟ";s:4:"ӟ";s:2:"ӟ";s:4:"Ӣ";s:2:"Ӣ";s:4:"ӣ";s:2:"ӣ";s:4:"Ӥ";s:2:"Ӥ";s:4:"ӥ";s:2:"ӥ";s:4:"Ӧ";s:2:"Ӧ";s:4:"ӧ";s:2:"ӧ";s:4:"Ӫ";s:2:"Ӫ";s:4:"ӫ";s:2:"ӫ";s:4:"Ӭ";s:2:"Ӭ";s:4:"ӭ";s:2:"ӭ";s:4:"Ӯ";s:2:"Ӯ";s:4:"ӯ";s:2:"ӯ";s:4:"Ӱ";s:2:"Ӱ";s:4:"ӱ";s:2:"ӱ";s:4:"Ӳ";s:2:"Ӳ";s:4:"ӳ";s:2:"ӳ";s:4:"Ӵ";s:2:"Ӵ";s:4:"ӵ";s:2:"ӵ";s:4:"Ӹ";s:2:"Ӹ";s:4:"ӹ";s:2:"ӹ";s:4:"آ";s:2:"آ";s:4:"أ";s:2:"أ";s:4:"ؤ";s:2:"ؤ";s:4:"إ";s:2:"إ";s:4:"ئ";s:2:"ئ";s:4:"ۀ";s:2:"ۀ";s:4:"ۂ";s:2:"ۂ";s:4:"ۓ";s:2:"ۓ";s:6:"ऩ";s:3:"ऩ";s:6:"ऱ";s:3:"ऱ";s:6:"ऴ";s:3:"ऴ";s:6:"ো";s:3:"ো";s:6:"ৌ";s:3:"ৌ";s:6:"ୈ";s:3:"ୈ";s:6:"ୋ";s:3:"ୋ";s:6:"ୌ";s:3:"ୌ";s:6:"ஔ";s:3:"ஔ";s:6:"ொ";s:3:"ொ";s:6:"ோ";s:3:"ோ";s:6:"ௌ";s:3:"ௌ";s:6:"ై";s:3:"ై";s:6:"ೀ";s:3:"ೀ";s:6:"ೇ";s:3:"ೇ";s:6:"ೈ";s:3:"ೈ";s:6:"ೊ";s:3:"ೊ";s:6:"ೋ";s:3:"ೋ";s:6:"ൊ";s:3:"ൊ";s:6:"ോ";s:3:"ോ";s:6:"ൌ";s:3:"ൌ";s:6:"ේ";s:3:"ේ";s:6:"ො";s:3:"ො";s:6:"ෝ";s:3:"ෝ";s:6:"ෞ";s:3:"ෞ";s:6:"ཱི";s:3:"ཱི";s:6:"ཱུ";s:3:"ཱུ";s:6:"ཱྀ";s:3:"ཱྀ";s:6:"ဦ";s:3:"ဦ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:4:"Ḉ";s:3:"Ḉ";s:4:"ḉ";s:3:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:4:"Ḕ";s:3:"Ḕ";s:4:"ḕ";s:3:"ḕ";s:4:"Ḗ";s:3:"Ḗ";s:4:"ḗ";s:3:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:4:"Ḝ";s:3:"Ḝ";s:4:"ḝ";s:3:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:4:"Ḯ";s:3:"Ḯ";s:4:"ḯ";s:3:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:5:"Ḹ";s:3:"Ḹ";s:5:"ḹ";s:3:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:4:"Ṍ";s:3:"Ṍ";s:4:"ṍ";s:3:"ṍ";s:4:"Ṏ";s:3:"Ṏ";s:4:"ṏ";s:3:"ṏ";s:4:"Ṑ";s:3:"Ṑ";s:4:"ṑ";s:3:"ṑ";s:4:"Ṓ";s:3:"Ṓ";s:4:"ṓ";s:3:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:5:"Ṝ";s:3:"Ṝ";s:5:"ṝ";s:3:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:4:"Ṥ";s:3:"Ṥ";s:4:"ṥ";s:3:"ṥ";s:4:"Ṧ";s:3:"Ṧ";s:4:"ṧ";s:3:"ṧ";s:5:"Ṩ";s:3:"Ṩ";s:5:"ṩ";s:3:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:4:"Ṹ";s:3:"Ṹ";s:4:"ṹ";s:3:"ṹ";s:4:"Ṻ";s:3:"Ṻ";s:4:"ṻ";s:3:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:4:"ẛ";s:3:"ẛ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:4:"Ấ";s:3:"Ấ";s:4:"ấ";s:3:"ấ";s:4:"Ầ";s:3:"Ầ";s:4:"ầ";s:3:"ầ";s:4:"Ẩ";s:3:"Ẩ";s:4:"ẩ";s:3:"ẩ";s:4:"Ẫ";s:3:"Ẫ";s:4:"ẫ";s:3:"ẫ";s:5:"Ậ";s:3:"Ậ";s:5:"ậ";s:3:"ậ";s:4:"Ắ";s:3:"Ắ";s:4:"ắ";s:3:"ắ";s:4:"Ằ";s:3:"Ằ";s:4:"ằ";s:3:"ằ";s:4:"Ẳ";s:3:"Ẳ";s:4:"ẳ";s:3:"ẳ";s:4:"Ẵ";s:3:"Ẵ";s:4:"ẵ";s:3:"ẵ";s:5:"Ặ";s:3:"Ặ";s:5:"ặ";s:3:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:4:"Ế";s:3:"Ế";s:4:"ế";s:3:"ế";s:4:"Ề";s:3:"Ề";s:4:"ề";s:3:"ề";s:4:"Ể";s:3:"Ể";s:4:"ể";s:3:"ể";s:4:"Ễ";s:3:"Ễ";s:4:"ễ";s:3:"ễ";s:5:"Ệ";s:3:"Ệ";s:5:"ệ";s:3:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:4:"Ố";s:3:"Ố";s:4:"ố";s:3:"ố";s:4:"Ồ";s:3:"Ồ";s:4:"ồ";s:3:"ồ";s:4:"Ổ";s:3:"Ổ";s:4:"ổ";s:3:"ổ";s:4:"Ỗ";s:3:"Ỗ";s:4:"ỗ";s:3:"ỗ";s:5:"Ộ";s:3:"Ộ";s:5:"ộ";s:3:"ộ";s:4:"Ớ";s:3:"Ớ";s:4:"ớ";s:3:"ớ";s:4:"Ờ";s:3:"Ờ";s:4:"ờ";s:3:"ờ";s:4:"Ở";s:3:"Ở";s:4:"ở";s:3:"ở";s:4:"Ỡ";s:3:"Ỡ";s:4:"ỡ";s:3:"ỡ";s:4:"Ợ";s:3:"Ợ";s:4:"ợ";s:3:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:4:"Ứ";s:3:"Ứ";s:4:"ứ";s:3:"ứ";s:4:"Ừ";s:3:"Ừ";s:4:"ừ";s:3:"ừ";s:4:"Ử";s:3:"Ử";s:4:"ử";s:3:"ử";s:4:"Ữ";s:3:"Ữ";s:4:"ữ";s:3:"ữ";s:4:"Ự";s:3:"Ự";s:4:"ự";s:3:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:4:"ἀ";s:3:"ἀ";s:4:"ἁ";s:3:"ἁ";s:5:"ἂ";s:3:"ἂ";s:5:"ἃ";s:3:"ἃ";s:5:"ἄ";s:3:"ἄ";s:5:"ἅ";s:3:"ἅ";s:5:"ἆ";s:3:"ἆ";s:5:"ἇ";s:3:"ἇ";s:4:"Ἀ";s:3:"Ἀ";s:4:"Ἁ";s:3:"Ἁ";s:5:"Ἂ";s:3:"Ἂ";s:5:"Ἃ";s:3:"Ἃ";s:5:"Ἄ";s:3:"Ἄ";s:5:"Ἅ";s:3:"Ἅ";s:5:"Ἆ";s:3:"Ἆ";s:5:"Ἇ";s:3:"Ἇ";s:4:"ἐ";s:3:"ἐ";s:4:"ἑ";s:3:"ἑ";s:5:"ἒ";s:3:"ἒ";s:5:"ἓ";s:3:"ἓ";s:5:"ἔ";s:3:"ἔ";s:5:"ἕ";s:3:"ἕ";s:4:"Ἐ";s:3:"Ἐ";s:4:"Ἑ";s:3:"Ἑ";s:5:"Ἒ";s:3:"Ἒ";s:5:"Ἓ";s:3:"Ἓ";s:5:"Ἔ";s:3:"Ἔ";s:5:"Ἕ";s:3:"Ἕ";s:4:"ἠ";s:3:"ἠ";s:4:"ἡ";s:3:"ἡ";s:5:"ἢ";s:3:"ἢ";s:5:"ἣ";s:3:"ἣ";s:5:"ἤ";s:3:"ἤ";s:5:"ἥ";s:3:"ἥ";s:5:"ἦ";s:3:"ἦ";s:5:"ἧ";s:3:"ἧ";s:4:"Ἠ";s:3:"Ἠ";s:4:"Ἡ";s:3:"Ἡ";s:5:"Ἢ";s:3:"Ἢ";s:5:"Ἣ";s:3:"Ἣ";s:5:"Ἤ";s:3:"Ἤ";s:5:"Ἥ";s:3:"Ἥ";s:5:"Ἦ";s:3:"Ἦ";s:5:"Ἧ";s:3:"Ἧ";s:4:"ἰ";s:3:"ἰ";s:4:"ἱ";s:3:"ἱ";s:5:"ἲ";s:3:"ἲ";s:5:"ἳ";s:3:"ἳ";s:5:"ἴ";s:3:"ἴ";s:5:"ἵ";s:3:"ἵ";s:5:"ἶ";s:3:"ἶ";s:5:"ἷ";s:3:"ἷ";s:4:"Ἰ";s:3:"Ἰ";s:4:"Ἱ";s:3:"Ἱ";s:5:"Ἲ";s:3:"Ἲ";s:5:"Ἳ";s:3:"Ἳ";s:5:"Ἴ";s:3:"Ἴ";s:5:"Ἵ";s:3:"Ἵ";s:5:"Ἶ";s:3:"Ἶ";s:5:"Ἷ";s:3:"Ἷ";s:4:"ὀ";s:3:"ὀ";s:4:"ὁ";s:3:"ὁ";s:5:"ὂ";s:3:"ὂ";s:5:"ὃ";s:3:"ὃ";s:5:"ὄ";s:3:"ὄ";s:5:"ὅ";s:3:"ὅ";s:4:"Ὀ";s:3:"Ὀ";s:4:"Ὁ";s:3:"Ὁ";s:5:"Ὂ";s:3:"Ὂ";s:5:"Ὃ";s:3:"Ὃ";s:5:"Ὄ";s:3:"Ὄ";s:5:"Ὅ";s:3:"Ὅ";s:4:"ὐ";s:3:"ὐ";s:4:"ὑ";s:3:"ὑ";s:5:"ὒ";s:3:"ὒ";s:5:"ὓ";s:3:"ὓ";s:5:"ὔ";s:3:"ὔ";s:5:"ὕ";s:3:"ὕ";s:5:"ὖ";s:3:"ὖ";s:5:"ὗ";s:3:"ὗ";s:4:"Ὑ";s:3:"Ὑ";s:5:"Ὓ";s:3:"Ὓ";s:5:"Ὕ";s:3:"Ὕ";s:5:"Ὗ";s:3:"Ὗ";s:4:"ὠ";s:3:"ὠ";s:4:"ὡ";s:3:"ὡ";s:5:"ὢ";s:3:"ὢ";s:5:"ὣ";s:3:"ὣ";s:5:"ὤ";s:3:"ὤ";s:5:"ὥ";s:3:"ὥ";s:5:"ὦ";s:3:"ὦ";s:5:"ὧ";s:3:"ὧ";s:4:"Ὠ";s:3:"Ὠ";s:4:"Ὡ";s:3:"Ὡ";s:5:"Ὢ";s:3:"Ὢ";s:5:"Ὣ";s:3:"Ὣ";s:5:"Ὤ";s:3:"Ὤ";s:5:"Ὥ";s:3:"Ὥ";s:5:"Ὦ";s:3:"Ὦ";s:5:"Ὧ";s:3:"Ὧ";s:4:"ὰ";s:3:"ὰ";s:2:"ά";s:3:"ά";s:4:"ὲ";s:3:"ὲ";s:2:"έ";s:3:"έ";s:4:"ὴ";s:3:"ὴ";s:2:"ή";s:3:"ή";s:4:"ὶ";s:3:"ὶ";s:2:"ί";s:3:"ί";s:4:"ὸ";s:3:"ὸ";s:2:"ό";s:3:"ό";s:4:"ὺ";s:3:"ὺ";s:2:"ύ";s:3:"ύ";s:4:"ὼ";s:3:"ὼ";s:2:"ώ";s:3:"ώ";s:5:"ᾀ";s:3:"ᾀ";s:5:"ᾁ";s:3:"ᾁ";s:5:"ᾂ";s:3:"ᾂ";s:5:"ᾃ";s:3:"ᾃ";s:5:"ᾄ";s:3:"ᾄ";s:5:"ᾅ";s:3:"ᾅ";s:5:"ᾆ";s:3:"ᾆ";s:5:"ᾇ";s:3:"ᾇ";s:5:"ᾈ";s:3:"ᾈ";s:5:"ᾉ";s:3:"ᾉ";s:5:"ᾊ";s:3:"ᾊ";s:5:"ᾋ";s:3:"ᾋ";s:5:"ᾌ";s:3:"ᾌ";s:5:"ᾍ";s:3:"ᾍ";s:5:"ᾎ";s:3:"ᾎ";s:5:"ᾏ";s:3:"ᾏ";s:5:"ᾐ";s:3:"ᾐ";s:5:"ᾑ";s:3:"ᾑ";s:5:"ᾒ";s:3:"ᾒ";s:5:"ᾓ";s:3:"ᾓ";s:5:"ᾔ";s:3:"ᾔ";s:5:"ᾕ";s:3:"ᾕ";s:5:"ᾖ";s:3:"ᾖ";s:5:"ᾗ";s:3:"ᾗ";s:5:"ᾘ";s:3:"ᾘ";s:5:"ᾙ";s:3:"ᾙ";s:5:"ᾚ";s:3:"ᾚ";s:5:"ᾛ";s:3:"ᾛ";s:5:"ᾜ";s:3:"ᾜ";s:5:"ᾝ";s:3:"ᾝ";s:5:"ᾞ";s:3:"ᾞ";s:5:"ᾟ";s:3:"ᾟ";s:5:"ᾠ";s:3:"ᾠ";s:5:"ᾡ";s:3:"ᾡ";s:5:"ᾢ";s:3:"ᾢ";s:5:"ᾣ";s:3:"ᾣ";s:5:"ᾤ";s:3:"ᾤ";s:5:"ᾥ";s:3:"ᾥ";s:5:"ᾦ";s:3:"ᾦ";s:5:"ᾧ";s:3:"ᾧ";s:5:"ᾨ";s:3:"ᾨ";s:5:"ᾩ";s:3:"ᾩ";s:5:"ᾪ";s:3:"ᾪ";s:5:"ᾫ";s:3:"ᾫ";s:5:"ᾬ";s:3:"ᾬ";s:5:"ᾭ";s:3:"ᾭ";s:5:"ᾮ";s:3:"ᾮ";s:5:"ᾯ";s:3:"ᾯ";s:4:"ᾰ";s:3:"ᾰ";s:4:"ᾱ";s:3:"ᾱ";s:5:"ᾲ";s:3:"ᾲ";s:4:"ᾳ";s:3:"ᾳ";s:4:"ᾴ";s:3:"ᾴ";s:4:"ᾶ";s:3:"ᾶ";s:5:"ᾷ";s:3:"ᾷ";s:4:"Ᾰ";s:3:"Ᾰ";s:4:"Ᾱ";s:3:"Ᾱ";s:4:"Ὰ";s:3:"Ὰ";s:2:"Ά";s:3:"Ά";s:4:"ᾼ";s:3:"ᾼ";s:2:"ι";s:3:"ι";s:4:"῁";s:3:"῁";s:5:"ῂ";s:3:"ῂ";s:4:"ῃ";s:3:"ῃ";s:4:"ῄ";s:3:"ῄ";s:4:"ῆ";s:3:"ῆ";s:5:"ῇ";s:3:"ῇ";s:4:"Ὲ";s:3:"Ὲ";s:2:"Έ";s:3:"Έ";s:4:"Ὴ";s:3:"Ὴ";s:2:"Ή";s:3:"Ή";s:4:"ῌ";s:3:"ῌ";s:5:"῍";s:3:"῍";s:5:"῎";s:3:"῎";s:5:"῏";s:3:"῏";s:4:"ῐ";s:3:"ῐ";s:4:"ῑ";s:3:"ῑ";s:4:"ῒ";s:3:"ῒ";s:2:"ΐ";s:3:"ΐ";s:4:"ῖ";s:3:"ῖ";s:4:"ῗ";s:3:"ῗ";s:4:"Ῐ";s:3:"Ῐ";s:4:"Ῑ";s:3:"Ῑ";s:4:"Ὶ";s:3:"Ὶ";s:2:"Ί";s:3:"Ί";s:5:"῝";s:3:"῝";s:5:"῞";s:3:"῞";s:5:"῟";s:3:"῟";s:4:"ῠ";s:3:"ῠ";s:4:"ῡ";s:3:"ῡ";s:4:"ῢ";s:3:"ῢ";s:2:"ΰ";s:3:"ΰ";s:4:"ῤ";s:3:"ῤ";s:4:"ῥ";s:3:"ῥ";s:4:"ῦ";s:3:"ῦ";s:4:"ῧ";s:3:"ῧ";s:4:"Ῠ";s:3:"Ῠ";s:4:"Ῡ";s:3:"Ῡ";s:4:"Ὺ";s:3:"Ὺ";s:2:"Ύ";s:3:"Ύ";s:4:"Ῥ";s:3:"Ῥ";s:4:"῭";s:3:"῭";s:2:"΅";s:3:"΅";s:1:"`";s:3:"`";s:5:"ῲ";s:3:"ῲ";s:4:"ῳ";s:3:"ῳ";s:4:"ῴ";s:3:"ῴ";s:4:"ῶ";s:3:"ῶ";s:5:"ῷ";s:3:"ῷ";s:4:"Ὸ";s:3:"Ὸ";s:2:"Ό";s:3:"Ό";s:4:"Ὼ";s:3:"Ὼ";s:2:"Ώ";s:3:"Ώ";s:4:"ῼ";s:3:"ῼ";s:2:"´";s:3:"´";s:3:" ";s:3:" ";s:3:" ";s:3:" ";s:2:"Ω";s:3:"Ω";s:1:"K";s:3:"K";s:2:"Å";s:3:"Å";s:5:"↚";s:3:"↚";s:5:"↛";s:3:"↛";s:5:"↮";s:3:"↮";s:5:"⇍";s:3:"⇍";s:5:"⇎";s:3:"⇎";s:5:"⇏";s:3:"⇏";s:5:"∄";s:3:"∄";s:5:"∉";s:3:"∉";s:5:"∌";s:3:"∌";s:5:"∤";s:3:"∤";s:5:"∦";s:3:"∦";s:5:"≁";s:3:"≁";s:5:"≄";s:3:"≄";s:5:"≇";s:3:"≇";s:5:"≉";s:3:"≉";s:3:"≠";s:3:"≠";s:5:"≢";s:3:"≢";s:5:"≭";s:3:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:5:"≰";s:3:"≰";s:5:"≱";s:3:"≱";s:5:"≴";s:3:"≴";s:5:"≵";s:3:"≵";s:5:"≸";s:3:"≸";s:5:"≹";s:3:"≹";s:5:"⊀";s:3:"⊀";s:5:"⊁";s:3:"⊁";s:5:"⊄";s:3:"⊄";s:5:"⊅";s:3:"⊅";s:5:"⊈";s:3:"⊈";s:5:"⊉";s:3:"⊉";s:5:"⊬";s:3:"⊬";s:5:"⊭";s:3:"⊭";s:5:"⊮";s:3:"⊮";s:5:"⊯";s:3:"⊯";s:5:"⋠";s:3:"⋠";s:5:"⋡";s:3:"⋡";s:5:"⋢";s:3:"⋢";s:5:"⋣";s:3:"⋣";s:5:"⋪";s:3:"⋪";s:5:"⋫";s:3:"⋫";s:5:"⋬";s:3:"⋬";s:5:"⋭";s:3:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:6:"が";s:3:"が";s:6:"ぎ";s:3:"ぎ";s:6:"ぐ";s:3:"ぐ";s:6:"げ";s:3:"げ";s:6:"ご";s:3:"ご";s:6:"ざ";s:3:"ざ";s:6:"じ";s:3:"じ";s:6:"ず";s:3:"ず";s:6:"ぜ";s:3:"ぜ";s:6:"ぞ";s:3:"ぞ";s:6:"だ";s:3:"だ";s:6:"ぢ";s:3:"ぢ";s:6:"づ";s:3:"づ";s:6:"で";s:3:"で";s:6:"ど";s:3:"ど";s:6:"ば";s:3:"ば";s:6:"ぱ";s:3:"ぱ";s:6:"び";s:3:"び";s:6:"ぴ";s:3:"ぴ";s:6:"ぶ";s:3:"ぶ";s:6:"ぷ";s:3:"ぷ";s:6:"べ";s:3:"べ";s:6:"ぺ";s:3:"ぺ";s:6:"ぼ";s:3:"ぼ";s:6:"ぽ";s:3:"ぽ";s:6:"ゔ";s:3:"ゔ";s:6:"ゞ";s:3:"ゞ";s:6:"ガ";s:3:"ガ";s:6:"ギ";s:3:"ギ";s:6:"グ";s:3:"グ";s:6:"ゲ";s:3:"ゲ";s:6:"ゴ";s:3:"ゴ";s:6:"ザ";s:3:"ザ";s:6:"ジ";s:3:"ジ";s:6:"ズ";s:3:"ズ";s:6:"ゼ";s:3:"ゼ";s:6:"ゾ";s:3:"ゾ";s:6:"ダ";s:3:"ダ";s:6:"ヂ";s:3:"ヂ";s:6:"ヅ";s:3:"ヅ";s:6:"デ";s:3:"デ";s:6:"ド";s:3:"ド";s:6:"バ";s:3:"バ";s:6:"パ";s:3:"パ";s:6:"ビ";s:3:"ビ";s:6:"ピ";s:3:"ピ";s:6:"ブ";s:3:"ブ";s:6:"プ";s:3:"プ";s:6:"ベ";s:3:"ベ";s:6:"ペ";s:3:"ペ";s:6:"ボ";s:3:"ボ";s:6:"ポ";s:3:"ポ";s:6:"ヴ";s:3:"ヴ";s:6:"ヷ";s:3:"ヷ";s:6:"ヸ";s:3:"ヸ";s:6:"ヹ";s:3:"ヹ";s:6:"ヺ";s:3:"ヺ";s:6:"ヾ";s:3:"ヾ";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:4:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:4:"廊";s:3:"朗";s:4:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:4:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:4:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:4:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:4:"異";s:3:"北";s:4:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:4:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:4:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:4:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:4:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:4:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:4:"侮";s:3:"僧";s:4:"僧";s:3:"免";s:4:"免";s:3:"勉";s:4:"勉";s:3:"勤";s:4:"勤";s:3:"卑";s:4:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:4:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:4:"屮";s:3:"悔";s:4:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:4:"憎";s:3:"懲";s:4:"懲";s:3:"敏";s:4:"敏";s:3:"既";s:3:"既";s:3:"暑";s:4:"暑";s:3:"梅";s:4:"梅";s:3:"海";s:4:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:4:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:4:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:4:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"著";s:4:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:4:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:4:"勇";s:3:"勺";s:4:"勺";s:3:"啕";s:3:"啕";s:3:"喙";s:4:"喙";s:3:"嗢";s:3:"嗢";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:4:"慎";s:3:"愈";s:3:"愈";s:3:"慠";s:3:"慠";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"望";s:4:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"滛";s:3:"滛";s:3:"滋";s:4:"滋";s:3:"瀞";s:4:"瀞";s:3:"瞧";s:3:"瞧";s:3:"爵";s:4:"爵";s:3:"犯";s:3:"犯";s:3:"瑱";s:4:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"盛";s:3:"盛";s:3:"直";s:4:"直";s:3:"睊";s:4:"睊";s:3:"着";s:3:"着";s:3:"磌";s:4:"磌";s:3:"窱";s:3:"窱";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"缾";s:3:"缾";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:4:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"調";s:3:"調";s:3:"請";s:3:"請";s:3:"諭";s:4:"諭";s:3:"變";s:4:"變";s:3:"輸";s:4:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"韛";s:3:"韛";s:3:"頋";s:4:"頋";s:3:"鬒";s:4:"鬒";s:4:"𢡊";s:3:"𢡊";s:4:"𢡄";s:3:"𢡄";s:4:"𣏕";s:3:"𣏕";s:3:"㮝";s:4:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:4:"䀹";s:4:"𥉉";s:3:"𥉉";s:4:"𥳐";s:3:"𥳐";s:4:"𧻓";s:3:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"丽";s:4:"丽";s:3:"丸";s:4:"丸";s:3:"乁";s:4:"乁";s:4:"𠄢";s:4:"𠄢";s:3:"你";s:4:"你";s:3:"侻";s:4:"侻";s:3:"倂";s:4:"倂";s:3:"偺";s:4:"偺";s:3:"備";s:4:"備";s:3:"像";s:4:"像";s:3:"㒞";s:4:"㒞";s:4:"𠘺";s:4:"𠘺";s:3:"兔";s:4:"兔";s:3:"兤";s:4:"兤";s:3:"具";s:4:"具";s:4:"𠔜";s:4:"𠔜";s:3:"㒹";s:4:"㒹";s:3:"內";s:4:"內";s:3:"再";s:4:"再";s:4:"𠕋";s:4:"𠕋";s:3:"冗";s:4:"冗";s:3:"冤";s:4:"冤";s:3:"仌";s:4:"仌";s:3:"冬";s:4:"冬";s:4:"𩇟";s:4:"𩇟";s:3:"凵";s:4:"凵";s:3:"刃";s:4:"刃";s:3:"㓟";s:4:"㓟";s:3:"刻";s:4:"刻";s:3:"剆";s:4:"剆";s:3:"割";s:4:"割";s:3:"剷";s:4:"剷";s:3:"㔕";s:4:"㔕";s:3:"包";s:4:"包";s:3:"匆";s:4:"匆";s:3:"卉";s:4:"卉";s:3:"博";s:4:"博";s:3:"即";s:4:"即";s:3:"卽";s:4:"卽";s:3:"卿";s:4:"卿";s:4:"𠨬";s:4:"𠨬";s:3:"灰";s:4:"灰";s:3:"及";s:4:"及";s:3:"叟";s:4:"叟";s:4:"𠭣";s:4:"𠭣";s:3:"叫";s:4:"叫";s:3:"叱";s:4:"叱";s:3:"吆";s:4:"吆";s:3:"咞";s:4:"咞";s:3:"吸";s:4:"吸";s:3:"呈";s:4:"呈";s:3:"周";s:4:"周";s:3:"咢";s:4:"咢";s:3:"哶";s:4:"哶";s:3:"唐";s:4:"唐";s:3:"啓";s:4:"啓";s:3:"啣";s:4:"啣";s:3:"善";s:4:"善";s:3:"喫";s:4:"喫";s:3:"喳";s:4:"喳";s:3:"嗂";s:4:"嗂";s:3:"圖";s:4:"圖";s:3:"圗";s:4:"圗";s:3:"噑";s:4:"噑";s:3:"噴";s:4:"噴";s:3:"壮";s:4:"壮";s:3:"城";s:4:"城";s:3:"埴";s:4:"埴";s:3:"堍";s:4:"堍";s:3:"型";s:4:"型";s:3:"堲";s:4:"堲";s:3:"報";s:4:"報";s:3:"墬";s:4:"墬";s:4:"𡓤";s:4:"𡓤";s:3:"売";s:4:"売";s:3:"壷";s:4:"壷";s:3:"夆";s:4:"夆";s:3:"多";s:4:"多";s:3:"夢";s:4:"夢";s:3:"奢";s:4:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:3:"姬";s:4:"姬";s:3:"娛";s:4:"娛";s:3:"娧";s:4:"娧";s:3:"姘";s:4:"姘";s:3:"婦";s:4:"婦";s:3:"㛮";s:4:"㛮";s:3:"㛼";s:4:"㛼";s:3:"嬈";s:4:"嬈";s:3:"嬾";s:4:"嬾";s:4:"𡧈";s:4:"𡧈";s:3:"寃";s:4:"寃";s:3:"寘";s:4:"寘";s:3:"寳";s:4:"寳";s:4:"𡬘";s:4:"𡬘";s:3:"寿";s:4:"寿";s:3:"将";s:4:"将";s:3:"当";s:4:"当";s:3:"尢";s:4:"尢";s:3:"㞁";s:4:"㞁";s:3:"屠";s:4:"屠";s:3:"峀";s:4:"峀";s:3:"岍";s:4:"岍";s:4:"𡷤";s:4:"𡷤";s:3:"嵃";s:4:"嵃";s:4:"𡷦";s:4:"𡷦";s:3:"嵮";s:4:"嵮";s:3:"嵫";s:4:"嵫";s:3:"嵼";s:4:"嵼";s:3:"巡";s:4:"巡";s:3:"巢";s:4:"巢";s:3:"㠯";s:4:"㠯";s:3:"巽";s:4:"巽";s:3:"帨";s:4:"帨";s:3:"帽";s:4:"帽";s:3:"幩";s:4:"幩";s:3:"㡢";s:4:"㡢";s:4:"𢆃";s:4:"𢆃";s:3:"㡼";s:4:"㡼";s:3:"庰";s:4:"庰";s:3:"庳";s:4:"庳";s:3:"庶";s:4:"庶";s:4:"𪎒";s:4:"𪎒";s:3:"廾";s:4:"廾";s:4:"𢌱";s:4:"𢌱";s:3:"舁";s:4:"舁";s:3:"弢";s:4:"弢";s:3:"㣇";s:4:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:3:"形";s:4:"形";s:3:"彫";s:4:"彫";s:3:"㣣";s:4:"㣣";s:3:"徚";s:4:"徚";s:3:"忍";s:4:"忍";s:3:"志";s:4:"志";s:3:"忹";s:4:"忹";s:3:"悁";s:4:"悁";s:3:"㤺";s:4:"㤺";s:3:"㤜";s:4:"㤜";s:4:"𢛔";s:4:"𢛔";s:3:"惇";s:4:"惇";s:3:"慈";s:4:"慈";s:3:"慌";s:4:"慌";s:3:"慺";s:4:"慺";s:3:"憲";s:4:"憲";s:3:"憤";s:4:"憤";s:3:"憯";s:4:"憯";s:3:"懞";s:4:"懞";s:3:"成";s:4:"成";s:3:"戛";s:4:"戛";s:3:"扝";s:4:"扝";s:3:"抱";s:4:"抱";s:3:"拔";s:4:"拔";s:3:"捐";s:4:"捐";s:4:"𢬌";s:4:"𢬌";s:3:"挽";s:4:"挽";s:3:"拼";s:4:"拼";s:3:"捨";s:4:"捨";s:3:"掃";s:4:"掃";s:3:"揤";s:4:"揤";s:4:"𢯱";s:4:"𢯱";s:3:"搢";s:4:"搢";s:3:"揅";s:4:"揅";s:3:"掩";s:4:"掩";s:3:"㨮";s:4:"㨮";s:3:"摩";s:4:"摩";s:3:"摾";s:4:"摾";s:3:"撝";s:4:"撝";s:3:"摷";s:4:"摷";s:3:"㩬";s:4:"㩬";s:3:"敬";s:4:"敬";s:4:"𣀊";s:4:"𣀊";s:3:"旣";s:4:"旣";s:3:"書";s:4:"書";s:3:"晉";s:4:"晉";s:3:"㬙";s:4:"㬙";s:3:"㬈";s:4:"㬈";s:3:"㫤";s:4:"㫤";s:3:"冒";s:4:"冒";s:3:"冕";s:4:"冕";s:3:"最";s:4:"最";s:3:"暜";s:4:"暜";s:3:"肭";s:4:"肭";s:3:"䏙";s:4:"䏙";s:3:"朡";s:4:"朡";s:3:"杞";s:4:"杞";s:3:"杓";s:4:"杓";s:4:"𣏃";s:4:"𣏃";s:3:"㭉";s:4:"㭉";s:3:"柺";s:4:"柺";s:3:"枅";s:4:"枅";s:3:"桒";s:4:"桒";s:4:"𣑭";s:4:"𣑭";s:3:"梎";s:4:"梎";s:3:"栟";s:4:"栟";s:3:"椔";s:4:"椔";s:3:"楂";s:4:"楂";s:3:"榣";s:4:"榣";s:3:"槪";s:4:"槪";s:3:"檨";s:4:"檨";s:4:"𣚣";s:4:"𣚣";s:3:"櫛";s:4:"櫛";s:3:"㰘";s:4:"㰘";s:3:"次";s:4:"次";s:4:"𣢧";s:4:"𣢧";s:3:"歔";s:4:"歔";s:3:"㱎";s:4:"㱎";s:3:"歲";s:4:"歲";s:3:"殟";s:4:"殟";s:3:"殻";s:4:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:3:"汎";s:4:"汎";s:4:"𣲼";s:4:"𣲼";s:3:"沿";s:4:"沿";s:3:"泍";s:4:"泍";s:3:"汧";s:4:"汧";s:3:"洖";s:4:"洖";s:3:"派";s:4:"派";s:3:"浩";s:4:"浩";s:3:"浸";s:4:"浸";s:3:"涅";s:4:"涅";s:4:"𣴞";s:4:"𣴞";s:3:"洴";s:4:"洴";s:3:"港";s:4:"港";s:3:"湮";s:4:"湮";s:3:"㴳";s:4:"㴳";s:3:"滇";s:4:"滇";s:4:"𣻑";s:4:"𣻑";s:3:"淹";s:4:"淹";s:3:"潮";s:4:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:3:"濆";s:4:"濆";s:3:"瀹";s:4:"瀹";s:3:"瀛";s:4:"瀛";s:3:"㶖";s:4:"㶖";s:3:"灊";s:4:"灊";s:3:"災";s:4:"災";s:3:"灷";s:4:"灷";s:3:"炭";s:4:"炭";s:4:"𠔥";s:4:"𠔥";s:3:"煅";s:4:"煅";s:4:"𤉣";s:4:"𤉣";s:3:"熜";s:4:"熜";s:4:"𤎫";s:4:"𤎫";s:3:"爨";s:4:"爨";s:3:"牐";s:4:"牐";s:4:"𤘈";s:4:"𤘈";s:3:"犀";s:4:"犀";s:3:"犕";s:4:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:3:"獺";s:4:"獺";s:3:"王";s:4:"王";s:3:"㺬";s:4:"㺬";s:3:"玥";s:4:"玥";s:3:"㺸";s:4:"㺸";s:3:"瑇";s:4:"瑇";s:3:"瑜";s:4:"瑜";s:3:"璅";s:4:"璅";s:3:"瓊";s:4:"瓊";s:3:"㼛";s:4:"㼛";s:3:"甤";s:4:"甤";s:4:"𤰶";s:4:"𤰶";s:3:"甾";s:4:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"𢆟";s:4:"𢆟";s:3:"瘐";s:4:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:3:"㿼";s:4:"㿼";s:3:"䀈";s:4:"䀈";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:3:"眞";s:4:"眞";s:3:"真";s:4:"真";s:3:"瞋";s:4:"瞋";s:3:"䁆";s:4:"䁆";s:3:"䂖";s:4:"䂖";s:4:"𥐝";s:4:"𥐝";s:3:"硎";s:4:"硎";s:3:"䃣";s:4:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:3:"秫";s:4:"秫";s:3:"䄯";s:4:"䄯";s:3:"穊";s:4:"穊";s:3:"穏";s:4:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:3:"竮";s:4:"竮";s:3:"䈂";s:4:"䈂";s:4:"𥮫";s:4:"𥮫";s:3:"篆";s:4:"篆";s:3:"築";s:4:"築";s:3:"䈧";s:4:"䈧";s:4:"𥲀";s:4:"𥲀";s:3:"糒";s:4:"糒";s:3:"䊠";s:4:"䊠";s:3:"糨";s:4:"糨";s:3:"糣";s:4:"糣";s:3:"紀";s:4:"紀";s:4:"𥾆";s:4:"𥾆";s:3:"絣";s:4:"絣";s:3:"䌁";s:4:"䌁";s:3:"緇";s:4:"緇";s:3:"縂";s:4:"縂";s:3:"繅";s:4:"繅";s:3:"䌴";s:4:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:3:"䍙";s:4:"䍙";s:4:"𦋙";s:4:"𦋙";s:3:"罺";s:4:"罺";s:4:"𦌾";s:4:"𦌾";s:3:"羕";s:4:"羕";s:3:"翺";s:4:"翺";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:3:"聠";s:4:"聠";s:4:"𦖨";s:4:"𦖨";s:3:"聰";s:4:"聰";s:4:"𣍟";s:4:"𣍟";s:3:"䏕";s:4:"䏕";s:3:"育";s:4:"育";s:3:"脃";s:4:"脃";s:3:"䐋";s:4:"䐋";s:3:"脾";s:4:"脾";s:3:"媵";s:4:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:3:"舄";s:4:"舄";s:3:"辞";s:4:"辞";s:3:"䑫";s:4:"䑫";s:3:"芑";s:4:"芑";s:3:"芋";s:4:"芋";s:3:"芝";s:4:"芝";s:3:"劳";s:4:"劳";s:3:"花";s:4:"花";s:3:"芳";s:4:"芳";s:3:"芽";s:4:"芽";s:3:"苦";s:4:"苦";s:4:"𦬼";s:4:"𦬼";s:3:"茝";s:4:"茝";s:3:"荣";s:4:"荣";s:3:"莭";s:4:"莭";s:3:"茣";s:4:"茣";s:3:"莽";s:4:"莽";s:3:"菧";s:4:"菧";s:3:"荓";s:4:"荓";s:3:"菊";s:4:"菊";s:3:"菌";s:4:"菌";s:3:"菜";s:4:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:3:"䔫";s:4:"䔫";s:3:"蓱";s:4:"蓱";s:3:"蓳";s:4:"蓳";s:3:"蔖";s:4:"蔖";s:4:"𧏊";s:4:"𧏊";s:3:"蕤";s:4:"蕤";s:4:"𦼬";s:4:"𦼬";s:3:"䕝";s:4:"䕝";s:3:"䕡";s:4:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:3:"䕫";s:4:"䕫";s:3:"虐";s:4:"虐";s:3:"虧";s:4:"虧";s:3:"虩";s:4:"虩";s:3:"蚩";s:4:"蚩";s:3:"蚈";s:4:"蚈";s:3:"蜎";s:4:"蜎";s:3:"蛢";s:4:"蛢";s:3:"蜨";s:4:"蜨";s:3:"蝫";s:4:"蝫";s:3:"螆";s:4:"螆";s:3:"䗗";s:4:"䗗";s:3:"蟡";s:4:"蟡";s:3:"蠁";s:4:"蠁";s:3:"䗹";s:4:"䗹";s:3:"衠";s:4:"衠";s:3:"衣";s:4:"衣";s:4:"𧙧";s:4:"𧙧";s:3:"裗";s:4:"裗";s:3:"裞";s:4:"裞";s:3:"䘵";s:4:"䘵";s:3:"裺";s:4:"裺";s:3:"㒻";s:4:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:3:"䚾";s:4:"䚾";s:3:"䛇";s:4:"䛇";s:3:"誠";s:4:"誠";s:3:"豕";s:4:"豕";s:4:"𧲨";s:4:"𧲨";s:3:"貫";s:4:"貫";s:3:"賁";s:4:"賁";s:3:"贛";s:4:"贛";s:3:"起";s:4:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:3:"跋";s:4:"跋";s:3:"趼";s:4:"趼";s:3:"跰";s:4:"跰";s:4:"𠣞";s:4:"𠣞";s:3:"軔";s:4:"軔";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:3:"邔";s:4:"邔";s:3:"郱";s:4:"郱";s:3:"鄑";s:4:"鄑";s:4:"𨜮";s:4:"𨜮";s:3:"鄛";s:4:"鄛";s:3:"鈸";s:4:"鈸";s:3:"鋗";s:4:"鋗";s:3:"鋘";s:4:"鋘";s:3:"鉼";s:4:"鉼";s:3:"鏹";s:4:"鏹";s:3:"鐕";s:4:"鐕";s:4:"𨯺";s:4:"𨯺";s:3:"開";s:4:"開";s:3:"䦕";s:4:"䦕";s:3:"閷";s:4:"閷";s:4:"𨵷";s:4:"𨵷";s:3:"䧦";s:4:"䧦";s:3:"雃";s:4:"雃";s:3:"嶲";s:4:"嶲";s:3:"霣";s:4:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:3:"䩮";s:4:"䩮";s:3:"䩶";s:4:"䩶";s:3:"韠";s:4:"韠";s:4:"𩐊";s:4:"𩐊";s:3:"䪲";s:4:"䪲";s:4:"𩒖";s:4:"𩒖";s:3:"頩";s:4:"頩";s:4:"𩖶";s:4:"𩖶";s:3:"飢";s:4:"飢";s:3:"䬳";s:4:"䬳";s:3:"餩";s:4:"餩";s:3:"馧";s:4:"馧";s:3:"駂";s:4:"駂";s:3:"駾";s:4:"駾";s:3:"䯎";s:4:"䯎";s:4:"𩬰";s:4:"𩬰";s:3:"鱀";s:4:"鱀";s:3:"鳽";s:4:"鳽";s:3:"䳎";s:4:"䳎";s:3:"䳭";s:4:"䳭";s:3:"鵧";s:4:"鵧";s:4:"𪃎";s:4:"𪃎";s:3:"䳸";s:4:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:3:"麻";s:4:"麻";s:3:"䵖";s:4:"䵖";s:3:"黹";s:4:"黹";s:3:"黾";s:4:"黾";s:3:"鼅";s:4:"鼅";s:3:"鼏";s:4:"鼏";s:3:"鼖";s:4:"鼖";s:3:"鼻";s:4:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
+$utfCanonicalDecomp = unserialize( 'a:2032:{s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s:3:"Ǐ";s:2:"ǐ";s:3:"ǐ";s:2:"Ǒ";s:3:"Ǒ";s:2:"ǒ";s:3:"ǒ";s:2:"Ǔ";s:3:"Ǔ";s:2:"ǔ";s:3:"ǔ";s:2:"Ǖ";s:5:"Ǖ";s:2:"ǖ";s:5:"ǖ";s:2:"Ǘ";s:5:"Ǘ";s:2:"ǘ";s:5:"ǘ";s:2:"Ǚ";s:5:"Ǚ";s:2:"ǚ";s:5:"ǚ";s:2:"Ǜ";s:5:"Ǜ";s:2:"ǜ";s:5:"ǜ";s:2:"Ǟ";s:5:"Ǟ";s:2:"ǟ";s:5:"ǟ";s:2:"Ǡ";s:5:"Ǡ";s:2:"ǡ";s:5:"ǡ";s:2:"Ǣ";s:4:"Ǣ";s:2:"ǣ";s:4:"ǣ";s:2:"Ǧ";s:3:"Ǧ";s:2:"ǧ";s:3:"ǧ";s:2:"Ǩ";s:3:"Ǩ";s:2:"ǩ";s:3:"ǩ";s:2:"Ǫ";s:3:"Ǫ";s:2:"ǫ";s:3:"ǫ";s:2:"Ǭ";s:5:"Ǭ";s:2:"ǭ";s:5:"ǭ";s:2:"Ǯ";s:4:"Ǯ";s:2:"ǯ";s:4:"ǯ";s:2:"ǰ";s:3:"ǰ";s:2:"Ǵ";s:3:"Ǵ";s:2:"ǵ";s:3:"ǵ";s:2:"Ǹ";s:3:"Ǹ";s:2:"ǹ";s:3:"ǹ";s:2:"Ǻ";s:5:"Ǻ";s:2:"ǻ";s:5:"ǻ";s:2:"Ǽ";s:4:"Ǽ";s:2:"ǽ";s:4:"ǽ";s:2:"Ǿ";s:4:"Ǿ";s:2:"ǿ";s:4:"ǿ";s:2:"Ȁ";s:3:"Ȁ";s:2:"ȁ";s:3:"ȁ";s:2:"Ȃ";s:3:"Ȃ";s:2:"ȃ";s:3:"ȃ";s:2:"Ȅ";s:3:"Ȅ";s:2:"ȅ";s:3:"ȅ";s:2:"Ȇ";s:3:"Ȇ";s:2:"ȇ";s:3:"ȇ";s:2:"Ȉ";s:3:"Ȉ";s:2:"ȉ";s:3:"ȉ";s:2:"Ȋ";s:3:"Ȋ";s:2:"ȋ";s:3:"ȋ";s:2:"Ȍ";s:3:"Ȍ";s:2:"ȍ";s:3:"ȍ";s:2:"Ȏ";s:3:"Ȏ";s:2:"ȏ";s:3:"ȏ";s:2:"Ȑ";s:3:"Ȑ";s:2:"ȑ";s:3:"ȑ";s:2:"Ȓ";s:3:"Ȓ";s:2:"ȓ";s:3:"ȓ";s:2:"Ȕ";s:3:"Ȕ";s:2:"ȕ";s:3:"ȕ";s:2:"Ȗ";s:3:"Ȗ";s:2:"ȗ";s:3:"ȗ";s:2:"Ș";s:3:"Ș";s:2:"ș";s:3:"ș";s:2:"Ț";s:3:"Ț";s:2:"ț";s:3:"ț";s:2:"Ȟ";s:3:"Ȟ";s:2:"ȟ";s:3:"ȟ";s:2:"Ȧ";s:3:"Ȧ";s:2:"ȧ";s:3:"ȧ";s:2:"Ȩ";s:3:"Ȩ";s:2:"ȩ";s:3:"ȩ";s:2:"Ȫ";s:5:"Ȫ";s:2:"ȫ";s:5:"ȫ";s:2:"Ȭ";s:5:"Ȭ";s:2:"ȭ";s:5:"ȭ";s:2:"Ȯ";s:3:"Ȯ";s:2:"ȯ";s:3:"ȯ";s:2:"Ȱ";s:5:"Ȱ";s:2:"ȱ";s:5:"ȱ";s:2:"Ȳ";s:3:"Ȳ";s:2:"ȳ";s:3:"ȳ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:2:"̈́";s:4:"̈́";s:2:"ʹ";s:2:"ʹ";s:2:";";s:1:";";s:2:"΅";s:4:"΅";s:2:"Ά";s:4:"Ά";s:2:"·";s:2:"·";s:2:"Έ";s:4:"Έ";s:2:"Ή";s:4:"Ή";s:2:"Ί";s:4:"Ί";s:2:"Ό";s:4:"Ό";s:2:"Ύ";s:4:"Ύ";s:2:"Ώ";s:4:"Ώ";s:2:"ΐ";s:6:"ΐ";s:2:"Ϊ";s:4:"Ϊ";s:2:"Ϋ";s:4:"Ϋ";s:2:"ά";s:4:"ά";s:2:"έ";s:4:"έ";s:2:"ή";s:4:"ή";s:2:"ί";s:4:"ί";s:2:"ΰ";s:6:"ΰ";s:2:"ϊ";s:4:"ϊ";s:2:"ϋ";s:4:"ϋ";s:2:"ό";s:4:"ό";s:2:"ύ";s:4:"ύ";s:2:"ώ";s:4:"ώ";s:2:"ϓ";s:4:"ϓ";s:2:"ϔ";s:4:"ϔ";s:2:"Ѐ";s:4:"Ѐ";s:2:"Ё";s:4:"Ё";s:2:"Ѓ";s:4:"Ѓ";s:2:"Ї";s:4:"Ї";s:2:"Ќ";s:4:"Ќ";s:2:"Ѝ";s:4:"Ѝ";s:2:"Ў";s:4:"Ў";s:2:"Й";s:4:"Й";s:2:"й";s:4:"й";s:2:"ѐ";s:4:"ѐ";s:2:"ё";s:4:"ё";s:2:"ѓ";s:4:"ѓ";s:2:"ї";s:4:"ї";s:2:"ќ";s:4:"ќ";s:2:"ѝ";s:4:"ѝ";s:2:"ў";s:4:"ў";s:2:"Ѷ";s:4:"Ѷ";s:2:"ѷ";s:4:"ѷ";s:2:"Ӂ";s:4:"Ӂ";s:2:"ӂ";s:4:"ӂ";s:2:"Ӑ";s:4:"Ӑ";s:2:"ӑ";s:4:"ӑ";s:2:"Ӓ";s:4:"Ӓ";s:2:"ӓ";s:4:"ӓ";s:2:"Ӗ";s:4:"Ӗ";s:2:"ӗ";s:4:"ӗ";s:2:"Ӛ";s:4:"Ӛ";s:2:"ӛ";s:4:"ӛ";s:2:"Ӝ";s:4:"Ӝ";s:2:"ӝ";s:4:"ӝ";s:2:"Ӟ";s:4:"Ӟ";s:2:"ӟ";s:4:"ӟ";s:2:"Ӣ";s:4:"Ӣ";s:2:"ӣ";s:4:"ӣ";s:2:"Ӥ";s:4:"Ӥ";s:2:"ӥ";s:4:"ӥ";s:2:"Ӧ";s:4:"Ӧ";s:2:"ӧ";s:4:"ӧ";s:2:"Ӫ";s:4:"Ӫ";s:2:"ӫ";s:4:"ӫ";s:2:"Ӭ";s:4:"Ӭ";s:2:"ӭ";s:4:"ӭ";s:2:"Ӯ";s:4:"Ӯ";s:2:"ӯ";s:4:"ӯ";s:2:"Ӱ";s:4:"Ӱ";s:2:"ӱ";s:4:"ӱ";s:2:"Ӳ";s:4:"Ӳ";s:2:"ӳ";s:4:"ӳ";s:2:"Ӵ";s:4:"Ӵ";s:2:"ӵ";s:4:"ӵ";s:2:"Ӹ";s:4:"Ӹ";s:2:"ӹ";s:4:"ӹ";s:2:"آ";s:4:"آ";s:2:"أ";s:4:"أ";s:2:"ؤ";s:4:"ؤ";s:2:"إ";s:4:"إ";s:2:"ئ";s:4:"ئ";s:2:"ۀ";s:4:"ۀ";s:2:"ۂ";s:4:"ۂ";s:2:"ۓ";s:4:"ۓ";s:3:"ऩ";s:6:"ऩ";s:3:"ऱ";s:6:"ऱ";s:3:"ऴ";s:6:"ऴ";s:3:"क़";s:6:"क़";s:3:"ख़";s:6:"ख़";s:3:"ग़";s:6:"ग़";s:3:"ज़";s:6:"ज़";s:3:"ड़";s:6:"ड़";s:3:"ढ़";s:6:"ढ़";s:3:"फ़";s:6:"फ़";s:3:"य़";s:6:"य़";s:3:"ো";s:6:"ো";s:3:"ৌ";s:6:"ৌ";s:3:"ড়";s:6:"ড়";s:3:"ঢ়";s:6:"ঢ়";s:3:"য়";s:6:"য়";s:3:"ਲ਼";s:6:"ਲ਼";s:3:"ਸ਼";s:6:"ਸ਼";s:3:"ਖ਼";s:6:"ਖ਼";s:3:"ਗ਼";s:6:"ਗ਼";s:3:"ਜ਼";s:6:"ਜ਼";s:3:"ਫ਼";s:6:"ਫ਼";s:3:"ୈ";s:6:"ୈ";s:3:"ୋ";s:6:"ୋ";s:3:"ୌ";s:6:"ୌ";s:3:"ଡ଼";s:6:"ଡ଼";s:3:"ଢ଼";s:6:"ଢ଼";s:3:"ஔ";s:6:"ஔ";s:3:"ொ";s:6:"ொ";s:3:"ோ";s:6:"ோ";s:3:"ௌ";s:6:"ௌ";s:3:"ై";s:6:"ై";s:3:"ೀ";s:6:"ೀ";s:3:"ೇ";s:6:"ೇ";s:3:"ೈ";s:6:"ೈ";s:3:"ೊ";s:6:"ೊ";s:3:"ೋ";s:9:"ೋ";s:3:"ൊ";s:6:"ൊ";s:3:"ോ";s:6:"ോ";s:3:"ൌ";s:6:"ൌ";s:3:"ේ";s:6:"ේ";s:3:"ො";s:6:"ො";s:3:"ෝ";s:9:"ෝ";s:3:"ෞ";s:6:"ෞ";s:3:"གྷ";s:6:"གྷ";s:3:"ཌྷ";s:6:"ཌྷ";s:3:"དྷ";s:6:"དྷ";s:3:"བྷ";s:6:"བྷ";s:3:"ཛྷ";s:6:"ཛྷ";s:3:"ཀྵ";s:6:"ཀྵ";s:3:"ཱི";s:6:"ཱི";s:3:"ཱུ";s:6:"ཱུ";s:3:"ྲྀ";s:6:"ྲྀ";s:3:"ླྀ";s:6:"ླྀ";s:3:"ཱྀ";s:6:"ཱྀ";s:3:"ྒྷ";s:6:"ྒྷ";s:3:"ྜྷ";s:6:"ྜྷ";s:3:"ྡྷ";s:6:"ྡྷ";s:3:"ྦྷ";s:6:"ྦྷ";s:3:"ྫྷ";s:6:"ྫྷ";s:3:"ྐྵ";s:6:"ྐྵ";s:3:"ဦ";s:6:"ဦ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:3:"Ḉ";s:5:"Ḉ";s:3:"ḉ";s:5:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:3:"Ḕ";s:5:"Ḕ";s:3:"ḕ";s:5:"ḕ";s:3:"Ḗ";s:5:"Ḗ";s:3:"ḗ";s:5:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:3:"Ḝ";s:5:"Ḝ";s:3:"ḝ";s:5:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:3:"Ḯ";s:5:"Ḯ";s:3:"ḯ";s:5:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:3:"Ḹ";s:5:"Ḹ";s:3:"ḹ";s:5:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:3:"Ṍ";s:5:"Ṍ";s:3:"ṍ";s:5:"ṍ";s:3:"Ṏ";s:5:"Ṏ";s:3:"ṏ";s:5:"ṏ";s:3:"Ṑ";s:5:"Ṑ";s:3:"ṑ";s:5:"ṑ";s:3:"Ṓ";s:5:"Ṓ";s:3:"ṓ";s:5:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:3:"Ṝ";s:5:"Ṝ";s:3:"ṝ";s:5:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:3:"Ṥ";s:5:"Ṥ";s:3:"ṥ";s:5:"ṥ";s:3:"Ṧ";s:5:"Ṧ";s:3:"ṧ";s:5:"ṧ";s:3:"Ṩ";s:5:"Ṩ";s:3:"ṩ";s:5:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:3:"Ṹ";s:5:"Ṹ";s:3:"ṹ";s:5:"ṹ";s:3:"Ṻ";s:5:"Ṻ";s:3:"ṻ";s:5:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:3:"ẛ";s:4:"ẛ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:3:"Ấ";s:5:"Ấ";s:3:"ấ";s:5:"ấ";s:3:"Ầ";s:5:"Ầ";s:3:"ầ";s:5:"ầ";s:3:"Ẩ";s:5:"Ẩ";s:3:"ẩ";s:5:"ẩ";s:3:"Ẫ";s:5:"Ẫ";s:3:"ẫ";s:5:"ẫ";s:3:"Ậ";s:5:"Ậ";s:3:"ậ";s:5:"ậ";s:3:"Ắ";s:5:"Ắ";s:3:"ắ";s:5:"ắ";s:3:"Ằ";s:5:"Ằ";s:3:"ằ";s:5:"ằ";s:3:"Ẳ";s:5:"Ẳ";s:3:"ẳ";s:5:"ẳ";s:3:"Ẵ";s:5:"Ẵ";s:3:"ẵ";s:5:"ẵ";s:3:"Ặ";s:5:"Ặ";s:3:"ặ";s:5:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:3:"Ế";s:5:"Ế";s:3:"ế";s:5:"ế";s:3:"Ề";s:5:"Ề";s:3:"ề";s:5:"ề";s:3:"Ể";s:5:"Ể";s:3:"ể";s:5:"ể";s:3:"Ễ";s:5:"Ễ";s:3:"ễ";s:5:"ễ";s:3:"Ệ";s:5:"Ệ";s:3:"ệ";s:5:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:3:"Ố";s:5:"Ố";s:3:"ố";s:5:"ố";s:3:"Ồ";s:5:"Ồ";s:3:"ồ";s:5:"ồ";s:3:"Ổ";s:5:"Ổ";s:3:"ổ";s:5:"ổ";s:3:"Ỗ";s:5:"Ỗ";s:3:"ỗ";s:5:"ỗ";s:3:"Ộ";s:5:"Ộ";s:3:"ộ";s:5:"ộ";s:3:"Ớ";s:5:"Ớ";s:3:"ớ";s:5:"ớ";s:3:"Ờ";s:5:"Ờ";s:3:"ờ";s:5:"ờ";s:3:"Ở";s:5:"Ở";s:3:"ở";s:5:"ở";s:3:"Ỡ";s:5:"Ỡ";s:3:"ỡ";s:5:"ỡ";s:3:"Ợ";s:5:"Ợ";s:3:"ợ";s:5:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:3:"Ứ";s:5:"Ứ";s:3:"ứ";s:5:"ứ";s:3:"Ừ";s:5:"Ừ";s:3:"ừ";s:5:"ừ";s:3:"Ử";s:5:"Ử";s:3:"ử";s:5:"ử";s:3:"Ữ";s:5:"Ữ";s:3:"ữ";s:5:"ữ";s:3:"Ự";s:5:"Ự";s:3:"ự";s:5:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:3:"ἀ";s:4:"ἀ";s:3:"ἁ";s:4:"ἁ";s:3:"ἂ";s:6:"ἂ";s:3:"ἃ";s:6:"ἃ";s:3:"ἄ";s:6:"ἄ";s:3:"ἅ";s:6:"ἅ";s:3:"ἆ";s:6:"ἆ";s:3:"ἇ";s:6:"ἇ";s:3:"Ἀ";s:4:"Ἀ";s:3:"Ἁ";s:4:"Ἁ";s:3:"Ἂ";s:6:"Ἂ";s:3:"Ἃ";s:6:"Ἃ";s:3:"Ἄ";s:6:"Ἄ";s:3:"Ἅ";s:6:"Ἅ";s:3:"Ἆ";s:6:"Ἆ";s:3:"Ἇ";s:6:"Ἇ";s:3:"ἐ";s:4:"ἐ";s:3:"ἑ";s:4:"ἑ";s:3:"ἒ";s:6:"ἒ";s:3:"ἓ";s:6:"ἓ";s:3:"ἔ";s:6:"ἔ";s:3:"ἕ";s:6:"ἕ";s:3:"Ἐ";s:4:"Ἐ";s:3:"Ἑ";s:4:"Ἑ";s:3:"Ἒ";s:6:"Ἒ";s:3:"Ἓ";s:6:"Ἓ";s:3:"Ἔ";s:6:"Ἔ";s:3:"Ἕ";s:6:"Ἕ";s:3:"ἠ";s:4:"ἠ";s:3:"ἡ";s:4:"ἡ";s:3:"ἢ";s:6:"ἢ";s:3:"ἣ";s:6:"ἣ";s:3:"ἤ";s:6:"ἤ";s:3:"ἥ";s:6:"ἥ";s:3:"ἦ";s:6:"ἦ";s:3:"ἧ";s:6:"ἧ";s:3:"Ἠ";s:4:"Ἠ";s:3:"Ἡ";s:4:"Ἡ";s:3:"Ἢ";s:6:"Ἢ";s:3:"Ἣ";s:6:"Ἣ";s:3:"Ἤ";s:6:"Ἤ";s:3:"Ἥ";s:6:"Ἥ";s:3:"Ἦ";s:6:"Ἦ";s:3:"Ἧ";s:6:"Ἧ";s:3:"ἰ";s:4:"ἰ";s:3:"ἱ";s:4:"ἱ";s:3:"ἲ";s:6:"ἲ";s:3:"ἳ";s:6:"ἳ";s:3:"ἴ";s:6:"ἴ";s:3:"ἵ";s:6:"ἵ";s:3:"ἶ";s:6:"ἶ";s:3:"ἷ";s:6:"ἷ";s:3:"Ἰ";s:4:"Ἰ";s:3:"Ἱ";s:4:"Ἱ";s:3:"Ἲ";s:6:"Ἲ";s:3:"Ἳ";s:6:"Ἳ";s:3:"Ἴ";s:6:"Ἴ";s:3:"Ἵ";s:6:"Ἵ";s:3:"Ἶ";s:6:"Ἶ";s:3:"Ἷ";s:6:"Ἷ";s:3:"ὀ";s:4:"ὀ";s:3:"ὁ";s:4:"ὁ";s:3:"ὂ";s:6:"ὂ";s:3:"ὃ";s:6:"ὃ";s:3:"ὄ";s:6:"ὄ";s:3:"ὅ";s:6:"ὅ";s:3:"Ὀ";s:4:"Ὀ";s:3:"Ὁ";s:4:"Ὁ";s:3:"Ὂ";s:6:"Ὂ";s:3:"Ὃ";s:6:"Ὃ";s:3:"Ὄ";s:6:"Ὄ";s:3:"Ὅ";s:6:"Ὅ";s:3:"ὐ";s:4:"ὐ";s:3:"ὑ";s:4:"ὑ";s:3:"ὒ";s:6:"ὒ";s:3:"ὓ";s:6:"ὓ";s:3:"ὔ";s:6:"ὔ";s:3:"ὕ";s:6:"ὕ";s:3:"ὖ";s:6:"ὖ";s:3:"ὗ";s:6:"ὗ";s:3:"Ὑ";s:4:"Ὑ";s:3:"Ὓ";s:6:"Ὓ";s:3:"Ὕ";s:6:"Ὕ";s:3:"Ὗ";s:6:"Ὗ";s:3:"ὠ";s:4:"ὠ";s:3:"ὡ";s:4:"ὡ";s:3:"ὢ";s:6:"ὢ";s:3:"ὣ";s:6:"ὣ";s:3:"ὤ";s:6:"ὤ";s:3:"ὥ";s:6:"ὥ";s:3:"ὦ";s:6:"ὦ";s:3:"ὧ";s:6:"ὧ";s:3:"Ὠ";s:4:"Ὠ";s:3:"Ὡ";s:4:"Ὡ";s:3:"Ὢ";s:6:"Ὢ";s:3:"Ὣ";s:6:"Ὣ";s:3:"Ὤ";s:6:"Ὤ";s:3:"Ὥ";s:6:"Ὥ";s:3:"Ὦ";s:6:"Ὦ";s:3:"Ὧ";s:6:"Ὧ";s:3:"ὰ";s:4:"ὰ";s:3:"ά";s:4:"ά";s:3:"ὲ";s:4:"ὲ";s:3:"έ";s:4:"έ";s:3:"ὴ";s:4:"ὴ";s:3:"ή";s:4:"ή";s:3:"ὶ";s:4:"ὶ";s:3:"ί";s:4:"ί";s:3:"ὸ";s:4:"ὸ";s:3:"ό";s:4:"ό";s:3:"ὺ";s:4:"ὺ";s:3:"ύ";s:4:"ύ";s:3:"ὼ";s:4:"ὼ";s:3:"ώ";s:4:"ώ";s:3:"ᾀ";s:6:"ᾀ";s:3:"ᾁ";s:6:"ᾁ";s:3:"ᾂ";s:8:"ᾂ";s:3:"ᾃ";s:8:"ᾃ";s:3:"ᾄ";s:8:"ᾄ";s:3:"ᾅ";s:8:"ᾅ";s:3:"ᾆ";s:8:"ᾆ";s:3:"ᾇ";s:8:"ᾇ";s:3:"ᾈ";s:6:"ᾈ";s:3:"ᾉ";s:6:"ᾉ";s:3:"ᾊ";s:8:"ᾊ";s:3:"ᾋ";s:8:"ᾋ";s:3:"ᾌ";s:8:"ᾌ";s:3:"ᾍ";s:8:"ᾍ";s:3:"ᾎ";s:8:"ᾎ";s:3:"ᾏ";s:8:"ᾏ";s:3:"ᾐ";s:6:"ᾐ";s:3:"ᾑ";s:6:"ᾑ";s:3:"ᾒ";s:8:"ᾒ";s:3:"ᾓ";s:8:"ᾓ";s:3:"ᾔ";s:8:"ᾔ";s:3:"ᾕ";s:8:"ᾕ";s:3:"ᾖ";s:8:"ᾖ";s:3:"ᾗ";s:8:"ᾗ";s:3:"ᾘ";s:6:"ᾘ";s:3:"ᾙ";s:6:"ᾙ";s:3:"ᾚ";s:8:"ᾚ";s:3:"ᾛ";s:8:"ᾛ";s:3:"ᾜ";s:8:"ᾜ";s:3:"ᾝ";s:8:"ᾝ";s:3:"ᾞ";s:8:"ᾞ";s:3:"ᾟ";s:8:"ᾟ";s:3:"ᾠ";s:6:"ᾠ";s:3:"ᾡ";s:6:"ᾡ";s:3:"ᾢ";s:8:"ᾢ";s:3:"ᾣ";s:8:"ᾣ";s:3:"ᾤ";s:8:"ᾤ";s:3:"ᾥ";s:8:"ᾥ";s:3:"ᾦ";s:8:"ᾦ";s:3:"ᾧ";s:8:"ᾧ";s:3:"ᾨ";s:6:"ᾨ";s:3:"ᾩ";s:6:"ᾩ";s:3:"ᾪ";s:8:"ᾪ";s:3:"ᾫ";s:8:"ᾫ";s:3:"ᾬ";s:8:"ᾬ";s:3:"ᾭ";s:8:"ᾭ";s:3:"ᾮ";s:8:"ᾮ";s:3:"ᾯ";s:8:"ᾯ";s:3:"ᾰ";s:4:"ᾰ";s:3:"ᾱ";s:4:"ᾱ";s:3:"ᾲ";s:6:"ᾲ";s:3:"ᾳ";s:4:"ᾳ";s:3:"ᾴ";s:6:"ᾴ";s:3:"ᾶ";s:4:"ᾶ";s:3:"ᾷ";s:6:"ᾷ";s:3:"Ᾰ";s:4:"Ᾰ";s:3:"Ᾱ";s:4:"Ᾱ";s:3:"Ὰ";s:4:"Ὰ";s:3:"Ά";s:4:"Ά";s:3:"ᾼ";s:4:"ᾼ";s:3:"ι";s:2:"ι";s:3:"῁";s:4:"῁";s:3:"ῂ";s:6:"ῂ";s:3:"ῃ";s:4:"ῃ";s:3:"ῄ";s:6:"ῄ";s:3:"ῆ";s:4:"ῆ";s:3:"ῇ";s:6:"ῇ";s:3:"Ὲ";s:4:"Ὲ";s:3:"Έ";s:4:"Έ";s:3:"Ὴ";s:4:"Ὴ";s:3:"Ή";s:4:"Ή";s:3:"ῌ";s:4:"ῌ";s:3:"῍";s:5:"῍";s:3:"῎";s:5:"῎";s:3:"῏";s:5:"῏";s:3:"ῐ";s:4:"ῐ";s:3:"ῑ";s:4:"ῑ";s:3:"ῒ";s:6:"ῒ";s:3:"ΐ";s:6:"ΐ";s:3:"ῖ";s:4:"ῖ";s:3:"ῗ";s:6:"ῗ";s:3:"Ῐ";s:4:"Ῐ";s:3:"Ῑ";s:4:"Ῑ";s:3:"Ὶ";s:4:"Ὶ";s:3:"Ί";s:4:"Ί";s:3:"῝";s:5:"῝";s:3:"῞";s:5:"῞";s:3:"῟";s:5:"῟";s:3:"ῠ";s:4:"ῠ";s:3:"ῡ";s:4:"ῡ";s:3:"ῢ";s:6:"ῢ";s:3:"ΰ";s:6:"ΰ";s:3:"ῤ";s:4:"ῤ";s:3:"ῥ";s:4:"ῥ";s:3:"ῦ";s:4:"ῦ";s:3:"ῧ";s:6:"ῧ";s:3:"Ῠ";s:4:"Ῠ";s:3:"Ῡ";s:4:"Ῡ";s:3:"Ὺ";s:4:"Ὺ";s:3:"Ύ";s:4:"Ύ";s:3:"Ῥ";s:4:"Ῥ";s:3:"῭";s:4:"῭";s:3:"΅";s:4:"΅";s:3:"`";s:1:"`";s:3:"ῲ";s:6:"ῲ";s:3:"ῳ";s:4:"ῳ";s:3:"ῴ";s:6:"ῴ";s:3:"ῶ";s:4:"ῶ";s:3:"ῷ";s:6:"ῷ";s:3:"Ὸ";s:4:"Ὸ";s:3:"Ό";s:4:"Ό";s:3:"Ὼ";s:4:"Ὼ";s:3:"Ώ";s:4:"Ώ";s:3:"ῼ";s:4:"ῼ";s:3:"´";s:2:"´";s:3:" ";s:3:" ";s:3:" ";s:3:" ";s:3:"Ω";s:2:"Ω";s:3:"K";s:1:"K";s:3:"Å";s:3:"Å";s:3:"↚";s:5:"↚";s:3:"↛";s:5:"↛";s:3:"↮";s:5:"↮";s:3:"⇍";s:5:"⇍";s:3:"⇎";s:5:"⇎";s:3:"⇏";s:5:"⇏";s:3:"∄";s:5:"∄";s:3:"∉";s:5:"∉";s:3:"∌";s:5:"∌";s:3:"∤";s:5:"∤";s:3:"∦";s:5:"∦";s:3:"≁";s:5:"≁";s:3:"≄";s:5:"≄";s:3:"≇";s:5:"≇";s:3:"≉";s:5:"≉";s:3:"≠";s:3:"≠";s:3:"≢";s:5:"≢";s:3:"≭";s:5:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:3:"≰";s:5:"≰";s:3:"≱";s:5:"≱";s:3:"≴";s:5:"≴";s:3:"≵";s:5:"≵";s:3:"≸";s:5:"≸";s:3:"≹";s:5:"≹";s:3:"⊀";s:5:"⊀";s:3:"⊁";s:5:"⊁";s:3:"⊄";s:5:"⊄";s:3:"⊅";s:5:"⊅";s:3:"⊈";s:5:"⊈";s:3:"⊉";s:5:"⊉";s:3:"⊬";s:5:"⊬";s:3:"⊭";s:5:"⊭";s:3:"⊮";s:5:"⊮";s:3:"⊯";s:5:"⊯";s:3:"⋠";s:5:"⋠";s:3:"⋡";s:5:"⋡";s:3:"⋢";s:5:"⋢";s:3:"⋣";s:5:"⋣";s:3:"⋪";s:5:"⋪";s:3:"⋫";s:5:"⋫";s:3:"⋬";s:5:"⋬";s:3:"⋭";s:5:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:3:"⫝̸";s:5:"⫝̸";s:3:"が";s:6:"が";s:3:"ぎ";s:6:"ぎ";s:3:"ぐ";s:6:"ぐ";s:3:"げ";s:6:"げ";s:3:"ご";s:6:"ご";s:3:"ざ";s:6:"ざ";s:3:"じ";s:6:"じ";s:3:"ず";s:6:"ず";s:3:"ぜ";s:6:"ぜ";s:3:"ぞ";s:6:"ぞ";s:3:"だ";s:6:"だ";s:3:"ぢ";s:6:"ぢ";s:3:"づ";s:6:"づ";s:3:"で";s:6:"で";s:3:"ど";s:6:"ど";s:3:"ば";s:6:"ば";s:3:"ぱ";s:6:"ぱ";s:3:"び";s:6:"び";s:3:"ぴ";s:6:"ぴ";s:3:"ぶ";s:6:"ぶ";s:3:"ぷ";s:6:"ぷ";s:3:"べ";s:6:"べ";s:3:"ぺ";s:6:"ぺ";s:3:"ぼ";s:6:"ぼ";s:3:"ぽ";s:6:"ぽ";s:3:"ゔ";s:6:"ゔ";s:3:"ゞ";s:6:"ゞ";s:3:"ガ";s:6:"ガ";s:3:"ギ";s:6:"ギ";s:3:"グ";s:6:"グ";s:3:"ゲ";s:6:"ゲ";s:3:"ゴ";s:6:"ゴ";s:3:"ザ";s:6:"ザ";s:3:"ジ";s:6:"ジ";s:3:"ズ";s:6:"ズ";s:3:"ゼ";s:6:"ゼ";s:3:"ゾ";s:6:"ゾ";s:3:"ダ";s:6:"ダ";s:3:"ヂ";s:6:"ヂ";s:3:"ヅ";s:6:"ヅ";s:3:"デ";s:6:"デ";s:3:"ド";s:6:"ド";s:3:"バ";s:6:"バ";s:3:"パ";s:6:"パ";s:3:"ビ";s:6:"ビ";s:3:"ピ";s:6:"ピ";s:3:"ブ";s:6:"ブ";s:3:"プ";s:6:"プ";s:3:"ベ";s:6:"ベ";s:3:"ペ";s:6:"ペ";s:3:"ボ";s:6:"ボ";s:3:"ポ";s:6:"ポ";s:3:"ヴ";s:6:"ヴ";s:3:"ヷ";s:6:"ヷ";s:3:"ヸ";s:6:"ヸ";s:3:"ヹ";s:6:"ヹ";s:3:"ヺ";s:6:"ヺ";s:3:"ヾ";s:6:"ヾ";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:3:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:3:"廊";s:3:"朗";s:3:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:3:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:3:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"樂";s:3:"樂";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:3:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:3:"異";s:3:"北";s:3:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:3:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:3:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"說";s:3:"說";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"寧";s:3:"寧";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"樂";s:3:"樂";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:3:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"率";s:3:"率";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:3:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:3:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:3:"侮";s:3:"僧";s:3:"僧";s:3:"免";s:3:"免";s:3:"勉";s:3:"勉";s:3:"勤";s:3:"勤";s:3:"卑";s:3:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:3:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:3:"屮";s:3:"悔";s:3:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:3:"憎";s:3:"懲";s:3:"懲";s:3:"敏";s:3:"敏";s:3:"既";s:3:"既";s:3:"暑";s:3:"暑";s:3:"梅";s:3:"梅";s:3:"海";s:3:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:3:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:3:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"練";s:3:"練";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:3:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"著";s:3:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"逸";s:3:"逸";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:3:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:3:"勇";s:3:"勺";s:3:"勺";s:3:"喝";s:3:"喝";s:3:"啕";s:3:"啕";s:3:"喙";s:3:"喙";s:3:"嗢";s:3:"嗢";s:3:"塚";s:3:"塚";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:3:"慎";s:3:"愈";s:3:"愈";s:3:"憎";s:3:"憎";s:3:"慠";s:3:"慠";s:3:"懲";s:3:"懲";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"晴";s:3:"晴";s:3:"朗";s:3:"朗";s:3:"望";s:3:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"殺";s:3:"殺";s:3:"流";s:3:"流";s:3:"滛";s:3:"滛";s:3:"滋";s:3:"滋";s:3:"漢";s:3:"漢";s:3:"瀞";s:3:"瀞";s:3:"煮";s:3:"煮";s:3:"瞧";s:3:"瞧";s:3:"爵";s:3:"爵";s:3:"犯";s:3:"犯";s:3:"猪";s:3:"猪";s:3:"瑱";s:3:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"益";s:3:"益";s:3:"盛";s:3:"盛";s:3:"直";s:3:"直";s:3:"睊";s:3:"睊";s:3:"着";s:3:"着";s:3:"磌";s:3:"磌";s:3:"窱";s:3:"窱";s:3:"節";s:3:"節";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"練";s:3:"練";s:3:"缾";s:3:"缾";s:3:"者";s:3:"者";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:3:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"視";s:3:"視";s:3:"調";s:3:"調";s:3:"諸";s:3:"諸";s:3:"請";s:3:"請";s:3:"謁";s:3:"謁";s:3:"諾";s:3:"諾";s:3:"諭";s:3:"諭";s:3:"謹";s:3:"謹";s:3:"變";s:3:"變";s:3:"贈";s:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s:3:"韛";s:3:"響";s:3:"響";s:3:"頋";s:3:"頋";s:3:"頻";s:3:"頻";s:3:"鬒";s:3:"鬒";s:3:"龜";s:3:"龜";s:3:"𢡊";s:4:"𢡊";s:3:"𢡄";s:4:"𢡄";s:3:"𣏕";s:4:"𣏕";s:3:"㮝";s:3:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:3:"䀹";s:3:"𥉉";s:4:"𥉉";s:3:"𥳐";s:4:"𥳐";s:3:"𧻓";s:4:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"יִ";s:4:"יִ";s:3:"ײַ";s:4:"ײַ";s:3:"שׁ";s:4:"שׁ";s:3:"שׂ";s:4:"שׂ";s:3:"שּׁ";s:6:"שּׁ";s:3:"שּׂ";s:6:"שּׂ";s:3:"אַ";s:4:"אַ";s:3:"אָ";s:4:"אָ";s:3:"אּ";s:4:"אּ";s:3:"בּ";s:4:"בּ";s:3:"גּ";s:4:"גּ";s:3:"דּ";s:4:"דּ";s:3:"הּ";s:4:"הּ";s:3:"וּ";s:4:"וּ";s:3:"זּ";s:4:"זּ";s:3:"טּ";s:4:"טּ";s:3:"יּ";s:4:"יּ";s:3:"ךּ";s:4:"ךּ";s:3:"כּ";s:4:"כּ";s:3:"לּ";s:4:"לּ";s:3:"מּ";s:4:"מּ";s:3:"נּ";s:4:"נּ";s:3:"סּ";s:4:"סּ";s:3:"ףּ";s:4:"ףּ";s:3:"פּ";s:4:"פּ";s:3:"צּ";s:4:"צּ";s:3:"קּ";s:4:"קּ";s:3:"רּ";s:4:"רּ";s:3:"שּ";s:4:"שּ";s:3:"תּ";s:4:"תּ";s:3:"וֹ";s:4:"וֹ";s:3:"בֿ";s:4:"בֿ";s:3:"כֿ";s:4:"כֿ";s:3:"פֿ";s:4:"פֿ";s:4:"𝅗𝅥";s:8:"𝅗𝅥";s:4:"𝅘𝅥";s:8:"𝅘𝅥";s:4:"𝅘𝅥𝅮";s:12:"𝅘𝅥𝅮";s:4:"𝅘𝅥𝅯";s:12:"𝅘𝅥𝅯";s:4:"𝅘𝅥𝅰";s:12:"𝅘𝅥𝅰";s:4:"𝅘𝅥𝅱";s:12:"𝅘𝅥𝅱";s:4:"𝅘𝅥𝅲";s:12:"𝅘𝅥𝅲";s:4:"𝆹𝅥";s:8:"𝆹𝅥";s:4:"𝆺𝅥";s:8:"𝆺𝅥";s:4:"𝆹𝅥𝅮";s:12:"𝆹𝅥𝅮";s:4:"𝆺𝅥𝅮";s:12:"𝆺𝅥𝅮";s:4:"𝆹𝅥𝅯";s:12:"𝆹𝅥𝅯";s:4:"𝆺𝅥𝅯";s:12:"𝆺𝅥𝅯";s:4:"丽";s:3:"丽";s:4:"丸";s:3:"丸";s:4:"乁";s:3:"乁";s:4:"𠄢";s:4:"𠄢";s:4:"你";s:3:"你";s:4:"侮";s:3:"侮";s:4:"侻";s:3:"侻";s:4:"倂";s:3:"倂";s:4:"偺";s:3:"偺";s:4:"備";s:3:"備";s:4:"僧";s:3:"僧";s:4:"像";s:3:"像";s:4:"㒞";s:3:"㒞";s:4:"𠘺";s:4:"𠘺";s:4:"免";s:3:"免";s:4:"兔";s:3:"兔";s:4:"兤";s:3:"兤";s:4:"具";s:3:"具";s:4:"𠔜";s:4:"𠔜";s:4:"㒹";s:3:"㒹";s:4:"內";s:3:"內";s:4:"再";s:3:"再";s:4:"𠕋";s:4:"𠕋";s:4:"冗";s:3:"冗";s:4:"冤";s:3:"冤";s:4:"仌";s:3:"仌";s:4:"冬";s:3:"冬";s:4:"况";s:3:"况";s:4:"𩇟";s:4:"𩇟";s:4:"凵";s:3:"凵";s:4:"刃";s:3:"刃";s:4:"㓟";s:3:"㓟";s:4:"刻";s:3:"刻";s:4:"剆";s:3:"剆";s:4:"割";s:3:"割";s:4:"剷";s:3:"剷";s:4:"㔕";s:3:"㔕";s:4:"勇";s:3:"勇";s:4:"勉";s:3:"勉";s:4:"勤";s:3:"勤";s:4:"勺";s:3:"勺";s:4:"包";s:3:"包";s:4:"匆";s:3:"匆";s:4:"北";s:3:"北";s:4:"卉";s:3:"卉";s:4:"卑";s:3:"卑";s:4:"博";s:3:"博";s:4:"即";s:3:"即";s:4:"卽";s:3:"卽";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"𠨬";s:4:"𠨬";s:4:"灰";s:3:"灰";s:4:"及";s:3:"及";s:4:"叟";s:3:"叟";s:4:"𠭣";s:4:"𠭣";s:4:"叫";s:3:"叫";s:4:"叱";s:3:"叱";s:4:"吆";s:3:"吆";s:4:"咞";s:3:"咞";s:4:"吸";s:3:"吸";s:4:"呈";s:3:"呈";s:4:"周";s:3:"周";s:4:"咢";s:3:"咢";s:4:"哶";s:3:"哶";s:4:"唐";s:3:"唐";s:4:"啓";s:3:"啓";s:4:"啣";s:3:"啣";s:4:"善";s:3:"善";s:4:"善";s:3:"善";s:4:"喙";s:3:"喙";s:4:"喫";s:3:"喫";s:4:"喳";s:3:"喳";s:4:"嗂";s:3:"嗂";s:4:"圖";s:3:"圖";s:4:"嘆";s:3:"嘆";s:4:"圗";s:3:"圗";s:4:"噑";s:3:"噑";s:4:"噴";s:3:"噴";s:4:"切";s:3:"切";s:4:"壮";s:3:"壮";s:4:"城";s:3:"城";s:4:"埴";s:3:"埴";s:4:"堍";s:3:"堍";s:4:"型";s:3:"型";s:4:"堲";s:3:"堲";s:4:"報";s:3:"報";s:4:"墬";s:3:"墬";s:4:"𡓤";s:4:"𡓤";s:4:"売";s:3:"売";s:4:"壷";s:3:"壷";s:4:"夆";s:3:"夆";s:4:"多";s:3:"多";s:4:"夢";s:3:"夢";s:4:"奢";s:3:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:4:"姬";s:3:"姬";s:4:"娛";s:3:"娛";s:4:"娧";s:3:"娧";s:4:"姘";s:3:"姘";s:4:"婦";s:3:"婦";s:4:"㛮";s:3:"㛮";s:4:"㛼";s:3:"㛼";s:4:"嬈";s:3:"嬈";s:4:"嬾";s:3:"嬾";s:4:"嬾";s:3:"嬾";s:4:"𡧈";s:4:"𡧈";s:4:"寃";s:3:"寃";s:4:"寘";s:3:"寘";s:4:"寧";s:3:"寧";s:4:"寳";s:3:"寳";s:4:"𡬘";s:4:"𡬘";s:4:"寿";s:3:"寿";s:4:"将";s:3:"将";s:4:"当";s:3:"当";s:4:"尢";s:3:"尢";s:4:"㞁";s:3:"㞁";s:4:"屠";s:3:"屠";s:4:"屮";s:3:"屮";s:4:"峀";s:3:"峀";s:4:"岍";s:3:"岍";s:4:"𡷤";s:4:"𡷤";s:4:"嵃";s:3:"嵃";s:4:"𡷦";s:4:"𡷦";s:4:"嵮";s:3:"嵮";s:4:"嵫";s:3:"嵫";s:4:"嵼";s:3:"嵼";s:4:"巡";s:3:"巡";s:4:"巢";s:3:"巢";s:4:"㠯";s:3:"㠯";s:4:"巽";s:3:"巽";s:4:"帨";s:3:"帨";s:4:"帽";s:3:"帽";s:4:"幩";s:3:"幩";s:4:"㡢";s:3:"㡢";s:4:"𢆃";s:4:"𢆃";s:4:"㡼";s:3:"㡼";s:4:"庰";s:3:"庰";s:4:"庳";s:3:"庳";s:4:"庶";s:3:"庶";s:4:"廊";s:3:"廊";s:4:"𪎒";s:4:"𪎒";s:4:"廾";s:3:"廾";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"舁";s:3:"舁";s:4:"弢";s:3:"弢";s:4:"弢";s:3:"弢";s:4:"㣇";s:3:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:4:"形";s:3:"形";s:4:"彫";s:3:"彫";s:4:"㣣";s:3:"㣣";s:4:"徚";s:3:"徚";s:4:"忍";s:3:"忍";s:4:"志";s:3:"志";s:4:"忹";s:3:"忹";s:4:"悁";s:3:"悁";s:4:"㤺";s:3:"㤺";s:4:"㤜";s:3:"㤜";s:4:"悔";s:3:"悔";s:4:"𢛔";s:4:"𢛔";s:4:"惇";s:3:"惇";s:4:"慈";s:3:"慈";s:4:"慌";s:3:"慌";s:4:"慎";s:3:"慎";s:4:"慌";s:3:"慌";s:4:"慺";s:3:"慺";s:4:"憎";s:3:"憎";s:4:"憲";s:3:"憲";s:4:"憤";s:3:"憤";s:4:"憯";s:3:"憯";s:4:"懞";s:3:"懞";s:4:"懲";s:3:"懲";s:4:"懶";s:3:"懶";s:4:"成";s:3:"成";s:4:"戛";s:3:"戛";s:4:"扝";s:3:"扝";s:4:"抱";s:3:"抱";s:4:"拔";s:3:"拔";s:4:"捐";s:3:"捐";s:4:"𢬌";s:4:"𢬌";s:4:"挽";s:3:"挽";s:4:"拼";s:3:"拼";s:4:"捨";s:3:"捨";s:4:"掃";s:3:"掃";s:4:"揤";s:3:"揤";s:4:"𢯱";s:4:"𢯱";s:4:"搢";s:3:"搢";s:4:"揅";s:3:"揅";s:4:"掩";s:3:"掩";s:4:"㨮";s:3:"㨮";s:4:"摩";s:3:"摩";s:4:"摾";s:3:"摾";s:4:"撝";s:3:"撝";s:4:"摷";s:3:"摷";s:4:"㩬";s:3:"㩬";s:4:"敏";s:3:"敏";s:4:"敬";s:3:"敬";s:4:"𣀊";s:4:"𣀊";s:4:"旣";s:3:"旣";s:4:"書";s:3:"書";s:4:"晉";s:3:"晉";s:4:"㬙";s:3:"㬙";s:4:"暑";s:3:"暑";s:4:"㬈";s:3:"㬈";s:4:"㫤";s:3:"㫤";s:4:"冒";s:3:"冒";s:4:"冕";s:3:"冕";s:4:"最";s:3:"最";s:4:"暜";s:3:"暜";s:4:"肭";s:3:"肭";s:4:"䏙";s:3:"䏙";s:4:"朗";s:3:"朗";s:4:"望";s:3:"望";s:4:"朡";s:3:"朡";s:4:"杞";s:3:"杞";s:4:"杓";s:3:"杓";s:4:"𣏃";s:4:"𣏃";s:4:"㭉";s:3:"㭉";s:4:"柺";s:3:"柺";s:4:"枅";s:3:"枅";s:4:"桒";s:3:"桒";s:4:"梅";s:3:"梅";s:4:"𣑭";s:4:"𣑭";s:4:"梎";s:3:"梎";s:4:"栟";s:3:"栟";s:4:"椔";s:3:"椔";s:4:"㮝";s:3:"㮝";s:4:"楂";s:3:"楂";s:4:"榣";s:3:"榣";s:4:"槪";s:3:"槪";s:4:"檨";s:3:"檨";s:4:"𣚣";s:4:"𣚣";s:4:"櫛";s:3:"櫛";s:4:"㰘";s:3:"㰘";s:4:"次";s:3:"次";s:4:"𣢧";s:4:"𣢧";s:4:"歔";s:3:"歔";s:4:"㱎";s:3:"㱎";s:4:"歲";s:3:"歲";s:4:"殟";s:3:"殟";s:4:"殺";s:3:"殺";s:4:"殻";s:3:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:4:"汎";s:3:"汎";s:4:"𣲼";s:4:"𣲼";s:4:"沿";s:3:"沿";s:4:"泍";s:3:"泍";s:4:"汧";s:3:"汧";s:4:"洖";s:3:"洖";s:4:"派";s:3:"派";s:4:"海";s:3:"海";s:4:"流";s:3:"流";s:4:"浩";s:3:"浩";s:4:"浸";s:3:"浸";s:4:"涅";s:3:"涅";s:4:"𣴞";s:4:"𣴞";s:4:"洴";s:3:"洴";s:4:"港";s:3:"港";s:4:"湮";s:3:"湮";s:4:"㴳";s:3:"㴳";s:4:"滋";s:3:"滋";s:4:"滇";s:3:"滇";s:4:"𣻑";s:4:"𣻑";s:4:"淹";s:3:"淹";s:4:"潮";s:3:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:4:"濆";s:3:"濆";s:4:"瀹";s:3:"瀹";s:4:"瀞";s:3:"瀞";s:4:"瀛";s:3:"瀛";s:4:"㶖";s:3:"㶖";s:4:"灊";s:3:"灊";s:4:"災";s:3:"災";s:4:"灷";s:3:"灷";s:4:"炭";s:3:"炭";s:4:"𠔥";s:4:"𠔥";s:4:"煅";s:3:"煅";s:4:"𤉣";s:4:"𤉣";s:4:"熜";s:3:"熜";s:4:"𤎫";s:4:"𤎫";s:4:"爨";s:3:"爨";s:4:"爵";s:3:"爵";s:4:"牐";s:3:"牐";s:4:"𤘈";s:4:"𤘈";s:4:"犀";s:3:"犀";s:4:"犕";s:3:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:4:"獺";s:3:"獺";s:4:"王";s:3:"王";s:4:"㺬";s:3:"㺬";s:4:"玥";s:3:"玥";s:4:"㺸";s:3:"㺸";s:4:"㺸";s:3:"㺸";s:4:"瑇";s:3:"瑇";s:4:"瑜";s:3:"瑜";s:4:"瑱";s:3:"瑱";s:4:"璅";s:3:"璅";s:4:"瓊";s:3:"瓊";s:4:"㼛";s:3:"㼛";s:4:"甤";s:3:"甤";s:4:"𤰶";s:4:"𤰶";s:4:"甾";s:3:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"異";s:3:"異";s:4:"𢆟";s:4:"𢆟";s:4:"瘐";s:3:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:4:"㿼";s:3:"㿼";s:4:"䀈";s:3:"䀈";s:4:"直";s:3:"直";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:4:"眞";s:3:"眞";s:4:"真";s:3:"真";s:4:"真";s:3:"真";s:4:"睊";s:3:"睊";s:4:"䀹";s:3:"䀹";s:4:"瞋";s:3:"瞋";s:4:"䁆";s:3:"䁆";s:4:"䂖";s:3:"䂖";s:4:"𥐝";s:4:"𥐝";s:4:"硎";s:3:"硎";s:4:"碌";s:3:"碌";s:4:"磌";s:3:"磌";s:4:"䃣";s:3:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"祖";s:3:"祖";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:4:"福";s:3:"福";s:4:"秫";s:3:"秫";s:4:"䄯";s:3:"䄯";s:4:"穀";s:3:"穀";s:4:"穊";s:3:"穊";s:4:"穏";s:3:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"竮";s:3:"竮";s:4:"䈂";s:3:"䈂";s:4:"𥮫";s:4:"𥮫";s:4:"篆";s:3:"篆";s:4:"築";s:3:"築";s:4:"䈧";s:3:"䈧";s:4:"𥲀";s:4:"𥲀";s:4:"糒";s:3:"糒";s:4:"䊠";s:3:"䊠";s:4:"糨";s:3:"糨";s:4:"糣";s:3:"糣";s:4:"紀";s:3:"紀";s:4:"𥾆";s:4:"𥾆";s:4:"絣";s:3:"絣";s:4:"䌁";s:3:"䌁";s:4:"緇";s:3:"緇";s:4:"縂";s:3:"縂";s:4:"繅";s:3:"繅";s:4:"䌴";s:3:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:4:"䍙";s:3:"䍙";s:4:"𦋙";s:4:"𦋙";s:4:"罺";s:3:"罺";s:4:"𦌾";s:4:"𦌾";s:4:"羕";s:3:"羕";s:4:"翺";s:3:"翺";s:4:"者";s:3:"者";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:4:"聠";s:3:"聠";s:4:"𦖨";s:4:"𦖨";s:4:"聰";s:3:"聰";s:4:"𣍟";s:4:"𣍟";s:4:"䏕";s:3:"䏕";s:4:"育";s:3:"育";s:4:"脃";s:3:"脃";s:4:"䐋";s:3:"䐋";s:4:"脾";s:3:"脾";s:4:"媵";s:3:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:4:"舁";s:3:"舁";s:4:"舄";s:3:"舄";s:4:"辞";s:3:"辞";s:4:"䑫";s:3:"䑫";s:4:"芑";s:3:"芑";s:4:"芋";s:3:"芋";s:4:"芝";s:3:"芝";s:4:"劳";s:3:"劳";s:4:"花";s:3:"花";s:4:"芳";s:3:"芳";s:4:"芽";s:3:"芽";s:4:"苦";s:3:"苦";s:4:"𦬼";s:4:"𦬼";s:4:"若";s:3:"若";s:4:"茝";s:3:"茝";s:4:"荣";s:3:"荣";s:4:"莭";s:3:"莭";s:4:"茣";s:3:"茣";s:4:"莽";s:3:"莽";s:4:"菧";s:3:"菧";s:4:"著";s:3:"著";s:4:"荓";s:3:"荓";s:4:"菊";s:3:"菊";s:4:"菌";s:3:"菌";s:4:"菜";s:3:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:4:"䔫";s:3:"䔫";s:4:"蓱";s:3:"蓱";s:4:"蓳";s:3:"蓳";s:4:"蔖";s:3:"蔖";s:4:"𧏊";s:4:"𧏊";s:4:"蕤";s:3:"蕤";s:4:"𦼬";s:4:"𦼬";s:4:"䕝";s:3:"䕝";s:4:"䕡";s:3:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:4:"䕫";s:3:"䕫";s:4:"虐";s:3:"虐";s:4:"虜";s:3:"虜";s:4:"虧";s:3:"虧";s:4:"虩";s:3:"虩";s:4:"蚩";s:3:"蚩";s:4:"蚈";s:3:"蚈";s:4:"蜎";s:3:"蜎";s:4:"蛢";s:3:"蛢";s:4:"蝹";s:3:"蝹";s:4:"蜨";s:3:"蜨";s:4:"蝫";s:3:"蝫";s:4:"螆";s:3:"螆";s:4:"䗗";s:3:"䗗";s:4:"蟡";s:3:"蟡";s:4:"蠁";s:3:"蠁";s:4:"䗹";s:3:"䗹";s:4:"衠";s:3:"衠";s:4:"衣";s:3:"衣";s:4:"𧙧";s:4:"𧙧";s:4:"裗";s:3:"裗";s:4:"裞";s:3:"裞";s:4:"䘵";s:3:"䘵";s:4:"裺";s:3:"裺";s:4:"㒻";s:3:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:4:"䚾";s:3:"䚾";s:4:"䛇";s:3:"䛇";s:4:"誠";s:3:"誠";s:4:"諭";s:3:"諭";s:4:"變";s:3:"變";s:4:"豕";s:3:"豕";s:4:"𧲨";s:4:"𧲨";s:4:"貫";s:3:"貫";s:4:"賁";s:3:"賁";s:4:"贛";s:3:"贛";s:4:"起";s:3:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:4:"跋";s:3:"跋";s:4:"趼";s:3:"趼";s:4:"跰";s:3:"跰";s:4:"𠣞";s:4:"𠣞";s:4:"軔";s:3:"軔";s:4:"輸";s:3:"輸";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:4:"邔";s:3:"邔";s:4:"郱";s:3:"郱";s:4:"鄑";s:3:"鄑";s:4:"𨜮";s:4:"𨜮";s:4:"鄛";s:3:"鄛";s:4:"鈸";s:3:"鈸";s:4:"鋗";s:3:"鋗";s:4:"鋘";s:3:"鋘";s:4:"鉼";s:3:"鉼";s:4:"鏹";s:3:"鏹";s:4:"鐕";s:3:"鐕";s:4:"𨯺";s:4:"𨯺";s:4:"開";s:3:"開";s:4:"䦕";s:3:"䦕";s:4:"閷";s:3:"閷";s:4:"𨵷";s:4:"𨵷";s:4:"䧦";s:3:"䧦";s:4:"雃";s:3:"雃";s:4:"嶲";s:3:"嶲";s:4:"霣";s:3:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:4:"䩮";s:3:"䩮";s:4:"䩶";s:3:"䩶";s:4:"韠";s:3:"韠";s:4:"𩐊";s:4:"𩐊";s:4:"䪲";s:3:"䪲";s:4:"𩒖";s:4:"𩒖";s:4:"頋";s:3:"頋";s:4:"頋";s:3:"頋";s:4:"頩";s:3:"頩";s:4:"𩖶";s:4:"𩖶";s:4:"飢";s:3:"飢";s:4:"䬳";s:3:"䬳";s:4:"餩";s:3:"餩";s:4:"馧";s:3:"馧";s:4:"駂";s:3:"駂";s:4:"駾";s:3:"駾";s:4:"䯎";s:3:"䯎";s:4:"𩬰";s:4:"𩬰";s:4:"鬒";s:3:"鬒";s:4:"鱀";s:3:"鱀";s:4:"鳽";s:3:"鳽";s:4:"䳎";s:3:"䳎";s:4:"䳭";s:3:"䳭";s:4:"鵧";s:3:"鵧";s:4:"𪃎";s:4:"𪃎";s:4:"䳸";s:3:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:4:"麻";s:3:"麻";s:4:"䵖";s:3:"䵖";s:4:"黹";s:3:"黹";s:4:"黾";s:3:"黾";s:4:"鼅";s:3:"鼅";s:4:"鼏";s:3:"鼏";s:4:"鼖";s:3:"鼖";s:4:"鼻";s:3:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
+$utfCheckNFC = unserialize( 'a:1216:{s:2:"̀";s:1:"N";s:2:"́";s:1:"N";s:2:"̓";s:1:"N";s:2:"̈́";s:1:"N";s:2:"ʹ";s:1:"N";s:2:";";s:1:"N";s:2:"·";s:1:"N";s:3:"क़";s:1:"N";s:3:"ख़";s:1:"N";s:3:"ग़";s:1:"N";s:3:"ज़";s:1:"N";s:3:"ड़";s:1:"N";s:3:"ढ़";s:1:"N";s:3:"फ़";s:1:"N";s:3:"य़";s:1:"N";s:3:"ড়";s:1:"N";s:3:"ঢ়";s:1:"N";s:3:"য়";s:1:"N";s:3:"ਲ਼";s:1:"N";s:3:"ਸ਼";s:1:"N";s:3:"ਖ਼";s:1:"N";s:3:"ਗ਼";s:1:"N";s:3:"ਜ਼";s:1:"N";s:3:"ਫ਼";s:1:"N";s:3:"ଡ଼";s:1:"N";s:3:"ଢ଼";s:1:"N";s:3:"གྷ";s:1:"N";s:3:"ཌྷ";s:1:"N";s:3:"དྷ";s:1:"N";s:3:"བྷ";s:1:"N";s:3:"ཛྷ";s:1:"N";s:3:"ཀྵ";s:1:"N";s:3:"ཱི";s:1:"N";s:3:"ཱུ";s:1:"N";s:3:"ྲྀ";s:1:"N";s:3:"ླྀ";s:1:"N";s:3:"ཱྀ";s:1:"N";s:3:"ྒྷ";s:1:"N";s:3:"ྜྷ";s:1:"N";s:3:"ྡྷ";s:1:"N";s:3:"ྦྷ";s:1:"N";s:3:"ྫྷ";s:1:"N";s:3:"ྐྵ";s:1:"N";s:3:"ά";s:1:"N";s:3:"έ";s:1:"N";s:3:"ή";s:1:"N";s:3:"ί";s:1:"N";s:3:"ό";s:1:"N";s:3:"ύ";s:1:"N";s:3:"ώ";s:1:"N";s:3:"Ά";s:1:"N";s:3:"ι";s:1:"N";s:3:"Έ";s:1:"N";s:3:"Ή";s:1:"N";s:3:"ΐ";s:1:"N";s:3:"Ί";s:1:"N";s:3:"ΰ";s:1:"N";s:3:"Ύ";s:1:"N";s:3:"΅";s:1:"N";s:3:"`";s:1:"N";s:3:"Ό";s:1:"N";s:3:"Ώ";s:1:"N";s:3:"´";s:1:"N";s:3:" ";s:1:"N";s:3:" ";s:1:"N";s:3:"Ω";s:1:"N";s:3:"K";s:1:"N";s:3:"Å";s:1:"N";s:3:"〈";s:1:"N";s:3:"〉";s:1:"N";s:3:"⫝̸";s:1:"N";s:3:"豈";s:1:"N";s:3:"更";s:1:"N";s:3:"車";s:1:"N";s:3:"賈";s:1:"N";s:3:"滑";s:1:"N";s:3:"串";s:1:"N";s:3:"句";s:1:"N";s:3:"龜";s:1:"N";s:3:"龜";s:1:"N";s:3:"契";s:1:"N";s:3:"金";s:1:"N";s:3:"喇";s:1:"N";s:3:"奈";s:1:"N";s:3:"懶";s:1:"N";s:3:"癩";s:1:"N";s:3:"羅";s:1:"N";s:3:"蘿";s:1:"N";s:3:"螺";s:1:"N";s:3:"裸";s:1:"N";s:3:"邏";s:1:"N";s:3:"樂";s:1:"N";s:3:"洛";s:1:"N";s:3:"烙";s:1:"N";s:3:"珞";s:1:"N";s:3:"落";s:1:"N";s:3:"酪";s:1:"N";s:3:"駱";s:1:"N";s:3:"亂";s:1:"N";s:3:"卵";s:1:"N";s:3:"欄";s:1:"N";s:3:"爛";s:1:"N";s:3:"蘭";s:1:"N";s:3:"鸞";s:1:"N";s:3:"嵐";s:1:"N";s:3:"濫";s:1:"N";s:3:"藍";s:1:"N";s:3:"襤";s:1:"N";s:3:"拉";s:1:"N";s:3:"臘";s:1:"N";s:3:"蠟";s:1:"N";s:3:"廊";s:1:"N";s:3:"朗";s:1:"N";s:3:"浪";s:1:"N";s:3:"狼";s:1:"N";s:3:"郎";s:1:"N";s:3:"來";s:1:"N";s:3:"冷";s:1:"N";s:3:"勞";s:1:"N";s:3:"擄";s:1:"N";s:3:"櫓";s:1:"N";s:3:"爐";s:1:"N";s:3:"盧";s:1:"N";s:3:"老";s:1:"N";s:3:"蘆";s:1:"N";s:3:"虜";s:1:"N";s:3:"路";s:1:"N";s:3:"露";s:1:"N";s:3:"魯";s:1:"N";s:3:"鷺";s:1:"N";s:3:"碌";s:1:"N";s:3:"祿";s:1:"N";s:3:"綠";s:1:"N";s:3:"菉";s:1:"N";s:3:"錄";s:1:"N";s:3:"鹿";s:1:"N";s:3:"論";s:1:"N";s:3:"壟";s:1:"N";s:3:"弄";s:1:"N";s:3:"籠";s:1:"N";s:3:"聾";s:1:"N";s:3:"牢";s:1:"N";s:3:"磊";s:1:"N";s:3:"賂";s:1:"N";s:3:"雷";s:1:"N";s:3:"壘";s:1:"N";s:3:"屢";s:1:"N";s:3:"樓";s:1:"N";s:3:"淚";s:1:"N";s:3:"漏";s:1:"N";s:3:"累";s:1:"N";s:3:"縷";s:1:"N";s:3:"陋";s:1:"N";s:3:"勒";s:1:"N";s:3:"肋";s:1:"N";s:3:"凜";s:1:"N";s:3:"凌";s:1:"N";s:3:"稜";s:1:"N";s:3:"綾";s:1:"N";s:3:"菱";s:1:"N";s:3:"陵";s:1:"N";s:3:"讀";s:1:"N";s:3:"拏";s:1:"N";s:3:"樂";s:1:"N";s:3:"諾";s:1:"N";s:3:"丹";s:1:"N";s:3:"寧";s:1:"N";s:3:"怒";s:1:"N";s:3:"率";s:1:"N";s:3:"異";s:1:"N";s:3:"北";s:1:"N";s:3:"磻";s:1:"N";s:3:"便";s:1:"N";s:3:"復";s:1:"N";s:3:"不";s:1:"N";s:3:"泌";s:1:"N";s:3:"數";s:1:"N";s:3:"索";s:1:"N";s:3:"參";s:1:"N";s:3:"塞";s:1:"N";s:3:"省";s:1:"N";s:3:"葉";s:1:"N";s:3:"說";s:1:"N";s:3:"殺";s:1:"N";s:3:"辰";s:1:"N";s:3:"沈";s:1:"N";s:3:"拾";s:1:"N";s:3:"若";s:1:"N";s:3:"掠";s:1:"N";s:3:"略";s:1:"N";s:3:"亮";s:1:"N";s:3:"兩";s:1:"N";s:3:"凉";s:1:"N";s:3:"梁";s:1:"N";s:3:"糧";s:1:"N";s:3:"良";s:1:"N";s:3:"諒";s:1:"N";s:3:"量";s:1:"N";s:3:"勵";s:1:"N";s:3:"呂";s:1:"N";s:3:"女";s:1:"N";s:3:"廬";s:1:"N";s:3:"旅";s:1:"N";s:3:"濾";s:1:"N";s:3:"礪";s:1:"N";s:3:"閭";s:1:"N";s:3:"驪";s:1:"N";s:3:"麗";s:1:"N";s:3:"黎";s:1:"N";s:3:"力";s:1:"N";s:3:"曆";s:1:"N";s:3:"歷";s:1:"N";s:3:"轢";s:1:"N";s:3:"年";s:1:"N";s:3:"憐";s:1:"N";s:3:"戀";s:1:"N";s:3:"撚";s:1:"N";s:3:"漣";s:1:"N";s:3:"煉";s:1:"N";s:3:"璉";s:1:"N";s:3:"秊";s:1:"N";s:3:"練";s:1:"N";s:3:"聯";s:1:"N";s:3:"輦";s:1:"N";s:3:"蓮";s:1:"N";s:3:"連";s:1:"N";s:3:"鍊";s:1:"N";s:3:"列";s:1:"N";s:3:"劣";s:1:"N";s:3:"咽";s:1:"N";s:3:"烈";s:1:"N";s:3:"裂";s:1:"N";s:3:"說";s:1:"N";s:3:"廉";s:1:"N";s:3:"念";s:1:"N";s:3:"捻";s:1:"N";s:3:"殮";s:1:"N";s:3:"簾";s:1:"N";s:3:"獵";s:1:"N";s:3:"令";s:1:"N";s:3:"囹";s:1:"N";s:3:"寧";s:1:"N";s:3:"嶺";s:1:"N";s:3:"怜";s:1:"N";s:3:"玲";s:1:"N";s:3:"瑩";s:1:"N";s:3:"羚";s:1:"N";s:3:"聆";s:1:"N";s:3:"鈴";s:1:"N";s:3:"零";s:1:"N";s:3:"靈";s:1:"N";s:3:"領";s:1:"N";s:3:"例";s:1:"N";s:3:"禮";s:1:"N";s:3:"醴";s:1:"N";s:3:"隸";s:1:"N";s:3:"惡";s:1:"N";s:3:"了";s:1:"N";s:3:"僚";s:1:"N";s:3:"寮";s:1:"N";s:3:"尿";s:1:"N";s:3:"料";s:1:"N";s:3:"樂";s:1:"N";s:3:"燎";s:1:"N";s:3:"療";s:1:"N";s:3:"蓼";s:1:"N";s:3:"遼";s:1:"N";s:3:"龍";s:1:"N";s:3:"暈";s:1:"N";s:3:"阮";s:1:"N";s:3:"劉";s:1:"N";s:3:"杻";s:1:"N";s:3:"柳";s:1:"N";s:3:"流";s:1:"N";s:3:"溜";s:1:"N";s:3:"琉";s:1:"N";s:3:"留";s:1:"N";s:3:"硫";s:1:"N";s:3:"紐";s:1:"N";s:3:"類";s:1:"N";s:3:"六";s:1:"N";s:3:"戮";s:1:"N";s:3:"陸";s:1:"N";s:3:"倫";s:1:"N";s:3:"崙";s:1:"N";s:3:"淪";s:1:"N";s:3:"輪";s:1:"N";s:3:"律";s:1:"N";s:3:"慄";s:1:"N";s:3:"栗";s:1:"N";s:3:"率";s:1:"N";s:3:"隆";s:1:"N";s:3:"利";s:1:"N";s:3:"吏";s:1:"N";s:3:"履";s:1:"N";s:3:"易";s:1:"N";s:3:"李";s:1:"N";s:3:"梨";s:1:"N";s:3:"泥";s:1:"N";s:3:"理";s:1:"N";s:3:"痢";s:1:"N";s:3:"罹";s:1:"N";s:3:"裏";s:1:"N";s:3:"裡";s:1:"N";s:3:"里";s:1:"N";s:3:"離";s:1:"N";s:3:"匿";s:1:"N";s:3:"溺";s:1:"N";s:3:"吝";s:1:"N";s:3:"燐";s:1:"N";s:3:"璘";s:1:"N";s:3:"藺";s:1:"N";s:3:"隣";s:1:"N";s:3:"鱗";s:1:"N";s:3:"麟";s:1:"N";s:3:"林";s:1:"N";s:3:"淋";s:1:"N";s:3:"臨";s:1:"N";s:3:"立";s:1:"N";s:3:"笠";s:1:"N";s:3:"粒";s:1:"N";s:3:"狀";s:1:"N";s:3:"炙";s:1:"N";s:3:"識";s:1:"N";s:3:"什";s:1:"N";s:3:"茶";s:1:"N";s:3:"刺";s:1:"N";s:3:"切";s:1:"N";s:3:"度";s:1:"N";s:3:"拓";s:1:"N";s:3:"糖";s:1:"N";s:3:"宅";s:1:"N";s:3:"洞";s:1:"N";s:3:"暴";s:1:"N";s:3:"輻";s:1:"N";s:3:"行";s:1:"N";s:3:"降";s:1:"N";s:3:"見";s:1:"N";s:3:"廓";s:1:"N";s:3:"兀";s:1:"N";s:3:"嗀";s:1:"N";s:3:"塚";s:1:"N";s:3:"晴";s:1:"N";s:3:"凞";s:1:"N";s:3:"猪";s:1:"N";s:3:"益";s:1:"N";s:3:"礼";s:1:"N";s:3:"神";s:1:"N";s:3:"祥";s:1:"N";s:3:"福";s:1:"N";s:3:"靖";s:1:"N";s:3:"精";s:1:"N";s:3:"羽";s:1:"N";s:3:"蘒";s:1:"N";s:3:"諸";s:1:"N";s:3:"逸";s:1:"N";s:3:"都";s:1:"N";s:3:"飯";s:1:"N";s:3:"飼";s:1:"N";s:3:"館";s:1:"N";s:3:"鶴";s:1:"N";s:3:"侮";s:1:"N";s:3:"僧";s:1:"N";s:3:"免";s:1:"N";s:3:"勉";s:1:"N";s:3:"勤";s:1:"N";s:3:"卑";s:1:"N";s:3:"喝";s:1:"N";s:3:"嘆";s:1:"N";s:3:"器";s:1:"N";s:3:"塀";s:1:"N";s:3:"墨";s:1:"N";s:3:"層";s:1:"N";s:3:"屮";s:1:"N";s:3:"悔";s:1:"N";s:3:"慨";s:1:"N";s:3:"憎";s:1:"N";s:3:"懲";s:1:"N";s:3:"敏";s:1:"N";s:3:"既";s:1:"N";s:3:"暑";s:1:"N";s:3:"梅";s:1:"N";s:3:"海";s:1:"N";s:3:"渚";s:1:"N";s:3:"漢";s:1:"N";s:3:"煮";s:1:"N";s:3:"爫";s:1:"N";s:3:"琢";s:1:"N";s:3:"碑";s:1:"N";s:3:"社";s:1:"N";s:3:"祉";s:1:"N";s:3:"祈";s:1:"N";s:3:"祐";s:1:"N";s:3:"祖";s:1:"N";s:3:"祝";s:1:"N";s:3:"禍";s:1:"N";s:3:"禎";s:1:"N";s:3:"穀";s:1:"N";s:3:"突";s:1:"N";s:3:"節";s:1:"N";s:3:"練";s:1:"N";s:3:"縉";s:1:"N";s:3:"繁";s:1:"N";s:3:"署";s:1:"N";s:3:"者";s:1:"N";s:3:"臭";s:1:"N";s:3:"艹";s:1:"N";s:3:"艹";s:1:"N";s:3:"著";s:1:"N";s:3:"褐";s:1:"N";s:3:"視";s:1:"N";s:3:"謁";s:1:"N";s:3:"謹";s:1:"N";s:3:"賓";s:1:"N";s:3:"贈";s:1:"N";s:3:"辶";s:1:"N";s:3:"逸";s:1:"N";s:3:"難";s:1:"N";s:3:"響";s:1:"N";s:3:"頻";s:1:"N";s:3:"並";s:1:"N";s:3:"况";s:1:"N";s:3:"全";s:1:"N";s:3:"侀";s:1:"N";s:3:"充";s:1:"N";s:3:"冀";s:1:"N";s:3:"勇";s:1:"N";s:3:"勺";s:1:"N";s:3:"喝";s:1:"N";s:3:"啕";s:1:"N";s:3:"喙";s:1:"N";s:3:"嗢";s:1:"N";s:3:"塚";s:1:"N";s:3:"墳";s:1:"N";s:3:"奄";s:1:"N";s:3:"奔";s:1:"N";s:3:"婢";s:1:"N";s:3:"嬨";s:1:"N";s:3:"廒";s:1:"N";s:3:"廙";s:1:"N";s:3:"彩";s:1:"N";s:3:"徭";s:1:"N";s:3:"惘";s:1:"N";s:3:"慎";s:1:"N";s:3:"愈";s:1:"N";s:3:"憎";s:1:"N";s:3:"慠";s:1:"N";s:3:"懲";s:1:"N";s:3:"戴";s:1:"N";s:3:"揄";s:1:"N";s:3:"搜";s:1:"N";s:3:"摒";s:1:"N";s:3:"敖";s:1:"N";s:3:"晴";s:1:"N";s:3:"朗";s:1:"N";s:3:"望";s:1:"N";s:3:"杖";s:1:"N";s:3:"歹";s:1:"N";s:3:"殺";s:1:"N";s:3:"流";s:1:"N";s:3:"滛";s:1:"N";s:3:"滋";s:1:"N";s:3:"漢";s:1:"N";s:3:"瀞";s:1:"N";s:3:"煮";s:1:"N";s:3:"瞧";s:1:"N";s:3:"爵";s:1:"N";s:3:"犯";s:1:"N";s:3:"猪";s:1:"N";s:3:"瑱";s:1:"N";s:3:"甆";s:1:"N";s:3:"画";s:1:"N";s:3:"瘝";s:1:"N";s:3:"瘟";s:1:"N";s:3:"益";s:1:"N";s:3:"盛";s:1:"N";s:3:"直";s:1:"N";s:3:"睊";s:1:"N";s:3:"着";s:1:"N";s:3:"磌";s:1:"N";s:3:"窱";s:1:"N";s:3:"節";s:1:"N";s:3:"类";s:1:"N";s:3:"絛";s:1:"N";s:3:"練";s:1:"N";s:3:"缾";s:1:"N";s:3:"者";s:1:"N";s:3:"荒";s:1:"N";s:3:"華";s:1:"N";s:3:"蝹";s:1:"N";s:3:"襁";s:1:"N";s:3:"覆";s:1:"N";s:3:"視";s:1:"N";s:3:"調";s:1:"N";s:3:"諸";s:1:"N";s:3:"請";s:1:"N";s:3:"謁";s:1:"N";s:3:"諾";s:1:"N";s:3:"諭";s:1:"N";s:3:"謹";s:1:"N";s:3:"變";s:1:"N";s:3:"贈";s:1:"N";s:3:"輸";s:1:"N";s:3:"遲";s:1:"N";s:3:"醙";s:1:"N";s:3:"鉶";s:1:"N";s:3:"陼";s:1:"N";s:3:"難";s:1:"N";s:3:"靖";s:1:"N";s:3:"韛";s:1:"N";s:3:"響";s:1:"N";s:3:"頋";s:1:"N";s:3:"頻";s:1:"N";s:3:"鬒";s:1:"N";s:3:"龜";s:1:"N";s:3:"𢡊";s:1:"N";s:3:"𢡄";s:1:"N";s:3:"𣏕";s:1:"N";s:3:"㮝";s:1:"N";s:3:"䀘";s:1:"N";s:3:"䀹";s:1:"N";s:3:"𥉉";s:1:"N";s:3:"𥳐";s:1:"N";s:3:"𧻓";s:1:"N";s:3:"齃";s:1:"N";s:3:"龎";s:1:"N";s:3:"יִ";s:1:"N";s:3:"ײַ";s:1:"N";s:3:"שׁ";s:1:"N";s:3:"שׂ";s:1:"N";s:3:"שּׁ";s:1:"N";s:3:"שּׂ";s:1:"N";s:3:"אַ";s:1:"N";s:3:"אָ";s:1:"N";s:3:"אּ";s:1:"N";s:3:"בּ";s:1:"N";s:3:"גּ";s:1:"N";s:3:"דּ";s:1:"N";s:3:"הּ";s:1:"N";s:3:"וּ";s:1:"N";s:3:"זּ";s:1:"N";s:3:"טּ";s:1:"N";s:3:"יּ";s:1:"N";s:3:"ךּ";s:1:"N";s:3:"כּ";s:1:"N";s:3:"לּ";s:1:"N";s:3:"מּ";s:1:"N";s:3:"נּ";s:1:"N";s:3:"סּ";s:1:"N";s:3:"ףּ";s:1:"N";s:3:"פּ";s:1:"N";s:3:"צּ";s:1:"N";s:3:"קּ";s:1:"N";s:3:"רּ";s:1:"N";s:3:"שּ";s:1:"N";s:3:"תּ";s:1:"N";s:3:"וֹ";s:1:"N";s:3:"בֿ";s:1:"N";s:3:"כֿ";s:1:"N";s:3:"פֿ";s:1:"N";s:4:"𝅗𝅥";s:1:"N";s:4:"𝅘𝅥";s:1:"N";s:4:"𝅘𝅥𝅮";s:1:"N";s:4:"𝅘𝅥𝅯";s:1:"N";s:4:"𝅘𝅥𝅰";s:1:"N";s:4:"𝅘𝅥𝅱";s:1:"N";s:4:"𝅘𝅥𝅲";s:1:"N";s:4:"𝆹𝅥";s:1:"N";s:4:"𝆺𝅥";s:1:"N";s:4:"𝆹𝅥𝅮";s:1:"N";s:4:"𝆺𝅥𝅮";s:1:"N";s:4:"𝆹𝅥𝅯";s:1:"N";s:4:"𝆺𝅥𝅯";s:1:"N";s:4:"丽";s:1:"N";s:4:"丸";s:1:"N";s:4:"乁";s:1:"N";s:4:"𠄢";s:1:"N";s:4:"你";s:1:"N";s:4:"侮";s:1:"N";s:4:"侻";s:1:"N";s:4:"倂";s:1:"N";s:4:"偺";s:1:"N";s:4:"備";s:1:"N";s:4:"僧";s:1:"N";s:4:"像";s:1:"N";s:4:"㒞";s:1:"N";s:4:"𠘺";s:1:"N";s:4:"免";s:1:"N";s:4:"兔";s:1:"N";s:4:"兤";s:1:"N";s:4:"具";s:1:"N";s:4:"𠔜";s:1:"N";s:4:"㒹";s:1:"N";s:4:"內";s:1:"N";s:4:"再";s:1:"N";s:4:"𠕋";s:1:"N";s:4:"冗";s:1:"N";s:4:"冤";s:1:"N";s:4:"仌";s:1:"N";s:4:"冬";s:1:"N";s:4:"况";s:1:"N";s:4:"𩇟";s:1:"N";s:4:"凵";s:1:"N";s:4:"刃";s:1:"N";s:4:"㓟";s:1:"N";s:4:"刻";s:1:"N";s:4:"剆";s:1:"N";s:4:"割";s:1:"N";s:4:"剷";s:1:"N";s:4:"㔕";s:1:"N";s:4:"勇";s:1:"N";s:4:"勉";s:1:"N";s:4:"勤";s:1:"N";s:4:"勺";s:1:"N";s:4:"包";s:1:"N";s:4:"匆";s:1:"N";s:4:"北";s:1:"N";s:4:"卉";s:1:"N";s:4:"卑";s:1:"N";s:4:"博";s:1:"N";s:4:"即";s:1:"N";s:4:"卽";s:1:"N";s:4:"卿";s:1:"N";s:4:"卿";s:1:"N";s:4:"卿";s:1:"N";s:4:"𠨬";s:1:"N";s:4:"灰";s:1:"N";s:4:"及";s:1:"N";s:4:"叟";s:1:"N";s:4:"𠭣";s:1:"N";s:4:"叫";s:1:"N";s:4:"叱";s:1:"N";s:4:"吆";s:1:"N";s:4:"咞";s:1:"N";s:4:"吸";s:1:"N";s:4:"呈";s:1:"N";s:4:"周";s:1:"N";s:4:"咢";s:1:"N";s:4:"哶";s:1:"N";s:4:"唐";s:1:"N";s:4:"啓";s:1:"N";s:4:"啣";s:1:"N";s:4:"善";s:1:"N";s:4:"善";s:1:"N";s:4:"喙";s:1:"N";s:4:"喫";s:1:"N";s:4:"喳";s:1:"N";s:4:"嗂";s:1:"N";s:4:"圖";s:1:"N";s:4:"嘆";s:1:"N";s:4:"圗";s:1:"N";s:4:"噑";s:1:"N";s:4:"噴";s:1:"N";s:4:"切";s:1:"N";s:4:"壮";s:1:"N";s:4:"城";s:1:"N";s:4:"埴";s:1:"N";s:4:"堍";s:1:"N";s:4:"型";s:1:"N";s:4:"堲";s:1:"N";s:4:"報";s:1:"N";s:4:"墬";s:1:"N";s:4:"𡓤";s:1:"N";s:4:"売";s:1:"N";s:4:"壷";s:1:"N";s:4:"夆";s:1:"N";s:4:"多";s:1:"N";s:4:"夢";s:1:"N";s:4:"奢";s:1:"N";s:4:"𡚨";s:1:"N";s:4:"𡛪";s:1:"N";s:4:"姬";s:1:"N";s:4:"娛";s:1:"N";s:4:"娧";s:1:"N";s:4:"姘";s:1:"N";s:4:"婦";s:1:"N";s:4:"㛮";s:1:"N";s:4:"㛼";s:1:"N";s:4:"嬈";s:1:"N";s:4:"嬾";s:1:"N";s:4:"嬾";s:1:"N";s:4:"𡧈";s:1:"N";s:4:"寃";s:1:"N";s:4:"寘";s:1:"N";s:4:"寧";s:1:"N";s:4:"寳";s:1:"N";s:4:"𡬘";s:1:"N";s:4:"寿";s:1:"N";s:4:"将";s:1:"N";s:4:"当";s:1:"N";s:4:"尢";s:1:"N";s:4:"㞁";s:1:"N";s:4:"屠";s:1:"N";s:4:"屮";s:1:"N";s:4:"峀";s:1:"N";s:4:"岍";s:1:"N";s:4:"𡷤";s:1:"N";s:4:"嵃";s:1:"N";s:4:"𡷦";s:1:"N";s:4:"嵮";s:1:"N";s:4:"嵫";s:1:"N";s:4:"嵼";s:1:"N";s:4:"巡";s:1:"N";s:4:"巢";s:1:"N";s:4:"㠯";s:1:"N";s:4:"巽";s:1:"N";s:4:"帨";s:1:"N";s:4:"帽";s:1:"N";s:4:"幩";s:1:"N";s:4:"㡢";s:1:"N";s:4:"𢆃";s:1:"N";s:4:"㡼";s:1:"N";s:4:"庰";s:1:"N";s:4:"庳";s:1:"N";s:4:"庶";s:1:"N";s:4:"廊";s:1:"N";s:4:"𪎒";s:1:"N";s:4:"廾";s:1:"N";s:4:"𢌱";s:1:"N";s:4:"𢌱";s:1:"N";s:4:"舁";s:1:"N";s:4:"弢";s:1:"N";s:4:"弢";s:1:"N";s:4:"㣇";s:1:"N";s:4:"𣊸";s:1:"N";s:4:"𦇚";s:1:"N";s:4:"形";s:1:"N";s:4:"彫";s:1:"N";s:4:"㣣";s:1:"N";s:4:"徚";s:1:"N";s:4:"忍";s:1:"N";s:4:"志";s:1:"N";s:4:"忹";s:1:"N";s:4:"悁";s:1:"N";s:4:"㤺";s:1:"N";s:4:"㤜";s:1:"N";s:4:"悔";s:1:"N";s:4:"𢛔";s:1:"N";s:4:"惇";s:1:"N";s:4:"慈";s:1:"N";s:4:"慌";s:1:"N";s:4:"慎";s:1:"N";s:4:"慌";s:1:"N";s:4:"慺";s:1:"N";s:4:"憎";s:1:"N";s:4:"憲";s:1:"N";s:4:"憤";s:1:"N";s:4:"憯";s:1:"N";s:4:"懞";s:1:"N";s:4:"懲";s:1:"N";s:4:"懶";s:1:"N";s:4:"成";s:1:"N";s:4:"戛";s:1:"N";s:4:"扝";s:1:"N";s:4:"抱";s:1:"N";s:4:"拔";s:1:"N";s:4:"捐";s:1:"N";s:4:"𢬌";s:1:"N";s:4:"挽";s:1:"N";s:4:"拼";s:1:"N";s:4:"捨";s:1:"N";s:4:"掃";s:1:"N";s:4:"揤";s:1:"N";s:4:"𢯱";s:1:"N";s:4:"搢";s:1:"N";s:4:"揅";s:1:"N";s:4:"掩";s:1:"N";s:4:"㨮";s:1:"N";s:4:"摩";s:1:"N";s:4:"摾";s:1:"N";s:4:"撝";s:1:"N";s:4:"摷";s:1:"N";s:4:"㩬";s:1:"N";s:4:"敏";s:1:"N";s:4:"敬";s:1:"N";s:4:"𣀊";s:1:"N";s:4:"旣";s:1:"N";s:4:"書";s:1:"N";s:4:"晉";s:1:"N";s:4:"㬙";s:1:"N";s:4:"暑";s:1:"N";s:4:"㬈";s:1:"N";s:4:"㫤";s:1:"N";s:4:"冒";s:1:"N";s:4:"冕";s:1:"N";s:4:"最";s:1:"N";s:4:"暜";s:1:"N";s:4:"肭";s:1:"N";s:4:"䏙";s:1:"N";s:4:"朗";s:1:"N";s:4:"望";s:1:"N";s:4:"朡";s:1:"N";s:4:"杞";s:1:"N";s:4:"杓";s:1:"N";s:4:"𣏃";s:1:"N";s:4:"㭉";s:1:"N";s:4:"柺";s:1:"N";s:4:"枅";s:1:"N";s:4:"桒";s:1:"N";s:4:"梅";s:1:"N";s:4:"𣑭";s:1:"N";s:4:"梎";s:1:"N";s:4:"栟";s:1:"N";s:4:"椔";s:1:"N";s:4:"㮝";s:1:"N";s:4:"楂";s:1:"N";s:4:"榣";s:1:"N";s:4:"槪";s:1:"N";s:4:"檨";s:1:"N";s:4:"𣚣";s:1:"N";s:4:"櫛";s:1:"N";s:4:"㰘";s:1:"N";s:4:"次";s:1:"N";s:4:"𣢧";s:1:"N";s:4:"歔";s:1:"N";s:4:"㱎";s:1:"N";s:4:"歲";s:1:"N";s:4:"殟";s:1:"N";s:4:"殺";s:1:"N";s:4:"殻";s:1:"N";s:4:"𣪍";s:1:"N";s:4:"𡴋";s:1:"N";s:4:"𣫺";s:1:"N";s:4:"汎";s:1:"N";s:4:"𣲼";s:1:"N";s:4:"沿";s:1:"N";s:4:"泍";s:1:"N";s:4:"汧";s:1:"N";s:4:"洖";s:1:"N";s:4:"派";s:1:"N";s:4:"海";s:1:"N";s:4:"流";s:1:"N";s:4:"浩";s:1:"N";s:4:"浸";s:1:"N";s:4:"涅";s:1:"N";s:4:"𣴞";s:1:"N";s:4:"洴";s:1:"N";s:4:"港";s:1:"N";s:4:"湮";s:1:"N";s:4:"㴳";s:1:"N";s:4:"滋";s:1:"N";s:4:"滇";s:1:"N";s:4:"𣻑";s:1:"N";s:4:"淹";s:1:"N";s:4:"潮";s:1:"N";s:4:"𣽞";s:1:"N";s:4:"𣾎";s:1:"N";s:4:"濆";s:1:"N";s:4:"瀹";s:1:"N";s:4:"瀞";s:1:"N";s:4:"瀛";s:1:"N";s:4:"㶖";s:1:"N";s:4:"灊";s:1:"N";s:4:"災";s:1:"N";s:4:"灷";s:1:"N";s:4:"炭";s:1:"N";s:4:"𠔥";s:1:"N";s:4:"煅";s:1:"N";s:4:"𤉣";s:1:"N";s:4:"熜";s:1:"N";s:4:"𤎫";s:1:"N";s:4:"爨";s:1:"N";s:4:"爵";s:1:"N";s:4:"牐";s:1:"N";s:4:"𤘈";s:1:"N";s:4:"犀";s:1:"N";s:4:"犕";s:1:"N";s:4:"𤜵";s:1:"N";s:4:"𤠔";s:1:"N";s:4:"獺";s:1:"N";s:4:"王";s:1:"N";s:4:"㺬";s:1:"N";s:4:"玥";s:1:"N";s:4:"㺸";s:1:"N";s:4:"㺸";s:1:"N";s:4:"瑇";s:1:"N";s:4:"瑜";s:1:"N";s:4:"瑱";s:1:"N";s:4:"璅";s:1:"N";s:4:"瓊";s:1:"N";s:4:"㼛";s:1:"N";s:4:"甤";s:1:"N";s:4:"𤰶";s:1:"N";s:4:"甾";s:1:"N";s:4:"𤲒";s:1:"N";s:4:"異";s:1:"N";s:4:"𢆟";s:1:"N";s:4:"瘐";s:1:"N";s:4:"𤾡";s:1:"N";s:4:"𤾸";s:1:"N";s:4:"𥁄";s:1:"N";s:4:"㿼";s:1:"N";s:4:"䀈";s:1:"N";s:4:"直";s:1:"N";s:4:"𥃳";s:1:"N";s:4:"𥃲";s:1:"N";s:4:"𥄙";s:1:"N";s:4:"𥄳";s:1:"N";s:4:"眞";s:1:"N";s:4:"真";s:1:"N";s:4:"真";s:1:"N";s:4:"睊";s:1:"N";s:4:"䀹";s:1:"N";s:4:"瞋";s:1:"N";s:4:"䁆";s:1:"N";s:4:"䂖";s:1:"N";s:4:"𥐝";s:1:"N";s:4:"硎";s:1:"N";s:4:"碌";s:1:"N";s:4:"磌";s:1:"N";s:4:"䃣";s:1:"N";s:4:"𥘦";s:1:"N";s:4:"祖";s:1:"N";s:4:"𥚚";s:1:"N";s:4:"𥛅";s:1:"N";s:4:"福";s:1:"N";s:4:"秫";s:1:"N";s:4:"䄯";s:1:"N";s:4:"穀";s:1:"N";s:4:"穊";s:1:"N";s:4:"穏";s:1:"N";s:4:"𥥼";s:1:"N";s:4:"𥪧";s:1:"N";s:4:"𥪧";s:1:"N";s:4:"竮";s:1:"N";s:4:"䈂";s:1:"N";s:4:"𥮫";s:1:"N";s:4:"篆";s:1:"N";s:4:"築";s:1:"N";s:4:"䈧";s:1:"N";s:4:"𥲀";s:1:"N";s:4:"糒";s:1:"N";s:4:"䊠";s:1:"N";s:4:"糨";s:1:"N";s:4:"糣";s:1:"N";s:4:"紀";s:1:"N";s:4:"𥾆";s:1:"N";s:4:"絣";s:1:"N";s:4:"䌁";s:1:"N";s:4:"緇";s:1:"N";s:4:"縂";s:1:"N";s:4:"繅";s:1:"N";s:4:"䌴";s:1:"N";s:4:"𦈨";s:1:"N";s:4:"𦉇";s:1:"N";s:4:"䍙";s:1:"N";s:4:"𦋙";s:1:"N";s:4:"罺";s:1:"N";s:4:"𦌾";s:1:"N";s:4:"羕";s:1:"N";s:4:"翺";s:1:"N";s:4:"者";s:1:"N";s:4:"𦓚";s:1:"N";s:4:"𦔣";s:1:"N";s:4:"聠";s:1:"N";s:4:"𦖨";s:1:"N";s:4:"聰";s:1:"N";s:4:"𣍟";s:1:"N";s:4:"䏕";s:1:"N";s:4:"育";s:1:"N";s:4:"脃";s:1:"N";s:4:"䐋";s:1:"N";s:4:"脾";s:1:"N";s:4:"媵";s:1:"N";s:4:"𦞧";s:1:"N";s:4:"𦞵";s:1:"N";s:4:"𣎓";s:1:"N";s:4:"𣎜";s:1:"N";s:4:"舁";s:1:"N";s:4:"舄";s:1:"N";s:4:"辞";s:1:"N";s:4:"䑫";s:1:"N";s:4:"芑";s:1:"N";s:4:"芋";s:1:"N";s:4:"芝";s:1:"N";s:4:"劳";s:1:"N";s:4:"花";s:1:"N";s:4:"芳";s:1:"N";s:4:"芽";s:1:"N";s:4:"苦";s:1:"N";s:4:"𦬼";s:1:"N";s:4:"若";s:1:"N";s:4:"茝";s:1:"N";s:4:"荣";s:1:"N";s:4:"莭";s:1:"N";s:4:"茣";s:1:"N";s:4:"莽";s:1:"N";s:4:"菧";s:1:"N";s:4:"著";s:1:"N";s:4:"荓";s:1:"N";s:4:"菊";s:1:"N";s:4:"菌";s:1:"N";s:4:"菜";s:1:"N";s:4:"𦰶";s:1:"N";s:4:"𦵫";s:1:"N";s:4:"𦳕";s:1:"N";s:4:"䔫";s:1:"N";s:4:"蓱";s:1:"N";s:4:"蓳";s:1:"N";s:4:"蔖";s:1:"N";s:4:"𧏊";s:1:"N";s:4:"蕤";s:1:"N";s:4:"𦼬";s:1:"N";s:4:"䕝";s:1:"N";s:4:"䕡";s:1:"N";s:4:"𦾱";s:1:"N";s:4:"𧃒";s:1:"N";s:4:"䕫";s:1:"N";s:4:"虐";s:1:"N";s:4:"虜";s:1:"N";s:4:"虧";s:1:"N";s:4:"虩";s:1:"N";s:4:"蚩";s:1:"N";s:4:"蚈";s:1:"N";s:4:"蜎";s:1:"N";s:4:"蛢";s:1:"N";s:4:"蝹";s:1:"N";s:4:"蜨";s:1:"N";s:4:"蝫";s:1:"N";s:4:"螆";s:1:"N";s:4:"䗗";s:1:"N";s:4:"蟡";s:1:"N";s:4:"蠁";s:1:"N";s:4:"䗹";s:1:"N";s:4:"衠";s:1:"N";s:4:"衣";s:1:"N";s:4:"𧙧";s:1:"N";s:4:"裗";s:1:"N";s:4:"裞";s:1:"N";s:4:"䘵";s:1:"N";s:4:"裺";s:1:"N";s:4:"㒻";s:1:"N";s:4:"𧢮";s:1:"N";s:4:"𧥦";s:1:"N";s:4:"䚾";s:1:"N";s:4:"䛇";s:1:"N";s:4:"誠";s:1:"N";s:4:"諭";s:1:"N";s:4:"變";s:1:"N";s:4:"豕";s:1:"N";s:4:"𧲨";s:1:"N";s:4:"貫";s:1:"N";s:4:"賁";s:1:"N";s:4:"贛";s:1:"N";s:4:"起";s:1:"N";s:4:"𧼯";s:1:"N";s:4:"𠠄";s:1:"N";s:4:"跋";s:1:"N";s:4:"趼";s:1:"N";s:4:"跰";s:1:"N";s:4:"𠣞";s:1:"N";s:4:"軔";s:1:"N";s:4:"輸";s:1:"N";s:4:"𨗒";s:1:"N";s:4:"𨗭";s:1:"N";s:4:"邔";s:1:"N";s:4:"郱";s:1:"N";s:4:"鄑";s:1:"N";s:4:"𨜮";s:1:"N";s:4:"鄛";s:1:"N";s:4:"鈸";s:1:"N";s:4:"鋗";s:1:"N";s:4:"鋘";s:1:"N";s:4:"鉼";s:1:"N";s:4:"鏹";s:1:"N";s:4:"鐕";s:1:"N";s:4:"𨯺";s:1:"N";s:4:"開";s:1:"N";s:4:"䦕";s:1:"N";s:4:"閷";s:1:"N";s:4:"𨵷";s:1:"N";s:4:"䧦";s:1:"N";s:4:"雃";s:1:"N";s:4:"嶲";s:1:"N";s:4:"霣";s:1:"N";s:4:"𩅅";s:1:"N";s:4:"𩈚";s:1:"N";s:4:"䩮";s:1:"N";s:4:"䩶";s:1:"N";s:4:"韠";s:1:"N";s:4:"𩐊";s:1:"N";s:4:"䪲";s:1:"N";s:4:"𩒖";s:1:"N";s:4:"頋";s:1:"N";s:4:"頋";s:1:"N";s:4:"頩";s:1:"N";s:4:"𩖶";s:1:"N";s:4:"飢";s:1:"N";s:4:"䬳";s:1:"N";s:4:"餩";s:1:"N";s:4:"馧";s:1:"N";s:4:"駂";s:1:"N";s:4:"駾";s:1:"N";s:4:"䯎";s:1:"N";s:4:"𩬰";s:1:"N";s:4:"鬒";s:1:"N";s:4:"鱀";s:1:"N";s:4:"鳽";s:1:"N";s:4:"䳎";s:1:"N";s:4:"䳭";s:1:"N";s:4:"鵧";s:1:"N";s:4:"𪃎";s:1:"N";s:4:"䳸";s:1:"N";s:4:"𪄅";s:1:"N";s:4:"𪈎";s:1:"N";s:4:"𪊑";s:1:"N";s:4:"麻";s:1:"N";s:4:"䵖";s:1:"N";s:4:"黹";s:1:"N";s:4:"黾";s:1:"N";s:4:"鼅";s:1:"N";s:4:"鼏";s:1:"N";s:4:"鼖";s:1:"N";s:4:"鼻";s:1:"N";s:4:"𪘀";s:1:"N";s:2:"̀";s:1:"M";s:2:"́";s:1:"M";s:2:"̂";s:1:"M";s:2:"̃";s:1:"M";s:2:"̄";s:1:"M";s:2:"̆";s:1:"M";s:2:"̇";s:1:"M";s:2:"̈";s:1:"M";s:2:"̉";s:1:"M";s:2:"̊";s:1:"M";s:2:"̋";s:1:"M";s:2:"̌";s:1:"M";s:2:"̏";s:1:"M";s:2:"̑";s:1:"M";s:2:"̓";s:1:"M";s:2:"̔";s:1:"M";s:2:"̛";s:1:"M";s:2:"̣";s:1:"M";s:2:"̤";s:1:"M";s:2:"̥";s:1:"M";s:2:"̦";s:1:"M";s:2:"̧";s:1:"M";s:2:"̨";s:1:"M";s:2:"̭";s:1:"M";s:2:"̮";s:1:"M";s:2:"̰";s:1:"M";s:2:"̱";s:1:"M";s:2:"̸";s:1:"M";s:2:"͂";s:1:"M";s:2:"ͅ";s:1:"M";s:2:"ٓ";s:1:"M";s:2:"ٔ";s:1:"M";s:2:"ٕ";s:1:"M";s:3:"़";s:1:"M";s:3:"া";s:1:"M";s:3:"ৗ";s:1:"M";s:3:"ା";s:1:"M";s:3:"ୖ";s:1:"M";s:3:"ୗ";s:1:"M";s:3:"ா";s:1:"M";s:3:"ௗ";s:1:"M";s:3:"ౖ";s:1:"M";s:3:"ೂ";s:1:"M";s:3:"ೕ";s:1:"M";s:3:"ೖ";s:1:"M";s:3:"ാ";s:1:"M";s:3:"ൗ";s:1:"M";s:3:"්";s:1:"M";s:3:"ා";s:1:"M";s:3:"ෟ";s:1:"M";s:3:"ီ";s:1:"M";s:3:"ᅡ";s:1:"M";s:3:"ᅢ";s:1:"M";s:3:"ᅣ";s:1:"M";s:3:"ᅤ";s:1:"M";s:3:"ᅥ";s:1:"M";s:3:"ᅦ";s:1:"M";s:3:"ᅧ";s:1:"M";s:3:"ᅨ";s:1:"M";s:3:"ᅩ";s:1:"M";s:3:"ᅪ";s:1:"M";s:3:"ᅫ";s:1:"M";s:3:"ᅬ";s:1:"M";s:3:"ᅭ";s:1:"M";s:3:"ᅮ";s:1:"M";s:3:"ᅯ";s:1:"M";s:3:"ᅰ";s:1:"M";s:3:"ᅱ";s:1:"M";s:3:"ᅲ";s:1:"M";s:3:"ᅳ";s:1:"M";s:3:"ᅴ";s:1:"M";s:3:"ᅵ";s:1:"M";s:3:"ᆨ";s:1:"M";s:3:"ᆩ";s:1:"M";s:3:"ᆪ";s:1:"M";s:3:"ᆫ";s:1:"M";s:3:"ᆬ";s:1:"M";s:3:"ᆭ";s:1:"M";s:3:"ᆮ";s:1:"M";s:3:"ᆯ";s:1:"M";s:3:"ᆰ";s:1:"M";s:3:"ᆱ";s:1:"M";s:3:"ᆲ";s:1:"M";s:3:"ᆳ";s:1:"M";s:3:"ᆴ";s:1:"M";s:3:"ᆵ";s:1:"M";s:3:"ᆶ";s:1:"M";s:3:"ᆷ";s:1:"M";s:3:"ᆸ";s:1:"M";s:3:"ᆹ";s:1:"M";s:3:"ᆺ";s:1:"M";s:3:"ᆻ";s:1:"M";s:3:"ᆼ";s:1:"M";s:3:"ᆽ";s:1:"M";s:3:"ᆾ";s:1:"M";s:3:"ᆿ";s:1:"M";s:3:"ᇀ";s:1:"M";s:3:"ᇁ";s:1:"M";s:3:"ᇂ";s:1:"M";s:3:"゙";s:1:"M";s:3:"゚";s:1:"M";}' );
+?>
diff --git a/includes/normal/UtfNormalDataK.inc b/includes/normal/UtfNormalDataK.inc
new file mode 100644
index 00000000..0f4cd7a5
--- /dev/null
+++ b/includes/normal/UtfNormalDataK.inc
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file was automatically generated -- do not edit!
+ * Run UtfNormalGenerate.php to create this file again (make clean && make)
+ * @package MediaWiki
+ */
+/** */
+global $utfCompatibilityDecomp;
+$utfCompatibilityDecomp = unserialize( 'a:5389:{s:2:" ";s:1:" ";s:2:"¨";s:3:" ̈";s:2:"ª";s:1:"a";s:2:"¯";s:3:" ̄";s:2:"²";s:1:"2";s:2:"³";s:1:"3";s:2:"´";s:3:" ́";s:2:"µ";s:2:"μ";s:2:"¸";s:3:" ̧";s:2:"¹";s:1:"1";s:2:"º";s:1:"o";s:2:"¼";s:5:"1⁄4";s:2:"½";s:5:"1⁄2";s:2:"¾";s:5:"3⁄4";s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"IJ";s:2:"IJ";s:2:"ij";s:2:"ij";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ŀ";s:3:"L·";s:2:"ŀ";s:3:"l·";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"ʼn";s:3:"ʼn";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"ſ";s:1:"s";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"DŽ";s:4:"DŽ";s:2:"Dž";s:4:"Dž";s:2:"dž";s:4:"dž";s:2:"LJ";s:2:"LJ";s:2:"Lj";s:2:"Lj";s:2:"lj";s:2:"lj";s:2:"NJ";s:2:"NJ";s:2:"Nj";s:2:"Nj";s:2:"nj";s:2:"nj";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s:3:"Ǐ";s:2:"ǐ";s:3:"ǐ";s:2:"Ǒ";s:3:"Ǒ";s:2:"ǒ";s:3:"ǒ";s:2:"Ǔ";s:3:"Ǔ";s:2:"ǔ";s:3:"ǔ";s:2:"Ǖ";s:5:"Ǖ";s:2:"ǖ";s:5:"ǖ";s:2:"Ǘ";s:5:"Ǘ";s:2:"ǘ";s:5:"ǘ";s:2:"Ǚ";s:5:"Ǚ";s:2:"ǚ";s:5:"ǚ";s:2:"Ǜ";s:5:"Ǜ";s:2:"ǜ";s:5:"ǜ";s:2:"Ǟ";s:5:"Ǟ";s:2:"ǟ";s:5:"ǟ";s:2:"Ǡ";s:5:"Ǡ";s:2:"ǡ";s:5:"ǡ";s:2:"Ǣ";s:4:"Ǣ";s:2:"ǣ";s:4:"ǣ";s:2:"Ǧ";s:3:"Ǧ";s:2:"ǧ";s:3:"ǧ";s:2:"Ǩ";s:3:"Ǩ";s:2:"ǩ";s:3:"ǩ";s:2:"Ǫ";s:3:"Ǫ";s:2:"ǫ";s:3:"ǫ";s:2:"Ǭ";s:5:"Ǭ";s:2:"ǭ";s:5:"ǭ";s:2:"Ǯ";s:4:"Ǯ";s:2:"ǯ";s:4:"ǯ";s:2:"ǰ";s:3:"ǰ";s:2:"DZ";s:2:"DZ";s:2:"Dz";s:2:"Dz";s:2:"dz";s:2:"dz";s:2:"Ǵ";s:3:"Ǵ";s:2:"ǵ";s:3:"ǵ";s:2:"Ǹ";s:3:"Ǹ";s:2:"ǹ";s:3:"ǹ";s:2:"Ǻ";s:5:"Ǻ";s:2:"ǻ";s:5:"ǻ";s:2:"Ǽ";s:4:"Ǽ";s:2:"ǽ";s:4:"ǽ";s:2:"Ǿ";s:4:"Ǿ";s:2:"ǿ";s:4:"ǿ";s:2:"Ȁ";s:3:"Ȁ";s:2:"ȁ";s:3:"ȁ";s:2:"Ȃ";s:3:"Ȃ";s:2:"ȃ";s:3:"ȃ";s:2:"Ȅ";s:3:"Ȅ";s:2:"ȅ";s:3:"ȅ";s:2:"Ȇ";s:3:"Ȇ";s:2:"ȇ";s:3:"ȇ";s:2:"Ȉ";s:3:"Ȉ";s:2:"ȉ";s:3:"ȉ";s:2:"Ȋ";s:3:"Ȋ";s:2:"ȋ";s:3:"ȋ";s:2:"Ȍ";s:3:"Ȍ";s:2:"ȍ";s:3:"ȍ";s:2:"Ȏ";s:3:"Ȏ";s:2:"ȏ";s:3:"ȏ";s:2:"Ȑ";s:3:"Ȑ";s:2:"ȑ";s:3:"ȑ";s:2:"Ȓ";s:3:"Ȓ";s:2:"ȓ";s:3:"ȓ";s:2:"Ȕ";s:3:"Ȕ";s:2:"ȕ";s:3:"ȕ";s:2:"Ȗ";s:3:"Ȗ";s:2:"ȗ";s:3:"ȗ";s:2:"Ș";s:3:"Ș";s:2:"ș";s:3:"ș";s:2:"Ț";s:3:"Ț";s:2:"ț";s:3:"ț";s:2:"Ȟ";s:3:"Ȟ";s:2:"ȟ";s:3:"ȟ";s:2:"Ȧ";s:3:"Ȧ";s:2:"ȧ";s:3:"ȧ";s:2:"Ȩ";s:3:"Ȩ";s:2:"ȩ";s:3:"ȩ";s:2:"Ȫ";s:5:"Ȫ";s:2:"ȫ";s:5:"ȫ";s:2:"Ȭ";s:5:"Ȭ";s:2:"ȭ";s:5:"ȭ";s:2:"Ȯ";s:3:"Ȯ";s:2:"ȯ";s:3:"ȯ";s:2:"Ȱ";s:5:"Ȱ";s:2:"ȱ";s:5:"ȱ";s:2:"Ȳ";s:3:"Ȳ";s:2:"ȳ";s:3:"ȳ";s:2:"ʰ";s:1:"h";s:2:"ʱ";s:2:"ɦ";s:2:"ʲ";s:1:"j";s:2:"ʳ";s:1:"r";s:2:"ʴ";s:2:"ɹ";s:2:"ʵ";s:2:"ɻ";s:2:"ʶ";s:2:"ʁ";s:2:"ʷ";s:1:"w";s:2:"ʸ";s:1:"y";s:2:"˘";s:3:" ̆";s:2:"˙";s:3:" ̇";s:2:"˚";s:3:" ̊";s:2:"˛";s:3:" ̨";s:2:"˜";s:3:" ̃";s:2:"˝";s:3:" ̋";s:2:"ˠ";s:2:"ɣ";s:2:"ˡ";s:1:"l";s:2:"ˢ";s:1:"s";s:2:"ˣ";s:1:"x";s:2:"ˤ";s:2:"ʕ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:2:"̈́";s:4:"̈́";s:2:"ʹ";s:2:"ʹ";s:2:"ͺ";s:3:" ͅ";s:2:";";s:1:";";s:2:"΄";s:3:" ́";s:2:"΅";s:5:" ̈́";s:2:"Ά";s:4:"Ά";s:2:"·";s:2:"·";s:2:"Έ";s:4:"Έ";s:2:"Ή";s:4:"Ή";s:2:"Ί";s:4:"Ί";s:2:"Ό";s:4:"Ό";s:2:"Ύ";s:4:"Ύ";s:2:"Ώ";s:4:"Ώ";s:2:"ΐ";s:6:"ΐ";s:2:"Ϊ";s:4:"Ϊ";s:2:"Ϋ";s:4:"Ϋ";s:2:"ά";s:4:"ά";s:2:"έ";s:4:"έ";s:2:"ή";s:4:"ή";s:2:"ί";s:4:"ί";s:2:"ΰ";s:6:"ΰ";s:2:"ϊ";s:4:"ϊ";s:2:"ϋ";s:4:"ϋ";s:2:"ό";s:4:"ό";s:2:"ύ";s:4:"ύ";s:2:"ώ";s:4:"ώ";s:2:"ϐ";s:2:"β";s:2:"ϑ";s:2:"θ";s:2:"ϒ";s:2:"Υ";s:2:"ϓ";s:4:"Ύ";s:2:"ϔ";s:4:"Ϋ";s:2:"ϕ";s:2:"φ";s:2:"ϖ";s:2:"π";s:2:"ϰ";s:2:"κ";s:2:"ϱ";s:2:"ρ";s:2:"ϲ";s:2:"ς";s:2:"ϴ";s:2:"Θ";s:2:"ϵ";s:2:"ε";s:2:"Ϲ";s:2:"Σ";s:2:"Ѐ";s:4:"Ѐ";s:2:"Ё";s:4:"Ё";s:2:"Ѓ";s:4:"Ѓ";s:2:"Ї";s:4:"Ї";s:2:"Ќ";s:4:"Ќ";s:2:"Ѝ";s:4:"Ѝ";s:2:"Ў";s:4:"Ў";s:2:"Й";s:4:"Й";s:2:"й";s:4:"й";s:2:"ѐ";s:4:"ѐ";s:2:"ё";s:4:"ё";s:2:"ѓ";s:4:"ѓ";s:2:"ї";s:4:"ї";s:2:"ќ";s:4:"ќ";s:2:"ѝ";s:4:"ѝ";s:2:"ў";s:4:"ў";s:2:"Ѷ";s:4:"Ѷ";s:2:"ѷ";s:4:"ѷ";s:2:"Ӂ";s:4:"Ӂ";s:2:"ӂ";s:4:"ӂ";s:2:"Ӑ";s:4:"Ӑ";s:2:"ӑ";s:4:"ӑ";s:2:"Ӓ";s:4:"Ӓ";s:2:"ӓ";s:4:"ӓ";s:2:"Ӗ";s:4:"Ӗ";s:2:"ӗ";s:4:"ӗ";s:2:"Ӛ";s:4:"Ӛ";s:2:"ӛ";s:4:"ӛ";s:2:"Ӝ";s:4:"Ӝ";s:2:"ӝ";s:4:"ӝ";s:2:"Ӟ";s:4:"Ӟ";s:2:"ӟ";s:4:"ӟ";s:2:"Ӣ";s:4:"Ӣ";s:2:"ӣ";s:4:"ӣ";s:2:"Ӥ";s:4:"Ӥ";s:2:"ӥ";s:4:"ӥ";s:2:"Ӧ";s:4:"Ӧ";s:2:"ӧ";s:4:"ӧ";s:2:"Ӫ";s:4:"Ӫ";s:2:"ӫ";s:4:"ӫ";s:2:"Ӭ";s:4:"Ӭ";s:2:"ӭ";s:4:"ӭ";s:2:"Ӯ";s:4:"Ӯ";s:2:"ӯ";s:4:"ӯ";s:2:"Ӱ";s:4:"Ӱ";s:2:"ӱ";s:4:"ӱ";s:2:"Ӳ";s:4:"Ӳ";s:2:"ӳ";s:4:"ӳ";s:2:"Ӵ";s:4:"Ӵ";s:2:"ӵ";s:4:"ӵ";s:2:"Ӹ";s:4:"Ӹ";s:2:"ӹ";s:4:"ӹ";s:2:"և";s:4:"եւ";s:2:"آ";s:4:"آ";s:2:"أ";s:4:"أ";s:2:"ؤ";s:4:"ؤ";s:2:"إ";s:4:"إ";s:2:"ئ";s:4:"ئ";s:2:"ٵ";s:4:"اٴ";s:2:"ٶ";s:4:"وٴ";s:2:"ٷ";s:4:"ۇٴ";s:2:"ٸ";s:4:"يٴ";s:2:"ۀ";s:4:"ۀ";s:2:"ۂ";s:4:"ۂ";s:2:"ۓ";s:4:"ۓ";s:3:"ऩ";s:6:"ऩ";s:3:"ऱ";s:6:"ऱ";s:3:"ऴ";s:6:"ऴ";s:3:"क़";s:6:"क़";s:3:"ख़";s:6:"ख़";s:3:"ग़";s:6:"ग़";s:3:"ज़";s:6:"ज़";s:3:"ड़";s:6:"ड़";s:3:"ढ़";s:6:"ढ़";s:3:"फ़";s:6:"फ़";s:3:"य़";s:6:"य़";s:3:"ো";s:6:"ো";s:3:"ৌ";s:6:"ৌ";s:3:"ড়";s:6:"ড়";s:3:"ঢ়";s:6:"ঢ়";s:3:"য়";s:6:"য়";s:3:"ਲ਼";s:6:"ਲ਼";s:3:"ਸ਼";s:6:"ਸ਼";s:3:"ਖ਼";s:6:"ਖ਼";s:3:"ਗ਼";s:6:"ਗ਼";s:3:"ਜ਼";s:6:"ਜ਼";s:3:"ਫ਼";s:6:"ਫ਼";s:3:"ୈ";s:6:"ୈ";s:3:"ୋ";s:6:"ୋ";s:3:"ୌ";s:6:"ୌ";s:3:"ଡ଼";s:6:"ଡ଼";s:3:"ଢ଼";s:6:"ଢ଼";s:3:"ஔ";s:6:"ஔ";s:3:"ொ";s:6:"ொ";s:3:"ோ";s:6:"ோ";s:3:"ௌ";s:6:"ௌ";s:3:"ై";s:6:"ై";s:3:"ೀ";s:6:"ೀ";s:3:"ೇ";s:6:"ೇ";s:3:"ೈ";s:6:"ೈ";s:3:"ೊ";s:6:"ೊ";s:3:"ೋ";s:9:"ೋ";s:3:"ൊ";s:6:"ൊ";s:3:"ോ";s:6:"ോ";s:3:"ൌ";s:6:"ൌ";s:3:"ේ";s:6:"ේ";s:3:"ො";s:6:"ො";s:3:"ෝ";s:9:"ෝ";s:3:"ෞ";s:6:"ෞ";s:3:"ำ";s:6:"ํา";s:3:"ຳ";s:6:"ໍາ";s:3:"ໜ";s:6:"ຫນ";s:3:"ໝ";s:6:"ຫມ";s:3:"༌";s:3:"་";s:3:"གྷ";s:6:"གྷ";s:3:"ཌྷ";s:6:"ཌྷ";s:3:"དྷ";s:6:"དྷ";s:3:"བྷ";s:6:"བྷ";s:3:"ཛྷ";s:6:"ཛྷ";s:3:"ཀྵ";s:6:"ཀྵ";s:3:"ཱི";s:6:"ཱི";s:3:"ཱུ";s:6:"ཱུ";s:3:"ྲྀ";s:6:"ྲྀ";s:3:"ཷ";s:9:"ྲཱྀ";s:3:"ླྀ";s:6:"ླྀ";s:3:"ཹ";s:9:"ླཱྀ";s:3:"ཱྀ";s:6:"ཱྀ";s:3:"ྒྷ";s:6:"ྒྷ";s:3:"ྜྷ";s:6:"ྜྷ";s:3:"ྡྷ";s:6:"ྡྷ";s:3:"ྦྷ";s:6:"ྦྷ";s:3:"ྫྷ";s:6:"ྫྷ";s:3:"ྐྵ";s:6:"ྐྵ";s:3:"ဦ";s:6:"ဦ";s:3:"ჼ";s:3:"ნ";s:3:"ᴬ";s:1:"A";s:3:"ᴭ";s:2:"Æ";s:3:"ᴮ";s:1:"B";s:3:"ᴰ";s:1:"D";s:3:"ᴱ";s:1:"E";s:3:"ᴲ";s:2:"Ǝ";s:3:"ᴳ";s:1:"G";s:3:"ᴴ";s:1:"H";s:3:"ᴵ";s:1:"I";s:3:"ᴶ";s:1:"J";s:3:"ᴷ";s:1:"K";s:3:"ᴸ";s:1:"L";s:3:"ᴹ";s:1:"M";s:3:"ᴺ";s:1:"N";s:3:"ᴼ";s:1:"O";s:3:"ᴽ";s:2:"Ȣ";s:3:"ᴾ";s:1:"P";s:3:"ᴿ";s:1:"R";s:3:"ᵀ";s:1:"T";s:3:"ᵁ";s:1:"U";s:3:"ᵂ";s:1:"W";s:3:"ᵃ";s:1:"a";s:3:"ᵄ";s:2:"ɐ";s:3:"ᵅ";s:2:"ɑ";s:3:"ᵆ";s:3:"ᴂ";s:3:"ᵇ";s:1:"b";s:3:"ᵈ";s:1:"d";s:3:"ᵉ";s:1:"e";s:3:"ᵊ";s:2:"ə";s:3:"ᵋ";s:2:"ɛ";s:3:"ᵌ";s:2:"ɜ";s:3:"ᵍ";s:1:"g";s:3:"ᵏ";s:1:"k";s:3:"ᵐ";s:1:"m";s:3:"ᵑ";s:2:"ŋ";s:3:"ᵒ";s:1:"o";s:3:"ᵓ";s:2:"ɔ";s:3:"ᵔ";s:3:"ᴖ";s:3:"ᵕ";s:3:"ᴗ";s:3:"ᵖ";s:1:"p";s:3:"ᵗ";s:1:"t";s:3:"ᵘ";s:1:"u";s:3:"ᵙ";s:3:"ᴝ";s:3:"ᵚ";s:2:"ɯ";s:3:"ᵛ";s:1:"v";s:3:"ᵜ";s:3:"ᴥ";s:3:"ᵝ";s:2:"β";s:3:"ᵞ";s:2:"γ";s:3:"ᵟ";s:2:"δ";s:3:"ᵠ";s:2:"φ";s:3:"ᵡ";s:2:"χ";s:3:"ᵢ";s:1:"i";s:3:"ᵣ";s:1:"r";s:3:"ᵤ";s:1:"u";s:3:"ᵥ";s:1:"v";s:3:"ᵦ";s:2:"β";s:3:"ᵧ";s:2:"γ";s:3:"ᵨ";s:2:"ρ";s:3:"ᵩ";s:2:"φ";s:3:"ᵪ";s:2:"χ";s:3:"ᵸ";s:2:"н";s:3:"ᶛ";s:2:"ɒ";s:3:"ᶜ";s:1:"c";s:3:"ᶝ";s:2:"ɕ";s:3:"ᶞ";s:2:"ð";s:3:"ᶟ";s:2:"ɜ";s:3:"ᶠ";s:1:"f";s:3:"ᶡ";s:2:"ɟ";s:3:"ᶢ";s:2:"ɡ";s:3:"ᶣ";s:2:"ɥ";s:3:"ᶤ";s:2:"ɨ";s:3:"ᶥ";s:2:"ɩ";s:3:"ᶦ";s:2:"ɪ";s:3:"ᶧ";s:3:"ᵻ";s:3:"ᶨ";s:2:"ʝ";s:3:"ᶩ";s:2:"ɭ";s:3:"ᶪ";s:3:"ᶅ";s:3:"ᶫ";s:2:"ʟ";s:3:"ᶬ";s:2:"ɱ";s:3:"ᶭ";s:2:"ɰ";s:3:"ᶮ";s:2:"ɲ";s:3:"ᶯ";s:2:"ɳ";s:3:"ᶰ";s:2:"ɴ";s:3:"ᶱ";s:2:"ɵ";s:3:"ᶲ";s:2:"ɸ";s:3:"ᶳ";s:2:"ʂ";s:3:"ᶴ";s:2:"ʃ";s:3:"ᶵ";s:2:"ƫ";s:3:"ᶶ";s:2:"ʉ";s:3:"ᶷ";s:2:"ʊ";s:3:"ᶸ";s:3:"ᴜ";s:3:"ᶹ";s:2:"ʋ";s:3:"ᶺ";s:2:"ʌ";s:3:"ᶻ";s:1:"z";s:3:"ᶼ";s:2:"ʐ";s:3:"ᶽ";s:2:"ʑ";s:3:"ᶾ";s:2:"ʒ";s:3:"ᶿ";s:2:"θ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:3:"Ḉ";s:5:"Ḉ";s:3:"ḉ";s:5:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:3:"Ḕ";s:5:"Ḕ";s:3:"ḕ";s:5:"ḕ";s:3:"Ḗ";s:5:"Ḗ";s:3:"ḗ";s:5:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:3:"Ḝ";s:5:"Ḝ";s:3:"ḝ";s:5:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:3:"Ḯ";s:5:"Ḯ";s:3:"ḯ";s:5:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:3:"Ḹ";s:5:"Ḹ";s:3:"ḹ";s:5:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:3:"Ṍ";s:5:"Ṍ";s:3:"ṍ";s:5:"ṍ";s:3:"Ṏ";s:5:"Ṏ";s:3:"ṏ";s:5:"ṏ";s:3:"Ṑ";s:5:"Ṑ";s:3:"ṑ";s:5:"ṑ";s:3:"Ṓ";s:5:"Ṓ";s:3:"ṓ";s:5:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:3:"Ṝ";s:5:"Ṝ";s:3:"ṝ";s:5:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:3:"Ṥ";s:5:"Ṥ";s:3:"ṥ";s:5:"ṥ";s:3:"Ṧ";s:5:"Ṧ";s:3:"ṧ";s:5:"ṧ";s:3:"Ṩ";s:5:"Ṩ";s:3:"ṩ";s:5:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:3:"Ṹ";s:5:"Ṹ";s:3:"ṹ";s:5:"ṹ";s:3:"Ṻ";s:5:"Ṻ";s:3:"ṻ";s:5:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:3:"ẚ";s:3:"aʾ";s:3:"ẛ";s:3:"ṡ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:3:"Ấ";s:5:"Ấ";s:3:"ấ";s:5:"ấ";s:3:"Ầ";s:5:"Ầ";s:3:"ầ";s:5:"ầ";s:3:"Ẩ";s:5:"Ẩ";s:3:"ẩ";s:5:"ẩ";s:3:"Ẫ";s:5:"Ẫ";s:3:"ẫ";s:5:"ẫ";s:3:"Ậ";s:5:"Ậ";s:3:"ậ";s:5:"ậ";s:3:"Ắ";s:5:"Ắ";s:3:"ắ";s:5:"ắ";s:3:"Ằ";s:5:"Ằ";s:3:"ằ";s:5:"ằ";s:3:"Ẳ";s:5:"Ẳ";s:3:"ẳ";s:5:"ẳ";s:3:"Ẵ";s:5:"Ẵ";s:3:"ẵ";s:5:"ẵ";s:3:"Ặ";s:5:"Ặ";s:3:"ặ";s:5:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:3:"Ế";s:5:"Ế";s:3:"ế";s:5:"ế";s:3:"Ề";s:5:"Ề";s:3:"ề";s:5:"ề";s:3:"Ể";s:5:"Ể";s:3:"ể";s:5:"ể";s:3:"Ễ";s:5:"Ễ";s:3:"ễ";s:5:"ễ";s:3:"Ệ";s:5:"Ệ";s:3:"ệ";s:5:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:3:"Ố";s:5:"Ố";s:3:"ố";s:5:"ố";s:3:"Ồ";s:5:"Ồ";s:3:"ồ";s:5:"ồ";s:3:"Ổ";s:5:"Ổ";s:3:"ổ";s:5:"ổ";s:3:"Ỗ";s:5:"Ỗ";s:3:"ỗ";s:5:"ỗ";s:3:"Ộ";s:5:"Ộ";s:3:"ộ";s:5:"ộ";s:3:"Ớ";s:5:"Ớ";s:3:"ớ";s:5:"ớ";s:3:"Ờ";s:5:"Ờ";s:3:"ờ";s:5:"ờ";s:3:"Ở";s:5:"Ở";s:3:"ở";s:5:"ở";s:3:"Ỡ";s:5:"Ỡ";s:3:"ỡ";s:5:"ỡ";s:3:"Ợ";s:5:"Ợ";s:3:"ợ";s:5:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:3:"Ứ";s:5:"Ứ";s:3:"ứ";s:5:"ứ";s:3:"Ừ";s:5:"Ừ";s:3:"ừ";s:5:"ừ";s:3:"Ử";s:5:"Ử";s:3:"ử";s:5:"ử";s:3:"Ữ";s:5:"Ữ";s:3:"ữ";s:5:"ữ";s:3:"Ự";s:5:"Ự";s:3:"ự";s:5:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:3:"ἀ";s:4:"ἀ";s:3:"ἁ";s:4:"ἁ";s:3:"ἂ";s:6:"ἂ";s:3:"ἃ";s:6:"ἃ";s:3:"ἄ";s:6:"ἄ";s:3:"ἅ";s:6:"ἅ";s:3:"ἆ";s:6:"ἆ";s:3:"ἇ";s:6:"ἇ";s:3:"Ἀ";s:4:"Ἀ";s:3:"Ἁ";s:4:"Ἁ";s:3:"Ἂ";s:6:"Ἂ";s:3:"Ἃ";s:6:"Ἃ";s:3:"Ἄ";s:6:"Ἄ";s:3:"Ἅ";s:6:"Ἅ";s:3:"Ἆ";s:6:"Ἆ";s:3:"Ἇ";s:6:"Ἇ";s:3:"ἐ";s:4:"ἐ";s:3:"ἑ";s:4:"ἑ";s:3:"ἒ";s:6:"ἒ";s:3:"ἓ";s:6:"ἓ";s:3:"ἔ";s:6:"ἔ";s:3:"ἕ";s:6:"ἕ";s:3:"Ἐ";s:4:"Ἐ";s:3:"Ἑ";s:4:"Ἑ";s:3:"Ἒ";s:6:"Ἒ";s:3:"Ἓ";s:6:"Ἓ";s:3:"Ἔ";s:6:"Ἔ";s:3:"Ἕ";s:6:"Ἕ";s:3:"ἠ";s:4:"ἠ";s:3:"ἡ";s:4:"ἡ";s:3:"ἢ";s:6:"ἢ";s:3:"ἣ";s:6:"ἣ";s:3:"ἤ";s:6:"ἤ";s:3:"ἥ";s:6:"ἥ";s:3:"ἦ";s:6:"ἦ";s:3:"ἧ";s:6:"ἧ";s:3:"Ἠ";s:4:"Ἠ";s:3:"Ἡ";s:4:"Ἡ";s:3:"Ἢ";s:6:"Ἢ";s:3:"Ἣ";s:6:"Ἣ";s:3:"Ἤ";s:6:"Ἤ";s:3:"Ἥ";s:6:"Ἥ";s:3:"Ἦ";s:6:"Ἦ";s:3:"Ἧ";s:6:"Ἧ";s:3:"ἰ";s:4:"ἰ";s:3:"ἱ";s:4:"ἱ";s:3:"ἲ";s:6:"ἲ";s:3:"ἳ";s:6:"ἳ";s:3:"ἴ";s:6:"ἴ";s:3:"ἵ";s:6:"ἵ";s:3:"ἶ";s:6:"ἶ";s:3:"ἷ";s:6:"ἷ";s:3:"Ἰ";s:4:"Ἰ";s:3:"Ἱ";s:4:"Ἱ";s:3:"Ἲ";s:6:"Ἲ";s:3:"Ἳ";s:6:"Ἳ";s:3:"Ἴ";s:6:"Ἴ";s:3:"Ἵ";s:6:"Ἵ";s:3:"Ἶ";s:6:"Ἶ";s:3:"Ἷ";s:6:"Ἷ";s:3:"ὀ";s:4:"ὀ";s:3:"ὁ";s:4:"ὁ";s:3:"ὂ";s:6:"ὂ";s:3:"ὃ";s:6:"ὃ";s:3:"ὄ";s:6:"ὄ";s:3:"ὅ";s:6:"ὅ";s:3:"Ὀ";s:4:"Ὀ";s:3:"Ὁ";s:4:"Ὁ";s:3:"Ὂ";s:6:"Ὂ";s:3:"Ὃ";s:6:"Ὃ";s:3:"Ὄ";s:6:"Ὄ";s:3:"Ὅ";s:6:"Ὅ";s:3:"ὐ";s:4:"ὐ";s:3:"ὑ";s:4:"ὑ";s:3:"ὒ";s:6:"ὒ";s:3:"ὓ";s:6:"ὓ";s:3:"ὔ";s:6:"ὔ";s:3:"ὕ";s:6:"ὕ";s:3:"ὖ";s:6:"ὖ";s:3:"ὗ";s:6:"ὗ";s:3:"Ὑ";s:4:"Ὑ";s:3:"Ὓ";s:6:"Ὓ";s:3:"Ὕ";s:6:"Ὕ";s:3:"Ὗ";s:6:"Ὗ";s:3:"ὠ";s:4:"ὠ";s:3:"ὡ";s:4:"ὡ";s:3:"ὢ";s:6:"ὢ";s:3:"ὣ";s:6:"ὣ";s:3:"ὤ";s:6:"ὤ";s:3:"ὥ";s:6:"ὥ";s:3:"ὦ";s:6:"ὦ";s:3:"ὧ";s:6:"ὧ";s:3:"Ὠ";s:4:"Ὠ";s:3:"Ὡ";s:4:"Ὡ";s:3:"Ὢ";s:6:"Ὢ";s:3:"Ὣ";s:6:"Ὣ";s:3:"Ὤ";s:6:"Ὤ";s:3:"Ὥ";s:6:"Ὥ";s:3:"Ὦ";s:6:"Ὦ";s:3:"Ὧ";s:6:"Ὧ";s:3:"ὰ";s:4:"ὰ";s:3:"ά";s:4:"ά";s:3:"ὲ";s:4:"ὲ";s:3:"έ";s:4:"έ";s:3:"ὴ";s:4:"ὴ";s:3:"ή";s:4:"ή";s:3:"ὶ";s:4:"ὶ";s:3:"ί";s:4:"ί";s:3:"ὸ";s:4:"ὸ";s:3:"ό";s:4:"ό";s:3:"ὺ";s:4:"ὺ";s:3:"ύ";s:4:"ύ";s:3:"ὼ";s:4:"ὼ";s:3:"ώ";s:4:"ώ";s:3:"ᾀ";s:6:"ᾀ";s:3:"ᾁ";s:6:"ᾁ";s:3:"ᾂ";s:8:"ᾂ";s:3:"ᾃ";s:8:"ᾃ";s:3:"ᾄ";s:8:"ᾄ";s:3:"ᾅ";s:8:"ᾅ";s:3:"ᾆ";s:8:"ᾆ";s:3:"ᾇ";s:8:"ᾇ";s:3:"ᾈ";s:6:"ᾈ";s:3:"ᾉ";s:6:"ᾉ";s:3:"ᾊ";s:8:"ᾊ";s:3:"ᾋ";s:8:"ᾋ";s:3:"ᾌ";s:8:"ᾌ";s:3:"ᾍ";s:8:"ᾍ";s:3:"ᾎ";s:8:"ᾎ";s:3:"ᾏ";s:8:"ᾏ";s:3:"ᾐ";s:6:"ᾐ";s:3:"ᾑ";s:6:"ᾑ";s:3:"ᾒ";s:8:"ᾒ";s:3:"ᾓ";s:8:"ᾓ";s:3:"ᾔ";s:8:"ᾔ";s:3:"ᾕ";s:8:"ᾕ";s:3:"ᾖ";s:8:"ᾖ";s:3:"ᾗ";s:8:"ᾗ";s:3:"ᾘ";s:6:"ᾘ";s:3:"ᾙ";s:6:"ᾙ";s:3:"ᾚ";s:8:"ᾚ";s:3:"ᾛ";s:8:"ᾛ";s:3:"ᾜ";s:8:"ᾜ";s:3:"ᾝ";s:8:"ᾝ";s:3:"ᾞ";s:8:"ᾞ";s:3:"ᾟ";s:8:"ᾟ";s:3:"ᾠ";s:6:"ᾠ";s:3:"ᾡ";s:6:"ᾡ";s:3:"ᾢ";s:8:"ᾢ";s:3:"ᾣ";s:8:"ᾣ";s:3:"ᾤ";s:8:"ᾤ";s:3:"ᾥ";s:8:"ᾥ";s:3:"ᾦ";s:8:"ᾦ";s:3:"ᾧ";s:8:"ᾧ";s:3:"ᾨ";s:6:"ᾨ";s:3:"ᾩ";s:6:"ᾩ";s:3:"ᾪ";s:8:"ᾪ";s:3:"ᾫ";s:8:"ᾫ";s:3:"ᾬ";s:8:"ᾬ";s:3:"ᾭ";s:8:"ᾭ";s:3:"ᾮ";s:8:"ᾮ";s:3:"ᾯ";s:8:"ᾯ";s:3:"ᾰ";s:4:"ᾰ";s:3:"ᾱ";s:4:"ᾱ";s:3:"ᾲ";s:6:"ᾲ";s:3:"ᾳ";s:4:"ᾳ";s:3:"ᾴ";s:6:"ᾴ";s:3:"ᾶ";s:4:"ᾶ";s:3:"ᾷ";s:6:"ᾷ";s:3:"Ᾰ";s:4:"Ᾰ";s:3:"Ᾱ";s:4:"Ᾱ";s:3:"Ὰ";s:4:"Ὰ";s:3:"Ά";s:4:"Ά";s:3:"ᾼ";s:4:"ᾼ";s:3:"᾽";s:3:" ̓";s:3:"ι";s:2:"ι";s:3:"᾿";s:3:" ̓";s:3:"῀";s:3:" ͂";s:3:"῁";s:5:" ̈͂";s:3:"ῂ";s:6:"ῂ";s:3:"ῃ";s:4:"ῃ";s:3:"ῄ";s:6:"ῄ";s:3:"ῆ";s:4:"ῆ";s:3:"ῇ";s:6:"ῇ";s:3:"Ὲ";s:4:"Ὲ";s:3:"Έ";s:4:"Έ";s:3:"Ὴ";s:4:"Ὴ";s:3:"Ή";s:4:"Ή";s:3:"ῌ";s:4:"ῌ";s:3:"῍";s:5:" ̓̀";s:3:"῎";s:5:" ̓́";s:3:"῏";s:5:" ̓͂";s:3:"ῐ";s:4:"ῐ";s:3:"ῑ";s:4:"ῑ";s:3:"ῒ";s:6:"ῒ";s:3:"ΐ";s:6:"ΐ";s:3:"ῖ";s:4:"ῖ";s:3:"ῗ";s:6:"ῗ";s:3:"Ῐ";s:4:"Ῐ";s:3:"Ῑ";s:4:"Ῑ";s:3:"Ὶ";s:4:"Ὶ";s:3:"Ί";s:4:"Ί";s:3:"῝";s:5:" ̔̀";s:3:"῞";s:5:" ̔́";s:3:"῟";s:5:" ̔͂";s:3:"ῠ";s:4:"ῠ";s:3:"ῡ";s:4:"ῡ";s:3:"ῢ";s:6:"ῢ";s:3:"ΰ";s:6:"ΰ";s:3:"ῤ";s:4:"ῤ";s:3:"ῥ";s:4:"ῥ";s:3:"ῦ";s:4:"ῦ";s:3:"ῧ";s:6:"ῧ";s:3:"Ῠ";s:4:"Ῠ";s:3:"Ῡ";s:4:"Ῡ";s:3:"Ὺ";s:4:"Ὺ";s:3:"Ύ";s:4:"Ύ";s:3:"Ῥ";s:4:"Ῥ";s:3:"῭";s:5:" ̈̀";s:3:"΅";s:5:" ̈́";s:3:"`";s:1:"`";s:3:"ῲ";s:6:"ῲ";s:3:"ῳ";s:4:"ῳ";s:3:"ῴ";s:6:"ῴ";s:3:"ῶ";s:4:"ῶ";s:3:"ῷ";s:6:"ῷ";s:3:"Ὸ";s:4:"Ὸ";s:3:"Ό";s:4:"Ό";s:3:"Ὼ";s:4:"Ὼ";s:3:"Ώ";s:4:"Ώ";s:3:"ῼ";s:4:"ῼ";s:3:"´";s:3:" ́";s:3:"῾";s:3:" ̔";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:"‑";s:3:"‐";s:3:"‗";s:3:" ̳";s:3:"․";s:1:".";s:3:"‥";s:2:"..";s:3:"…";s:3:"...";s:3:" ";s:1:" ";s:3:"″";s:6:"′′";s:3:"‴";s:9:"′′′";s:3:"‶";s:6:"‵‵";s:3:"‷";s:9:"‵‵‵";s:3:"‼";s:2:"!!";s:3:"‾";s:3:" ̅";s:3:"⁇";s:2:"??";s:3:"⁈";s:2:"?!";s:3:"⁉";s:2:"!?";s:3:"⁗";s:12:"′′′′";s:3:" ";s:1:" ";s:3:"⁰";s:1:"0";s:3:"ⁱ";s:1:"i";s:3:"⁴";s:1:"4";s:3:"⁵";s:1:"5";s:3:"⁶";s:1:"6";s:3:"⁷";s:1:"7";s:3:"⁸";s:1:"8";s:3:"⁹";s:1:"9";s:3:"⁺";s:1:"+";s:3:"⁻";s:3:"−";s:3:"⁼";s:1:"=";s:3:"⁽";s:1:"(";s:3:"⁾";s:1:")";s:3:"ⁿ";s:1:"n";s:3:"₀";s:1:"0";s:3:"₁";s:1:"1";s:3:"₂";s:1:"2";s:3:"₃";s:1:"3";s:3:"₄";s:1:"4";s:3:"₅";s:1:"5";s:3:"₆";s:1:"6";s:3:"₇";s:1:"7";s:3:"₈";s:1:"8";s:3:"₉";s:1:"9";s:3:"₊";s:1:"+";s:3:"₋";s:3:"−";s:3:"₌";s:1:"=";s:3:"₍";s:1:"(";s:3:"₎";s:1:")";s:3:"ₐ";s:1:"a";s:3:"ₑ";s:1:"e";s:3:"ₒ";s:1:"o";s:3:"ₓ";s:1:"x";s:3:"ₔ";s:2:"ə";s:3:"₨";s:2:"Rs";s:3:"℀";s:3:"a/c";s:3:"℁";s:3:"a/s";s:3:"ℂ";s:1:"C";s:3:"℃";s:3:"°C";s:3:"℅";s:3:"c/o";s:3:"℆";s:3:"c/u";s:3:"ℇ";s:2:"Ɛ";s:3:"℉";s:3:"°F";s:3:"ℊ";s:1:"g";s:3:"ℋ";s:1:"H";s:3:"ℌ";s:1:"H";s:3:"ℍ";s:1:"H";s:3:"ℎ";s:1:"h";s:3:"ℏ";s:2:"ħ";s:3:"ℐ";s:1:"I";s:3:"ℑ";s:1:"I";s:3:"ℒ";s:1:"L";s:3:"ℓ";s:1:"l";s:3:"ℕ";s:1:"N";s:3:"№";s:2:"No";s:3:"ℙ";s:1:"P";s:3:"ℚ";s:1:"Q";s:3:"ℛ";s:1:"R";s:3:"ℜ";s:1:"R";s:3:"ℝ";s:1:"R";s:3:"℠";s:2:"SM";s:3:"℡";s:3:"TEL";s:3:"™";s:2:"TM";s:3:"ℤ";s:1:"Z";s:3:"Ω";s:2:"Ω";s:3:"ℨ";s:1:"Z";s:3:"K";s:1:"K";s:3:"Å";s:3:"Å";s:3:"ℬ";s:1:"B";s:3:"ℭ";s:1:"C";s:3:"ℯ";s:1:"e";s:3:"ℰ";s:1:"E";s:3:"ℱ";s:1:"F";s:3:"ℳ";s:1:"M";s:3:"ℴ";s:1:"o";s:3:"ℵ";s:2:"א";s:3:"ℶ";s:2:"ב";s:3:"ℷ";s:2:"ג";s:3:"ℸ";s:2:"ד";s:3:"ℹ";s:1:"i";s:3:"℻";s:3:"FAX";s:3:"ℼ";s:2:"π";s:3:"ℽ";s:2:"γ";s:3:"ℾ";s:2:"Γ";s:3:"ℿ";s:2:"Π";s:3:"⅀";s:3:"∑";s:3:"ⅅ";s:1:"D";s:3:"ⅆ";s:1:"d";s:3:"ⅇ";s:1:"e";s:3:"ⅈ";s:1:"i";s:3:"ⅉ";s:1:"j";s:3:"⅓";s:5:"1⁄3";s:3:"⅔";s:5:"2⁄3";s:3:"⅕";s:5:"1⁄5";s:3:"⅖";s:5:"2⁄5";s:3:"⅗";s:5:"3⁄5";s:3:"⅘";s:5:"4⁄5";s:3:"⅙";s:5:"1⁄6";s:3:"⅚";s:5:"5⁄6";s:3:"⅛";s:5:"1⁄8";s:3:"⅜";s:5:"3⁄8";s:3:"⅝";s:5:"5⁄8";s:3:"⅞";s:5:"7⁄8";s:3:"⅟";s:4:"1⁄";s:3:"Ⅰ";s:1:"I";s:3:"Ⅱ";s:2:"II";s:3:"Ⅲ";s:3:"III";s:3:"Ⅳ";s:2:"IV";s:3:"Ⅴ";s:1:"V";s:3:"Ⅵ";s:2:"VI";s:3:"Ⅶ";s:3:"VII";s:3:"Ⅷ";s:4:"VIII";s:3:"Ⅸ";s:2:"IX";s:3:"Ⅹ";s:1:"X";s:3:"Ⅺ";s:2:"XI";s:3:"Ⅻ";s:3:"XII";s:3:"Ⅼ";s:1:"L";s:3:"Ⅽ";s:1:"C";s:3:"Ⅾ";s:1:"D";s:3:"Ⅿ";s:1:"M";s:3:"ⅰ";s:1:"i";s:3:"ⅱ";s:2:"ii";s:3:"ⅲ";s:3:"iii";s:3:"ⅳ";s:2:"iv";s:3:"ⅴ";s:1:"v";s:3:"ⅵ";s:2:"vi";s:3:"ⅶ";s:3:"vii";s:3:"ⅷ";s:4:"viii";s:3:"ⅸ";s:2:"ix";s:3:"ⅹ";s:1:"x";s:3:"ⅺ";s:2:"xi";s:3:"ⅻ";s:3:"xii";s:3:"ⅼ";s:1:"l";s:3:"ⅽ";s:1:"c";s:3:"ⅾ";s:1:"d";s:3:"ⅿ";s:1:"m";s:3:"↚";s:5:"↚";s:3:"↛";s:5:"↛";s:3:"↮";s:5:"↮";s:3:"⇍";s:5:"⇍";s:3:"⇎";s:5:"⇎";s:3:"⇏";s:5:"⇏";s:3:"∄";s:5:"∄";s:3:"∉";s:5:"∉";s:3:"∌";s:5:"∌";s:3:"∤";s:5:"∤";s:3:"∦";s:5:"∦";s:3:"∬";s:6:"∫∫";s:3:"∭";s:9:"∫∫∫";s:3:"∯";s:6:"∮∮";s:3:"∰";s:9:"∮∮∮";s:3:"≁";s:5:"≁";s:3:"≄";s:5:"≄";s:3:"≇";s:5:"≇";s:3:"≉";s:5:"≉";s:3:"≠";s:3:"≠";s:3:"≢";s:5:"≢";s:3:"≭";s:5:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:3:"≰";s:5:"≰";s:3:"≱";s:5:"≱";s:3:"≴";s:5:"≴";s:3:"≵";s:5:"≵";s:3:"≸";s:5:"≸";s:3:"≹";s:5:"≹";s:3:"⊀";s:5:"⊀";s:3:"⊁";s:5:"⊁";s:3:"⊄";s:5:"⊄";s:3:"⊅";s:5:"⊅";s:3:"⊈";s:5:"⊈";s:3:"⊉";s:5:"⊉";s:3:"⊬";s:5:"⊬";s:3:"⊭";s:5:"⊭";s:3:"⊮";s:5:"⊮";s:3:"⊯";s:5:"⊯";s:3:"⋠";s:5:"⋠";s:3:"⋡";s:5:"⋡";s:3:"⋢";s:5:"⋢";s:3:"⋣";s:5:"⋣";s:3:"⋪";s:5:"⋪";s:3:"⋫";s:5:"⋫";s:3:"⋬";s:5:"⋬";s:3:"⋭";s:5:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:3:"①";s:1:"1";s:3:"②";s:1:"2";s:3:"③";s:1:"3";s:3:"④";s:1:"4";s:3:"⑤";s:1:"5";s:3:"⑥";s:1:"6";s:3:"⑦";s:1:"7";s:3:"⑧";s:1:"8";s:3:"⑨";s:1:"9";s:3:"⑩";s:2:"10";s:3:"⑪";s:2:"11";s:3:"⑫";s:2:"12";s:3:"⑬";s:2:"13";s:3:"⑭";s:2:"14";s:3:"⑮";s:2:"15";s:3:"⑯";s:2:"16";s:3:"⑰";s:2:"17";s:3:"⑱";s:2:"18";s:3:"⑲";s:2:"19";s:3:"⑳";s:2:"20";s:3:"⑴";s:3:"(1)";s:3:"⑵";s:3:"(2)";s:3:"⑶";s:3:"(3)";s:3:"⑷";s:3:"(4)";s:3:"⑸";s:3:"(5)";s:3:"⑹";s:3:"(6)";s:3:"⑺";s:3:"(7)";s:3:"⑻";s:3:"(8)";s:3:"⑼";s:3:"(9)";s:3:"⑽";s:4:"(10)";s:3:"⑾";s:4:"(11)";s:3:"⑿";s:4:"(12)";s:3:"⒀";s:4:"(13)";s:3:"⒁";s:4:"(14)";s:3:"⒂";s:4:"(15)";s:3:"⒃";s:4:"(16)";s:3:"⒄";s:4:"(17)";s:3:"⒅";s:4:"(18)";s:3:"⒆";s:4:"(19)";s:3:"⒇";s:4:"(20)";s:3:"⒈";s:2:"1.";s:3:"⒉";s:2:"2.";s:3:"⒊";s:2:"3.";s:3:"⒋";s:2:"4.";s:3:"⒌";s:2:"5.";s:3:"⒍";s:2:"6.";s:3:"⒎";s:2:"7.";s:3:"⒏";s:2:"8.";s:3:"⒐";s:2:"9.";s:3:"⒑";s:3:"10.";s:3:"⒒";s:3:"11.";s:3:"⒓";s:3:"12.";s:3:"⒔";s:3:"13.";s:3:"⒕";s:3:"14.";s:3:"⒖";s:3:"15.";s:3:"⒗";s:3:"16.";s:3:"⒘";s:3:"17.";s:3:"⒙";s:3:"18.";s:3:"⒚";s:3:"19.";s:3:"⒛";s:3:"20.";s:3:"⒜";s:3:"(a)";s:3:"⒝";s:3:"(b)";s:3:"⒞";s:3:"(c)";s:3:"⒟";s:3:"(d)";s:3:"⒠";s:3:"(e)";s:3:"⒡";s:3:"(f)";s:3:"⒢";s:3:"(g)";s:3:"⒣";s:3:"(h)";s:3:"⒤";s:3:"(i)";s:3:"⒥";s:3:"(j)";s:3:"⒦";s:3:"(k)";s:3:"⒧";s:3:"(l)";s:3:"⒨";s:3:"(m)";s:3:"⒩";s:3:"(n)";s:3:"⒪";s:3:"(o)";s:3:"⒫";s:3:"(p)";s:3:"⒬";s:3:"(q)";s:3:"⒭";s:3:"(r)";s:3:"⒮";s:3:"(s)";s:3:"⒯";s:3:"(t)";s:3:"⒰";s:3:"(u)";s:3:"⒱";s:3:"(v)";s:3:"⒲";s:3:"(w)";s:3:"⒳";s:3:"(x)";s:3:"⒴";s:3:"(y)";s:3:"⒵";s:3:"(z)";s:3:"Ⓐ";s:1:"A";s:3:"Ⓑ";s:1:"B";s:3:"Ⓒ";s:1:"C";s:3:"Ⓓ";s:1:"D";s:3:"Ⓔ";s:1:"E";s:3:"Ⓕ";s:1:"F";s:3:"Ⓖ";s:1:"G";s:3:"Ⓗ";s:1:"H";s:3:"Ⓘ";s:1:"I";s:3:"Ⓙ";s:1:"J";s:3:"Ⓚ";s:1:"K";s:3:"Ⓛ";s:1:"L";s:3:"Ⓜ";s:1:"M";s:3:"Ⓝ";s:1:"N";s:3:"Ⓞ";s:1:"O";s:3:"Ⓟ";s:1:"P";s:3:"Ⓠ";s:1:"Q";s:3:"Ⓡ";s:1:"R";s:3:"Ⓢ";s:1:"S";s:3:"Ⓣ";s:1:"T";s:3:"Ⓤ";s:1:"U";s:3:"Ⓥ";s:1:"V";s:3:"Ⓦ";s:1:"W";s:3:"Ⓧ";s:1:"X";s:3:"Ⓨ";s:1:"Y";s:3:"Ⓩ";s:1:"Z";s:3:"ⓐ";s:1:"a";s:3:"ⓑ";s:1:"b";s:3:"ⓒ";s:1:"c";s:3:"ⓓ";s:1:"d";s:3:"ⓔ";s:1:"e";s:3:"ⓕ";s:1:"f";s:3:"ⓖ";s:1:"g";s:3:"ⓗ";s:1:"h";s:3:"ⓘ";s:1:"i";s:3:"ⓙ";s:1:"j";s:3:"ⓚ";s:1:"k";s:3:"ⓛ";s:1:"l";s:3:"ⓜ";s:1:"m";s:3:"ⓝ";s:1:"n";s:3:"ⓞ";s:1:"o";s:3:"ⓟ";s:1:"p";s:3:"ⓠ";s:1:"q";s:3:"ⓡ";s:1:"r";s:3:"ⓢ";s:1:"s";s:3:"ⓣ";s:1:"t";s:3:"ⓤ";s:1:"u";s:3:"ⓥ";s:1:"v";s:3:"ⓦ";s:1:"w";s:3:"ⓧ";s:1:"x";s:3:"ⓨ";s:1:"y";s:3:"ⓩ";s:1:"z";s:3:"⓪";s:1:"0";s:3:"⨌";s:12:"∫∫∫∫";s:3:"⩴";s:3:"::=";s:3:"⩵";s:2:"==";s:3:"⩶";s:3:"===";s:3:"⫝̸";s:5:"⫝̸";s:3:"ⵯ";s:3:"ⵡ";s:3:"⺟";s:3:"母";s:3:"⻳";s:3:"龟";s:3:"⼀";s:3:"一";s:3:"⼁";s:3:"丨";s:3:"⼂";s:3:"丶";s:3:"⼃";s:3:"丿";s:3:"⼄";s:3:"乙";s:3:"⼅";s:3:"亅";s:3:"⼆";s:3:"二";s:3:"⼇";s:3:"亠";s:3:"⼈";s:3:"人";s:3:"⼉";s:3:"儿";s:3:"⼊";s:3:"入";s:3:"⼋";s:3:"八";s:3:"⼌";s:3:"冂";s:3:"⼍";s:3:"冖";s:3:"⼎";s:3:"冫";s:3:"⼏";s:3:"几";s:3:"⼐";s:3:"凵";s:3:"⼑";s:3:"刀";s:3:"⼒";s:3:"力";s:3:"⼓";s:3:"勹";s:3:"⼔";s:3:"匕";s:3:"⼕";s:3:"匚";s:3:"⼖";s:3:"匸";s:3:"⼗";s:3:"十";s:3:"⼘";s:3:"卜";s:3:"⼙";s:3:"卩";s:3:"⼚";s:3:"厂";s:3:"⼛";s:3:"厶";s:3:"⼜";s:3:"又";s:3:"⼝";s:3:"口";s:3:"⼞";s:3:"囗";s:3:"⼟";s:3:"土";s:3:"⼠";s:3:"士";s:3:"⼡";s:3:"夂";s:3:"⼢";s:3:"夊";s:3:"⼣";s:3:"夕";s:3:"⼤";s:3:"大";s:3:"⼥";s:3:"女";s:3:"⼦";s:3:"子";s:3:"⼧";s:3:"宀";s:3:"⼨";s:3:"寸";s:3:"⼩";s:3:"小";s:3:"⼪";s:3:"尢";s:3:"⼫";s:3:"尸";s:3:"⼬";s:3:"屮";s:3:"⼭";s:3:"山";s:3:"⼮";s:3:"巛";s:3:"⼯";s:3:"工";s:3:"⼰";s:3:"己";s:3:"⼱";s:3:"巾";s:3:"⼲";s:3:"干";s:3:"⼳";s:3:"幺";s:3:"⼴";s:3:"广";s:3:"⼵";s:3:"廴";s:3:"⼶";s:3:"廾";s:3:"⼷";s:3:"弋";s:3:"⼸";s:3:"弓";s:3:"⼹";s:3:"彐";s:3:"⼺";s:3:"彡";s:3:"⼻";s:3:"彳";s:3:"⼼";s:3:"心";s:3:"⼽";s:3:"戈";s:3:"⼾";s:3:"戶";s:3:"⼿";s:3:"手";s:3:"⽀";s:3:"支";s:3:"⽁";s:3:"攴";s:3:"⽂";s:3:"文";s:3:"⽃";s:3:"斗";s:3:"⽄";s:3:"斤";s:3:"⽅";s:3:"方";s:3:"⽆";s:3:"无";s:3:"⽇";s:3:"日";s:3:"⽈";s:3:"曰";s:3:"⽉";s:3:"月";s:3:"⽊";s:3:"木";s:3:"⽋";s:3:"欠";s:3:"⽌";s:3:"止";s:3:"⽍";s:3:"歹";s:3:"⽎";s:3:"殳";s:3:"⽏";s:3:"毋";s:3:"⽐";s:3:"比";s:3:"⽑";s:3:"毛";s:3:"⽒";s:3:"氏";s:3:"⽓";s:3:"气";s:3:"⽔";s:3:"水";s:3:"⽕";s:3:"火";s:3:"⽖";s:3:"爪";s:3:"⽗";s:3:"父";s:3:"⽘";s:3:"爻";s:3:"⽙";s:3:"爿";s:3:"⽚";s:3:"片";s:3:"⽛";s:3:"牙";s:3:"⽜";s:3:"牛";s:3:"⽝";s:3:"犬";s:3:"⽞";s:3:"玄";s:3:"⽟";s:3:"玉";s:3:"⽠";s:3:"瓜";s:3:"⽡";s:3:"瓦";s:3:"⽢";s:3:"甘";s:3:"⽣";s:3:"生";s:3:"⽤";s:3:"用";s:3:"⽥";s:3:"田";s:3:"⽦";s:3:"疋";s:3:"⽧";s:3:"疒";s:3:"⽨";s:3:"癶";s:3:"⽩";s:3:"白";s:3:"⽪";s:3:"皮";s:3:"⽫";s:3:"皿";s:3:"⽬";s:3:"目";s:3:"⽭";s:3:"矛";s:3:"⽮";s:3:"矢";s:3:"⽯";s:3:"石";s:3:"⽰";s:3:"示";s:3:"⽱";s:3:"禸";s:3:"⽲";s:3:"禾";s:3:"⽳";s:3:"穴";s:3:"⽴";s:3:"立";s:3:"⽵";s:3:"竹";s:3:"⽶";s:3:"米";s:3:"⽷";s:3:"糸";s:3:"⽸";s:3:"缶";s:3:"⽹";s:3:"网";s:3:"⽺";s:3:"羊";s:3:"⽻";s:3:"羽";s:3:"⽼";s:3:"老";s:3:"⽽";s:3:"而";s:3:"⽾";s:3:"耒";s:3:"⽿";s:3:"耳";s:3:"⾀";s:3:"聿";s:3:"⾁";s:3:"肉";s:3:"⾂";s:3:"臣";s:3:"⾃";s:3:"自";s:3:"⾄";s:3:"至";s:3:"⾅";s:3:"臼";s:3:"⾆";s:3:"舌";s:3:"⾇";s:3:"舛";s:3:"⾈";s:3:"舟";s:3:"⾉";s:3:"艮";s:3:"⾊";s:3:"色";s:3:"⾋";s:3:"艸";s:3:"⾌";s:3:"虍";s:3:"⾍";s:3:"虫";s:3:"⾎";s:3:"血";s:3:"⾏";s:3:"行";s:3:"⾐";s:3:"衣";s:3:"⾑";s:3:"襾";s:3:"⾒";s:3:"見";s:3:"⾓";s:3:"角";s:3:"⾔";s:3:"言";s:3:"⾕";s:3:"谷";s:3:"⾖";s:3:"豆";s:3:"⾗";s:3:"豕";s:3:"⾘";s:3:"豸";s:3:"⾙";s:3:"貝";s:3:"⾚";s:3:"赤";s:3:"⾛";s:3:"走";s:3:"⾜";s:3:"足";s:3:"⾝";s:3:"身";s:3:"⾞";s:3:"車";s:3:"⾟";s:3:"辛";s:3:"⾠";s:3:"辰";s:3:"⾡";s:3:"辵";s:3:"⾢";s:3:"邑";s:3:"⾣";s:3:"酉";s:3:"⾤";s:3:"釆";s:3:"⾥";s:3:"里";s:3:"⾦";s:3:"金";s:3:"⾧";s:3:"長";s:3:"⾨";s:3:"門";s:3:"⾩";s:3:"阜";s:3:"⾪";s:3:"隶";s:3:"⾫";s:3:"隹";s:3:"⾬";s:3:"雨";s:3:"⾭";s:3:"靑";s:3:"⾮";s:3:"非";s:3:"⾯";s:3:"面";s:3:"⾰";s:3:"革";s:3:"⾱";s:3:"韋";s:3:"⾲";s:3:"韭";s:3:"⾳";s:3:"音";s:3:"⾴";s:3:"頁";s:3:"⾵";s:3:"風";s:3:"⾶";s:3:"飛";s:3:"⾷";s:3:"食";s:3:"⾸";s:3:"首";s:3:"⾹";s:3:"香";s:3:"⾺";s:3:"馬";s:3:"⾻";s:3:"骨";s:3:"⾼";s:3:"高";s:3:"⾽";s:3:"髟";s:3:"⾾";s:3:"鬥";s:3:"⾿";s:3:"鬯";s:3:"⿀";s:3:"鬲";s:3:"⿁";s:3:"鬼";s:3:"⿂";s:3:"魚";s:3:"⿃";s:3:"鳥";s:3:"⿄";s:3:"鹵";s:3:"⿅";s:3:"鹿";s:3:"⿆";s:3:"麥";s:3:"⿇";s:3:"麻";s:3:"⿈";s:3:"黃";s:3:"⿉";s:3:"黍";s:3:"⿊";s:3:"黑";s:3:"⿋";s:3:"黹";s:3:"⿌";s:3:"黽";s:3:"⿍";s:3:"鼎";s:3:"⿎";s:3:"鼓";s:3:"⿏";s:3:"鼠";s:3:"⿐";s:3:"鼻";s:3:"⿑";s:3:"齊";s:3:"⿒";s:3:"齒";s:3:"⿓";s:3:"龍";s:3:"⿔";s:3:"龜";s:3:"⿕";s:3:"龠";s:3:" ";s:1:" ";s:3:"〶";s:3:"〒";s:3:"〸";s:3:"十";s:3:"〹";s:3:"卄";s:3:"〺";s:3:"卅";s:3:"が";s:6:"が";s:3:"ぎ";s:6:"ぎ";s:3:"ぐ";s:6:"ぐ";s:3:"げ";s:6:"げ";s:3:"ご";s:6:"ご";s:3:"ざ";s:6:"ざ";s:3:"じ";s:6:"じ";s:3:"ず";s:6:"ず";s:3:"ぜ";s:6:"ぜ";s:3:"ぞ";s:6:"ぞ";s:3:"だ";s:6:"だ";s:3:"ぢ";s:6:"ぢ";s:3:"づ";s:6:"づ";s:3:"で";s:6:"で";s:3:"ど";s:6:"ど";s:3:"ば";s:6:"ば";s:3:"ぱ";s:6:"ぱ";s:3:"び";s:6:"び";s:3:"ぴ";s:6:"ぴ";s:3:"ぶ";s:6:"ぶ";s:3:"ぷ";s:6:"ぷ";s:3:"べ";s:6:"べ";s:3:"ぺ";s:6:"ぺ";s:3:"ぼ";s:6:"ぼ";s:3:"ぽ";s:6:"ぽ";s:3:"ゔ";s:6:"ゔ";s:3:"゛";s:4:" ゙";s:3:"゜";s:4:" ゚";s:3:"ゞ";s:6:"ゞ";s:3:"ゟ";s:6:"より";s:3:"ガ";s:6:"ガ";s:3:"ギ";s:6:"ギ";s:3:"グ";s:6:"グ";s:3:"ゲ";s:6:"ゲ";s:3:"ゴ";s:6:"ゴ";s:3:"ザ";s:6:"ザ";s:3:"ジ";s:6:"ジ";s:3:"ズ";s:6:"ズ";s:3:"ゼ";s:6:"ゼ";s:3:"ゾ";s:6:"ゾ";s:3:"ダ";s:6:"ダ";s:3:"ヂ";s:6:"ヂ";s:3:"ヅ";s:6:"ヅ";s:3:"デ";s:6:"デ";s:3:"ド";s:6:"ド";s:3:"バ";s:6:"バ";s:3:"パ";s:6:"パ";s:3:"ビ";s:6:"ビ";s:3:"ピ";s:6:"ピ";s:3:"ブ";s:6:"ブ";s:3:"プ";s:6:"プ";s:3:"ベ";s:6:"ベ";s:3:"ペ";s:6:"ペ";s:3:"ボ";s:6:"ボ";s:3:"ポ";s:6:"ポ";s:3:"ヴ";s:6:"ヴ";s:3:"ヷ";s:6:"ヷ";s:3:"ヸ";s:6:"ヸ";s:3:"ヹ";s:6:"ヹ";s:3:"ヺ";s:6:"ヺ";s:3:"ヾ";s:6:"ヾ";s:3:"ヿ";s:6:"コト";s:3:"ㄱ";s:3:"ᄀ";s:3:"ㄲ";s:3:"ᄁ";s:3:"ㄳ";s:3:"ᆪ";s:3:"ㄴ";s:3:"ᄂ";s:3:"ㄵ";s:3:"ᆬ";s:3:"ㄶ";s:3:"ᆭ";s:3:"ㄷ";s:3:"ᄃ";s:3:"ㄸ";s:3:"ᄄ";s:3:"ㄹ";s:3:"ᄅ";s:3:"ㄺ";s:3:"ᆰ";s:3:"ㄻ";s:3:"ᆱ";s:3:"ㄼ";s:3:"ᆲ";s:3:"ㄽ";s:3:"ᆳ";s:3:"ㄾ";s:3:"ᆴ";s:3:"ㄿ";s:3:"ᆵ";s:3:"ㅀ";s:3:"ᄚ";s:3:"ㅁ";s:3:"ᄆ";s:3:"ㅂ";s:3:"ᄇ";s:3:"ㅃ";s:3:"ᄈ";s:3:"ㅄ";s:3:"ᄡ";s:3:"ㅅ";s:3:"ᄉ";s:3:"ㅆ";s:3:"ᄊ";s:3:"ㅇ";s:3:"ᄋ";s:3:"ㅈ";s:3:"ᄌ";s:3:"ㅉ";s:3:"ᄍ";s:3:"ㅊ";s:3:"ᄎ";s:3:"ㅋ";s:3:"ᄏ";s:3:"ㅌ";s:3:"ᄐ";s:3:"ㅍ";s:3:"ᄑ";s:3:"ㅎ";s:3:"ᄒ";s:3:"ㅏ";s:3:"ᅡ";s:3:"ㅐ";s:3:"ᅢ";s:3:"ㅑ";s:3:"ᅣ";s:3:"ㅒ";s:3:"ᅤ";s:3:"ㅓ";s:3:"ᅥ";s:3:"ㅔ";s:3:"ᅦ";s:3:"ㅕ";s:3:"ᅧ";s:3:"ㅖ";s:3:"ᅨ";s:3:"ㅗ";s:3:"ᅩ";s:3:"ㅘ";s:3:"ᅪ";s:3:"ㅙ";s:3:"ᅫ";s:3:"ㅚ";s:3:"ᅬ";s:3:"ㅛ";s:3:"ᅭ";s:3:"ㅜ";s:3:"ᅮ";s:3:"ㅝ";s:3:"ᅯ";s:3:"ㅞ";s:3:"ᅰ";s:3:"ㅟ";s:3:"ᅱ";s:3:"ㅠ";s:3:"ᅲ";s:3:"ㅡ";s:3:"ᅳ";s:3:"ㅢ";s:3:"ᅴ";s:3:"ㅣ";s:3:"ᅵ";s:3:"ㅤ";s:3:"ᅠ";s:3:"ㅥ";s:3:"ᄔ";s:3:"ㅦ";s:3:"ᄕ";s:3:"ㅧ";s:3:"ᇇ";s:3:"ㅨ";s:3:"ᇈ";s:3:"ㅩ";s:3:"ᇌ";s:3:"ㅪ";s:3:"ᇎ";s:3:"ㅫ";s:3:"ᇓ";s:3:"ㅬ";s:3:"ᇗ";s:3:"ㅭ";s:3:"ᇙ";s:3:"ㅮ";s:3:"ᄜ";s:3:"ㅯ";s:3:"ᇝ";s:3:"ㅰ";s:3:"ᇟ";s:3:"ㅱ";s:3:"ᄝ";s:3:"ㅲ";s:3:"ᄞ";s:3:"ㅳ";s:3:"ᄠ";s:3:"ㅴ";s:3:"ᄢ";s:3:"ㅵ";s:3:"ᄣ";s:3:"ㅶ";s:3:"ᄧ";s:3:"ㅷ";s:3:"ᄩ";s:3:"ㅸ";s:3:"ᄫ";s:3:"ㅹ";s:3:"ᄬ";s:3:"ㅺ";s:3:"ᄭ";s:3:"ㅻ";s:3:"ᄮ";s:3:"ㅼ";s:3:"ᄯ";s:3:"ㅽ";s:3:"ᄲ";s:3:"ㅾ";s:3:"ᄶ";s:3:"ㅿ";s:3:"ᅀ";s:3:"ㆀ";s:3:"ᅇ";s:3:"ㆁ";s:3:"ᅌ";s:3:"ㆂ";s:3:"ᇱ";s:3:"ㆃ";s:3:"ᇲ";s:3:"ㆄ";s:3:"ᅗ";s:3:"ㆅ";s:3:"ᅘ";s:3:"ㆆ";s:3:"ᅙ";s:3:"ㆇ";s:3:"ᆄ";s:3:"ㆈ";s:3:"ᆅ";s:3:"ㆉ";s:3:"ᆈ";s:3:"ㆊ";s:3:"ᆑ";s:3:"ㆋ";s:3:"ᆒ";s:3:"ㆌ";s:3:"ᆔ";s:3:"ㆍ";s:3:"ᆞ";s:3:"ㆎ";s:3:"ᆡ";s:3:"㆒";s:3:"一";s:3:"㆓";s:3:"二";s:3:"㆔";s:3:"三";s:3:"㆕";s:3:"四";s:3:"㆖";s:3:"上";s:3:"㆗";s:3:"中";s:3:"㆘";s:3:"下";s:3:"㆙";s:3:"甲";s:3:"㆚";s:3:"乙";s:3:"㆛";s:3:"丙";s:3:"㆜";s:3:"丁";s:3:"㆝";s:3:"天";s:3:"㆞";s:3:"地";s:3:"㆟";s:3:"人";s:3:"㈀";s:5:"(ᄀ)";s:3:"㈁";s:5:"(ᄂ)";s:3:"㈂";s:5:"(ᄃ)";s:3:"㈃";s:5:"(ᄅ)";s:3:"㈄";s:5:"(ᄆ)";s:3:"㈅";s:5:"(ᄇ)";s:3:"㈆";s:5:"(ᄉ)";s:3:"㈇";s:5:"(ᄋ)";s:3:"㈈";s:5:"(ᄌ)";s:3:"㈉";s:5:"(ᄎ)";s:3:"㈊";s:5:"(ᄏ)";s:3:"㈋";s:5:"(ᄐ)";s:3:"㈌";s:5:"(ᄑ)";s:3:"㈍";s:5:"(ᄒ)";s:3:"㈎";s:8:"(가)";s:3:"㈏";s:8:"(나)";s:3:"㈐";s:8:"(다)";s:3:"㈑";s:8:"(라)";s:3:"㈒";s:8:"(마)";s:3:"㈓";s:8:"(바)";s:3:"㈔";s:8:"(사)";s:3:"㈕";s:8:"(아)";s:3:"㈖";s:8:"(자)";s:3:"㈗";s:8:"(차)";s:3:"㈘";s:8:"(카)";s:3:"㈙";s:8:"(타)";s:3:"㈚";s:8:"(파)";s:3:"㈛";s:8:"(하)";s:3:"㈜";s:8:"(주)";s:3:"㈝";s:17:"(오전)";s:3:"㈞";s:14:"(오후)";s:3:"㈠";s:5:"(一)";s:3:"㈡";s:5:"(二)";s:3:"㈢";s:5:"(三)";s:3:"㈣";s:5:"(四)";s:3:"㈤";s:5:"(五)";s:3:"㈥";s:5:"(六)";s:3:"㈦";s:5:"(七)";s:3:"㈧";s:5:"(八)";s:3:"㈨";s:5:"(九)";s:3:"㈩";s:5:"(十)";s:3:"㈪";s:5:"(月)";s:3:"㈫";s:5:"(火)";s:3:"㈬";s:5:"(水)";s:3:"㈭";s:5:"(木)";s:3:"㈮";s:5:"(金)";s:3:"㈯";s:5:"(土)";s:3:"㈰";s:5:"(日)";s:3:"㈱";s:5:"(株)";s:3:"㈲";s:5:"(有)";s:3:"㈳";s:5:"(社)";s:3:"㈴";s:5:"(名)";s:3:"㈵";s:5:"(特)";s:3:"㈶";s:5:"(財)";s:3:"㈷";s:5:"(祝)";s:3:"㈸";s:5:"(労)";s:3:"㈹";s:5:"(代)";s:3:"㈺";s:5:"(呼)";s:3:"㈻";s:5:"(学)";s:3:"㈼";s:5:"(監)";s:3:"㈽";s:5:"(企)";s:3:"㈾";s:5:"(資)";s:3:"㈿";s:5:"(協)";s:3:"㉀";s:5:"(祭)";s:3:"㉁";s:5:"(休)";s:3:"㉂";s:5:"(自)";s:3:"㉃";s:5:"(至)";s:3:"㉐";s:3:"PTE";s:3:"㉑";s:2:"21";s:3:"㉒";s:2:"22";s:3:"㉓";s:2:"23";s:3:"㉔";s:2:"24";s:3:"㉕";s:2:"25";s:3:"㉖";s:2:"26";s:3:"㉗";s:2:"27";s:3:"㉘";s:2:"28";s:3:"㉙";s:2:"29";s:3:"㉚";s:2:"30";s:3:"㉛";s:2:"31";s:3:"㉜";s:2:"32";s:3:"㉝";s:2:"33";s:3:"㉞";s:2:"34";s:3:"㉟";s:2:"35";s:3:"㉠";s:3:"ᄀ";s:3:"㉡";s:3:"ᄂ";s:3:"㉢";s:3:"ᄃ";s:3:"㉣";s:3:"ᄅ";s:3:"㉤";s:3:"ᄆ";s:3:"㉥";s:3:"ᄇ";s:3:"㉦";s:3:"ᄉ";s:3:"㉧";s:3:"ᄋ";s:3:"㉨";s:3:"ᄌ";s:3:"㉩";s:3:"ᄎ";s:3:"㉪";s:3:"ᄏ";s:3:"㉫";s:3:"ᄐ";s:3:"㉬";s:3:"ᄑ";s:3:"㉭";s:3:"ᄒ";s:3:"㉮";s:6:"가";s:3:"㉯";s:6:"나";s:3:"㉰";s:6:"다";s:3:"㉱";s:6:"라";s:3:"㉲";s:6:"마";s:3:"㉳";s:6:"바";s:3:"㉴";s:6:"사";s:3:"㉵";s:6:"아";s:3:"㉶";s:6:"자";s:3:"㉷";s:6:"차";s:3:"㉸";s:6:"카";s:3:"㉹";s:6:"타";s:3:"㉺";s:6:"파";s:3:"㉻";s:6:"하";s:3:"㉼";s:15:"참고";s:3:"㉽";s:12:"주의";s:3:"㉾";s:6:"우";s:3:"㊀";s:3:"一";s:3:"㊁";s:3:"二";s:3:"㊂";s:3:"三";s:3:"㊃";s:3:"四";s:3:"㊄";s:3:"五";s:3:"㊅";s:3:"六";s:3:"㊆";s:3:"七";s:3:"㊇";s:3:"八";s:3:"㊈";s:3:"九";s:3:"㊉";s:3:"十";s:3:"㊊";s:3:"月";s:3:"㊋";s:3:"火";s:3:"㊌";s:3:"水";s:3:"㊍";s:3:"木";s:3:"㊎";s:3:"金";s:3:"㊏";s:3:"土";s:3:"㊐";s:3:"日";s:3:"㊑";s:3:"株";s:3:"㊒";s:3:"有";s:3:"㊓";s:3:"社";s:3:"㊔";s:3:"名";s:3:"㊕";s:3:"特";s:3:"㊖";s:3:"財";s:3:"㊗";s:3:"祝";s:3:"㊘";s:3:"労";s:3:"㊙";s:3:"秘";s:3:"㊚";s:3:"男";s:3:"㊛";s:3:"女";s:3:"㊜";s:3:"適";s:3:"㊝";s:3:"優";s:3:"㊞";s:3:"印";s:3:"㊟";s:3:"注";s:3:"㊠";s:3:"項";s:3:"㊡";s:3:"休";s:3:"㊢";s:3:"写";s:3:"㊣";s:3:"正";s:3:"㊤";s:3:"上";s:3:"㊥";s:3:"中";s:3:"㊦";s:3:"下";s:3:"㊧";s:3:"左";s:3:"㊨";s:3:"右";s:3:"㊩";s:3:"医";s:3:"㊪";s:3:"宗";s:3:"㊫";s:3:"学";s:3:"㊬";s:3:"監";s:3:"㊭";s:3:"企";s:3:"㊮";s:3:"資";s:3:"㊯";s:3:"協";s:3:"㊰";s:3:"夜";s:3:"㊱";s:2:"36";s:3:"㊲";s:2:"37";s:3:"㊳";s:2:"38";s:3:"㊴";s:2:"39";s:3:"㊵";s:2:"40";s:3:"㊶";s:2:"41";s:3:"㊷";s:2:"42";s:3:"㊸";s:2:"43";s:3:"㊹";s:2:"44";s:3:"㊺";s:2:"45";s:3:"㊻";s:2:"46";s:3:"㊼";s:2:"47";s:3:"㊽";s:2:"48";s:3:"㊾";s:2:"49";s:3:"㊿";s:2:"50";s:3:"㋀";s:4:"1月";s:3:"㋁";s:4:"2月";s:3:"㋂";s:4:"3月";s:3:"㋃";s:4:"4月";s:3:"㋄";s:4:"5月";s:3:"㋅";s:4:"6月";s:3:"㋆";s:4:"7月";s:3:"㋇";s:4:"8月";s:3:"㋈";s:4:"9月";s:3:"㋉";s:5:"10月";s:3:"㋊";s:5:"11月";s:3:"㋋";s:5:"12月";s:3:"㋌";s:2:"Hg";s:3:"㋍";s:3:"erg";s:3:"㋎";s:2:"eV";s:3:"㋏";s:3:"LTD";s:3:"㋐";s:3:"ア";s:3:"㋑";s:3:"イ";s:3:"㋒";s:3:"ウ";s:3:"㋓";s:3:"エ";s:3:"㋔";s:3:"オ";s:3:"㋕";s:3:"カ";s:3:"㋖";s:3:"キ";s:3:"㋗";s:3:"ク";s:3:"㋘";s:3:"ケ";s:3:"㋙";s:3:"コ";s:3:"㋚";s:3:"サ";s:3:"㋛";s:3:"シ";s:3:"㋜";s:3:"ス";s:3:"㋝";s:3:"セ";s:3:"㋞";s:3:"ソ";s:3:"㋟";s:3:"タ";s:3:"㋠";s:3:"チ";s:3:"㋡";s:3:"ツ";s:3:"㋢";s:3:"テ";s:3:"㋣";s:3:"ト";s:3:"㋤";s:3:"ナ";s:3:"㋥";s:3:"ニ";s:3:"㋦";s:3:"ヌ";s:3:"㋧";s:3:"ネ";s:3:"㋨";s:3:"ノ";s:3:"㋩";s:3:"ハ";s:3:"㋪";s:3:"ヒ";s:3:"㋫";s:3:"フ";s:3:"㋬";s:3:"ヘ";s:3:"㋭";s:3:"ホ";s:3:"㋮";s:3:"マ";s:3:"㋯";s:3:"ミ";s:3:"㋰";s:3:"ム";s:3:"㋱";s:3:"メ";s:3:"㋲";s:3:"モ";s:3:"㋳";s:3:"ヤ";s:3:"㋴";s:3:"ユ";s:3:"㋵";s:3:"ヨ";s:3:"㋶";s:3:"ラ";s:3:"㋷";s:3:"リ";s:3:"㋸";s:3:"ル";s:3:"㋹";s:3:"レ";s:3:"㋺";s:3:"ロ";s:3:"㋻";s:3:"ワ";s:3:"㋼";s:3:"ヰ";s:3:"㋽";s:3:"ヱ";s:3:"㋾";s:3:"ヲ";s:3:"㌀";s:15:"アパート";s:3:"㌁";s:12:"アルファ";s:3:"㌂";s:15:"アンペア";s:3:"㌃";s:9:"アール";s:3:"㌄";s:15:"イニング";s:3:"㌅";s:9:"インチ";s:3:"㌆";s:9:"ウォン";s:3:"㌇";s:18:"エスクード";s:3:"㌈";s:12:"エーカー";s:3:"㌉";s:9:"オンス";s:3:"㌊";s:9:"オーム";s:3:"㌋";s:9:"カイリ";s:3:"㌌";s:12:"カラット";s:3:"㌍";s:12:"カロリー";s:3:"㌎";s:12:"ガロン";s:3:"㌏";s:12:"ガンマ";s:3:"㌐";s:12:"ギガ";s:3:"㌑";s:12:"ギニー";s:3:"㌒";s:12:"キュリー";s:3:"㌓";s:18:"ギルダー";s:3:"㌔";s:6:"キロ";s:3:"㌕";s:18:"キログラム";s:3:"㌖";s:18:"キロメートル";s:3:"㌗";s:15:"キロワット";s:3:"㌘";s:12:"グラム";s:3:"㌙";s:18:"グラムトン";s:3:"㌚";s:18:"クルゼイロ";s:3:"㌛";s:12:"クローネ";s:3:"㌜";s:9:"ケース";s:3:"㌝";s:9:"コルナ";s:3:"㌞";s:12:"コーポ";s:3:"㌟";s:12:"サイクル";s:3:"㌠";s:15:"サンチーム";s:3:"㌡";s:15:"シリング";s:3:"㌢";s:9:"センチ";s:3:"㌣";s:9:"セント";s:3:"㌤";s:12:"ダース";s:3:"㌥";s:9:"デシ";s:3:"㌦";s:9:"ドル";s:3:"㌧";s:6:"トン";s:3:"㌨";s:6:"ナノ";s:3:"㌩";s:9:"ノット";s:3:"㌪";s:9:"ハイツ";s:3:"㌫";s:18:"パーセント";s:3:"㌬";s:12:"パーツ";s:3:"㌭";s:15:"バーレル";s:3:"㌮";s:18:"ピアストル";s:3:"㌯";s:12:"ピクル";s:3:"㌰";s:9:"ピコ";s:3:"㌱";s:9:"ビル";s:3:"㌲";s:18:"ファラッド";s:3:"㌳";s:12:"フィート";s:3:"㌴";s:18:"ブッシェル";s:3:"㌵";s:9:"フラン";s:3:"㌶";s:15:"ヘクタール";s:3:"㌷";s:9:"ペソ";s:3:"㌸";s:12:"ペニヒ";s:3:"㌹";s:9:"ヘルツ";s:3:"㌺";s:12:"ペンス";s:3:"㌻";s:15:"ページ";s:3:"㌼";s:12:"ベータ";s:3:"㌽";s:15:"ポイント";s:3:"㌾";s:12:"ボルト";s:3:"㌿";s:6:"ホン";s:3:"㍀";s:15:"ポンド";s:3:"㍁";s:9:"ホール";s:3:"㍂";s:9:"ホーン";s:3:"㍃";s:12:"マイクロ";s:3:"㍄";s:9:"マイル";s:3:"㍅";s:9:"マッハ";s:3:"㍆";s:9:"マルク";s:3:"㍇";s:15:"マンション";s:3:"㍈";s:12:"ミクロン";s:3:"㍉";s:6:"ミリ";s:3:"㍊";s:18:"ミリバール";s:3:"㍋";s:9:"メガ";s:3:"㍌";s:15:"メガトン";s:3:"㍍";s:12:"メートル";s:3:"㍎";s:12:"ヤード";s:3:"㍏";s:9:"ヤール";s:3:"㍐";s:9:"ユアン";s:3:"㍑";s:12:"リットル";s:3:"㍒";s:6:"リラ";s:3:"㍓";s:12:"ルピー";s:3:"㍔";s:15:"ルーブル";s:3:"㍕";s:6:"レム";s:3:"㍖";s:18:"レントゲン";s:3:"㍗";s:9:"ワット";s:3:"㍘";s:4:"0点";s:3:"㍙";s:4:"1点";s:3:"㍚";s:4:"2点";s:3:"㍛";s:4:"3点";s:3:"㍜";s:4:"4点";s:3:"㍝";s:4:"5点";s:3:"㍞";s:4:"6点";s:3:"㍟";s:4:"7点";s:3:"㍠";s:4:"8点";s:3:"㍡";s:4:"9点";s:3:"㍢";s:5:"10点";s:3:"㍣";s:5:"11点";s:3:"㍤";s:5:"12点";s:3:"㍥";s:5:"13点";s:3:"㍦";s:5:"14点";s:3:"㍧";s:5:"15点";s:3:"㍨";s:5:"16点";s:3:"㍩";s:5:"17点";s:3:"㍪";s:5:"18点";s:3:"㍫";s:5:"19点";s:3:"㍬";s:5:"20点";s:3:"㍭";s:5:"21点";s:3:"㍮";s:5:"22点";s:3:"㍯";s:5:"23点";s:3:"㍰";s:5:"24点";s:3:"㍱";s:3:"hPa";s:3:"㍲";s:2:"da";s:3:"㍳";s:2:"AU";s:3:"㍴";s:3:"bar";s:3:"㍵";s:2:"oV";s:3:"㍶";s:2:"pc";s:3:"㍷";s:2:"dm";s:3:"㍸";s:3:"dm2";s:3:"㍹";s:3:"dm3";s:3:"㍺";s:2:"IU";s:3:"㍻";s:6:"平成";s:3:"㍼";s:6:"昭和";s:3:"㍽";s:6:"大正";s:3:"㍾";s:6:"明治";s:3:"㍿";s:12:"株式会社";s:3:"㎀";s:2:"pA";s:3:"㎁";s:2:"nA";s:3:"㎂";s:3:"μA";s:3:"㎃";s:2:"mA";s:3:"㎄";s:2:"kA";s:3:"㎅";s:2:"KB";s:3:"㎆";s:2:"MB";s:3:"㎇";s:2:"GB";s:3:"㎈";s:3:"cal";s:3:"㎉";s:4:"kcal";s:3:"㎊";s:2:"pF";s:3:"㎋";s:2:"nF";s:3:"㎌";s:3:"μF";s:3:"㎍";s:3:"μg";s:3:"㎎";s:2:"mg";s:3:"㎏";s:2:"kg";s:3:"㎐";s:2:"Hz";s:3:"㎑";s:3:"kHz";s:3:"㎒";s:3:"MHz";s:3:"㎓";s:3:"GHz";s:3:"㎔";s:3:"THz";s:3:"㎕";s:3:"μl";s:3:"㎖";s:2:"ml";s:3:"㎗";s:2:"dl";s:3:"㎘";s:2:"kl";s:3:"㎙";s:2:"fm";s:3:"㎚";s:2:"nm";s:3:"㎛";s:3:"μm";s:3:"㎜";s:2:"mm";s:3:"㎝";s:2:"cm";s:3:"㎞";s:2:"km";s:3:"㎟";s:3:"mm2";s:3:"㎠";s:3:"cm2";s:3:"㎡";s:2:"m2";s:3:"㎢";s:3:"km2";s:3:"㎣";s:3:"mm3";s:3:"㎤";s:3:"cm3";s:3:"㎥";s:2:"m3";s:3:"㎦";s:3:"km3";s:3:"㎧";s:5:"m∕s";s:3:"㎨";s:6:"m∕s2";s:3:"㎩";s:2:"Pa";s:3:"㎪";s:3:"kPa";s:3:"㎫";s:3:"MPa";s:3:"㎬";s:3:"GPa";s:3:"㎭";s:3:"rad";s:3:"㎮";s:7:"rad∕s";s:3:"㎯";s:8:"rad∕s2";s:3:"㎰";s:2:"ps";s:3:"㎱";s:2:"ns";s:3:"㎲";s:3:"μs";s:3:"㎳";s:2:"ms";s:3:"㎴";s:2:"pV";s:3:"㎵";s:2:"nV";s:3:"㎶";s:3:"μV";s:3:"㎷";s:2:"mV";s:3:"㎸";s:2:"kV";s:3:"㎹";s:2:"MV";s:3:"㎺";s:2:"pW";s:3:"㎻";s:2:"nW";s:3:"㎼";s:3:"μW";s:3:"㎽";s:2:"mW";s:3:"㎾";s:2:"kW";s:3:"㎿";s:2:"MW";s:3:"㏀";s:3:"kΩ";s:3:"㏁";s:3:"MΩ";s:3:"㏂";s:4:"a.m.";s:3:"㏃";s:2:"Bq";s:3:"㏄";s:2:"cc";s:3:"㏅";s:2:"cd";s:3:"㏆";s:6:"C∕kg";s:3:"㏇";s:3:"Co.";s:3:"㏈";s:2:"dB";s:3:"㏉";s:2:"Gy";s:3:"㏊";s:2:"ha";s:3:"㏋";s:2:"HP";s:3:"㏌";s:2:"in";s:3:"㏍";s:2:"KK";s:3:"㏎";s:2:"KM";s:3:"㏏";s:2:"kt";s:3:"㏐";s:2:"lm";s:3:"㏑";s:2:"ln";s:3:"㏒";s:3:"log";s:3:"㏓";s:2:"lx";s:3:"㏔";s:2:"mb";s:3:"㏕";s:3:"mil";s:3:"㏖";s:3:"mol";s:3:"㏗";s:2:"PH";s:3:"㏘";s:4:"p.m.";s:3:"㏙";s:3:"PPM";s:3:"㏚";s:2:"PR";s:3:"㏛";s:2:"sr";s:3:"㏜";s:2:"Sv";s:3:"㏝";s:2:"Wb";s:3:"㏞";s:5:"V∕m";s:3:"㏟";s:5:"A∕m";s:3:"㏠";s:4:"1日";s:3:"㏡";s:4:"2日";s:3:"㏢";s:4:"3日";s:3:"㏣";s:4:"4日";s:3:"㏤";s:4:"5日";s:3:"㏥";s:4:"6日";s:3:"㏦";s:4:"7日";s:3:"㏧";s:4:"8日";s:3:"㏨";s:4:"9日";s:3:"㏩";s:5:"10日";s:3:"㏪";s:5:"11日";s:3:"㏫";s:5:"12日";s:3:"㏬";s:5:"13日";s:3:"㏭";s:5:"14日";s:3:"㏮";s:5:"15日";s:3:"㏯";s:5:"16日";s:3:"㏰";s:5:"17日";s:3:"㏱";s:5:"18日";s:3:"㏲";s:5:"19日";s:3:"㏳";s:5:"20日";s:3:"㏴";s:5:"21日";s:3:"㏵";s:5:"22日";s:3:"㏶";s:5:"23日";s:3:"㏷";s:5:"24日";s:3:"㏸";s:5:"25日";s:3:"㏹";s:5:"26日";s:3:"㏺";s:5:"27日";s:3:"㏻";s:5:"28日";s:3:"㏼";s:5:"29日";s:3:"㏽";s:5:"30日";s:3:"㏾";s:5:"31日";s:3:"㏿";s:3:"gal";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:3:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:3:"廊";s:3:"朗";s:3:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:3:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:3:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"樂";s:3:"樂";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:3:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:3:"異";s:3:"北";s:3:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:3:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:3:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"說";s:3:"說";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"寧";s:3:"寧";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"樂";s:3:"樂";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:3:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"率";s:3:"率";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:3:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:3:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:3:"侮";s:3:"僧";s:3:"僧";s:3:"免";s:3:"免";s:3:"勉";s:3:"勉";s:3:"勤";s:3:"勤";s:3:"卑";s:3:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:3:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:3:"屮";s:3:"悔";s:3:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:3:"憎";s:3:"懲";s:3:"懲";s:3:"敏";s:3:"敏";s:3:"既";s:3:"既";s:3:"暑";s:3:"暑";s:3:"梅";s:3:"梅";s:3:"海";s:3:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:3:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:3:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"練";s:3:"練";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:3:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"著";s:3:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"逸";s:3:"逸";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:3:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:3:"勇";s:3:"勺";s:3:"勺";s:3:"喝";s:3:"喝";s:3:"啕";s:3:"啕";s:3:"喙";s:3:"喙";s:3:"嗢";s:3:"嗢";s:3:"塚";s:3:"塚";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:3:"慎";s:3:"愈";s:3:"愈";s:3:"憎";s:3:"憎";s:3:"慠";s:3:"慠";s:3:"懲";s:3:"懲";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"晴";s:3:"晴";s:3:"朗";s:3:"朗";s:3:"望";s:3:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"殺";s:3:"殺";s:3:"流";s:3:"流";s:3:"滛";s:3:"滛";s:3:"滋";s:3:"滋";s:3:"漢";s:3:"漢";s:3:"瀞";s:3:"瀞";s:3:"煮";s:3:"煮";s:3:"瞧";s:3:"瞧";s:3:"爵";s:3:"爵";s:3:"犯";s:3:"犯";s:3:"猪";s:3:"猪";s:3:"瑱";s:3:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"益";s:3:"益";s:3:"盛";s:3:"盛";s:3:"直";s:3:"直";s:3:"睊";s:3:"睊";s:3:"着";s:3:"着";s:3:"磌";s:3:"磌";s:3:"窱";s:3:"窱";s:3:"節";s:3:"節";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"練";s:3:"練";s:3:"缾";s:3:"缾";s:3:"者";s:3:"者";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:3:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"視";s:3:"視";s:3:"調";s:3:"調";s:3:"諸";s:3:"諸";s:3:"請";s:3:"請";s:3:"謁";s:3:"謁";s:3:"諾";s:3:"諾";s:3:"諭";s:3:"諭";s:3:"謹";s:3:"謹";s:3:"變";s:3:"變";s:3:"贈";s:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s:3:"韛";s:3:"響";s:3:"響";s:3:"頋";s:3:"頋";s:3:"頻";s:3:"頻";s:3:"鬒";s:3:"鬒";s:3:"龜";s:3:"龜";s:3:"𢡊";s:4:"𢡊";s:3:"𢡄";s:4:"𢡄";s:3:"𣏕";s:4:"𣏕";s:3:"㮝";s:3:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:3:"䀹";s:3:"𥉉";s:4:"𥉉";s:3:"𥳐";s:4:"𥳐";s:3:"𧻓";s:4:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"ff";s:2:"ff";s:3:"fi";s:2:"fi";s:3:"fl";s:2:"fl";s:3:"ffi";s:3:"ffi";s:3:"ffl";s:3:"ffl";s:3:"ſt";s:2:"st";s:3:"st";s:2:"st";s:3:"ﬓ";s:4:"մն";s:3:"ﬔ";s:4:"մե";s:3:"ﬕ";s:4:"մի";s:3:"ﬖ";s:4:"վն";s:3:"ﬗ";s:4:"մխ";s:3:"יִ";s:4:"יִ";s:3:"ײַ";s:4:"ײַ";s:3:"ﬠ";s:2:"ע";s:3:"ﬡ";s:2:"א";s:3:"ﬢ";s:2:"ד";s:3:"ﬣ";s:2:"ה";s:3:"ﬤ";s:2:"כ";s:3:"ﬥ";s:2:"ל";s:3:"ﬦ";s:2:"ם";s:3:"ﬧ";s:2:"ר";s:3:"ﬨ";s:2:"ת";s:3:"﬩";s:1:"+";s:3:"שׁ";s:4:"שׁ";s:3:"שׂ";s:4:"שׂ";s:3:"שּׁ";s:6:"שּׁ";s:3:"שּׂ";s:6:"שּׂ";s:3:"אַ";s:4:"אַ";s:3:"אָ";s:4:"אָ";s:3:"אּ";s:4:"אּ";s:3:"בּ";s:4:"בּ";s:3:"גּ";s:4:"גּ";s:3:"דּ";s:4:"דּ";s:3:"הּ";s:4:"הּ";s:3:"וּ";s:4:"וּ";s:3:"זּ";s:4:"זּ";s:3:"טּ";s:4:"טּ";s:3:"יּ";s:4:"יּ";s:3:"ךּ";s:4:"ךּ";s:3:"כּ";s:4:"כּ";s:3:"לּ";s:4:"לּ";s:3:"מּ";s:4:"מּ";s:3:"נּ";s:4:"נּ";s:3:"סּ";s:4:"סּ";s:3:"ףּ";s:4:"ףּ";s:3:"פּ";s:4:"פּ";s:3:"צּ";s:4:"צּ";s:3:"קּ";s:4:"קּ";s:3:"רּ";s:4:"רּ";s:3:"שּ";s:4:"שּ";s:3:"תּ";s:4:"תּ";s:3:"וֹ";s:4:"וֹ";s:3:"בֿ";s:4:"בֿ";s:3:"כֿ";s:4:"כֿ";s:3:"פֿ";s:4:"פֿ";s:3:"ﭏ";s:4:"אל";s:3:"ﭐ";s:2:"ٱ";s:3:"ﭑ";s:2:"ٱ";s:3:"ﭒ";s:2:"ٻ";s:3:"ﭓ";s:2:"ٻ";s:3:"ﭔ";s:2:"ٻ";s:3:"ﭕ";s:2:"ٻ";s:3:"ﭖ";s:2:"پ";s:3:"ﭗ";s:2:"پ";s:3:"ﭘ";s:2:"پ";s:3:"ﭙ";s:2:"پ";s:3:"ﭚ";s:2:"ڀ";s:3:"ﭛ";s:2:"ڀ";s:3:"ﭜ";s:2:"ڀ";s:3:"ﭝ";s:2:"ڀ";s:3:"ﭞ";s:2:"ٺ";s:3:"ﭟ";s:2:"ٺ";s:3:"ﭠ";s:2:"ٺ";s:3:"ﭡ";s:2:"ٺ";s:3:"ﭢ";s:2:"ٿ";s:3:"ﭣ";s:2:"ٿ";s:3:"ﭤ";s:2:"ٿ";s:3:"ﭥ";s:2:"ٿ";s:3:"ﭦ";s:2:"ٹ";s:3:"ﭧ";s:2:"ٹ";s:3:"ﭨ";s:2:"ٹ";s:3:"ﭩ";s:2:"ٹ";s:3:"ﭪ";s:2:"ڤ";s:3:"ﭫ";s:2:"ڤ";s:3:"ﭬ";s:2:"ڤ";s:3:"ﭭ";s:2:"ڤ";s:3:"ﭮ";s:2:"ڦ";s:3:"ﭯ";s:2:"ڦ";s:3:"ﭰ";s:2:"ڦ";s:3:"ﭱ";s:2:"ڦ";s:3:"ﭲ";s:2:"ڄ";s:3:"ﭳ";s:2:"ڄ";s:3:"ﭴ";s:2:"ڄ";s:3:"ﭵ";s:2:"ڄ";s:3:"ﭶ";s:2:"ڃ";s:3:"ﭷ";s:2:"ڃ";s:3:"ﭸ";s:2:"ڃ";s:3:"ﭹ";s:2:"ڃ";s:3:"ﭺ";s:2:"چ";s:3:"ﭻ";s:2:"چ";s:3:"ﭼ";s:2:"چ";s:3:"ﭽ";s:2:"چ";s:3:"ﭾ";s:2:"ڇ";s:3:"ﭿ";s:2:"ڇ";s:3:"ﮀ";s:2:"ڇ";s:3:"ﮁ";s:2:"ڇ";s:3:"ﮂ";s:2:"ڍ";s:3:"ﮃ";s:2:"ڍ";s:3:"ﮄ";s:2:"ڌ";s:3:"ﮅ";s:2:"ڌ";s:3:"ﮆ";s:2:"ڎ";s:3:"ﮇ";s:2:"ڎ";s:3:"ﮈ";s:2:"ڈ";s:3:"ﮉ";s:2:"ڈ";s:3:"ﮊ";s:2:"ژ";s:3:"ﮋ";s:2:"ژ";s:3:"ﮌ";s:2:"ڑ";s:3:"ﮍ";s:2:"ڑ";s:3:"ﮎ";s:2:"ک";s:3:"ﮏ";s:2:"ک";s:3:"ﮐ";s:2:"ک";s:3:"ﮑ";s:2:"ک";s:3:"ﮒ";s:2:"گ";s:3:"ﮓ";s:2:"گ";s:3:"ﮔ";s:2:"گ";s:3:"ﮕ";s:2:"گ";s:3:"ﮖ";s:2:"ڳ";s:3:"ﮗ";s:2:"ڳ";s:3:"ﮘ";s:2:"ڳ";s:3:"ﮙ";s:2:"ڳ";s:3:"ﮚ";s:2:"ڱ";s:3:"ﮛ";s:2:"ڱ";s:3:"ﮜ";s:2:"ڱ";s:3:"ﮝ";s:2:"ڱ";s:3:"ﮞ";s:2:"ں";s:3:"ﮟ";s:2:"ں";s:3:"ﮠ";s:2:"ڻ";s:3:"ﮡ";s:2:"ڻ";s:3:"ﮢ";s:2:"ڻ";s:3:"ﮣ";s:2:"ڻ";s:3:"ﮤ";s:4:"ۀ";s:3:"ﮥ";s:4:"ۀ";s:3:"ﮦ";s:2:"ہ";s:3:"ﮧ";s:2:"ہ";s:3:"ﮨ";s:2:"ہ";s:3:"ﮩ";s:2:"ہ";s:3:"ﮪ";s:2:"ھ";s:3:"ﮫ";s:2:"ھ";s:3:"ﮬ";s:2:"ھ";s:3:"ﮭ";s:2:"ھ";s:3:"ﮮ";s:2:"ے";s:3:"ﮯ";s:2:"ے";s:3:"ﮰ";s:4:"ۓ";s:3:"ﮱ";s:4:"ۓ";s:3:"ﯓ";s:2:"ڭ";s:3:"ﯔ";s:2:"ڭ";s:3:"ﯕ";s:2:"ڭ";s:3:"ﯖ";s:2:"ڭ";s:3:"ﯗ";s:2:"ۇ";s:3:"ﯘ";s:2:"ۇ";s:3:"ﯙ";s:2:"ۆ";s:3:"ﯚ";s:2:"ۆ";s:3:"ﯛ";s:2:"ۈ";s:3:"ﯜ";s:2:"ۈ";s:3:"ﯝ";s:4:"ۇٴ";s:3:"ﯞ";s:2:"ۋ";s:3:"ﯟ";s:2:"ۋ";s:3:"ﯠ";s:2:"ۅ";s:3:"ﯡ";s:2:"ۅ";s:3:"ﯢ";s:2:"ۉ";s:3:"ﯣ";s:2:"ۉ";s:3:"ﯤ";s:2:"ې";s:3:"ﯥ";s:2:"ې";s:3:"ﯦ";s:2:"ې";s:3:"ﯧ";s:2:"ې";s:3:"ﯨ";s:2:"ى";s:3:"ﯩ";s:2:"ى";s:3:"ﯪ";s:6:"ئا";s:3:"ﯫ";s:6:"ئا";s:3:"ﯬ";s:6:"ئە";s:3:"ﯭ";s:6:"ئە";s:3:"ﯮ";s:6:"ئو";s:3:"ﯯ";s:6:"ئو";s:3:"ﯰ";s:6:"ئۇ";s:3:"ﯱ";s:6:"ئۇ";s:3:"ﯲ";s:6:"ئۆ";s:3:"ﯳ";s:6:"ئۆ";s:3:"ﯴ";s:6:"ئۈ";s:3:"ﯵ";s:6:"ئۈ";s:3:"ﯶ";s:6:"ئې";s:3:"ﯷ";s:6:"ئې";s:3:"ﯸ";s:6:"ئې";s:3:"ﯹ";s:6:"ئى";s:3:"ﯺ";s:6:"ئى";s:3:"ﯻ";s:6:"ئى";s:3:"ﯼ";s:2:"ی";s:3:"ﯽ";s:2:"ی";s:3:"ﯾ";s:2:"ی";s:3:"ﯿ";s:2:"ی";s:3:"ﰀ";s:6:"ئج";s:3:"ﰁ";s:6:"ئح";s:3:"ﰂ";s:6:"ئم";s:3:"ﰃ";s:6:"ئى";s:3:"ﰄ";s:6:"ئي";s:3:"ﰅ";s:4:"بج";s:3:"ﰆ";s:4:"بح";s:3:"ﰇ";s:4:"بخ";s:3:"ﰈ";s:4:"بم";s:3:"ﰉ";s:4:"بى";s:3:"ﰊ";s:4:"بي";s:3:"ﰋ";s:4:"تج";s:3:"ﰌ";s:4:"تح";s:3:"ﰍ";s:4:"تخ";s:3:"ﰎ";s:4:"تم";s:3:"ﰏ";s:4:"تى";s:3:"ﰐ";s:4:"تي";s:3:"ﰑ";s:4:"ثج";s:3:"ﰒ";s:4:"ثم";s:3:"ﰓ";s:4:"ثى";s:3:"ﰔ";s:4:"ثي";s:3:"ﰕ";s:4:"جح";s:3:"ﰖ";s:4:"جم";s:3:"ﰗ";s:4:"حج";s:3:"ﰘ";s:4:"حم";s:3:"ﰙ";s:4:"خج";s:3:"ﰚ";s:4:"خح";s:3:"ﰛ";s:4:"خم";s:3:"ﰜ";s:4:"سج";s:3:"ﰝ";s:4:"سح";s:3:"ﰞ";s:4:"سخ";s:3:"ﰟ";s:4:"سم";s:3:"ﰠ";s:4:"صح";s:3:"ﰡ";s:4:"صم";s:3:"ﰢ";s:4:"ضج";s:3:"ﰣ";s:4:"ضح";s:3:"ﰤ";s:4:"ضخ";s:3:"ﰥ";s:4:"ضم";s:3:"ﰦ";s:4:"طح";s:3:"ﰧ";s:4:"طم";s:3:"ﰨ";s:4:"ظم";s:3:"ﰩ";s:4:"عج";s:3:"ﰪ";s:4:"عم";s:3:"ﰫ";s:4:"غج";s:3:"ﰬ";s:4:"غم";s:3:"ﰭ";s:4:"فج";s:3:"ﰮ";s:4:"فح";s:3:"ﰯ";s:4:"فخ";s:3:"ﰰ";s:4:"فم";s:3:"ﰱ";s:4:"فى";s:3:"ﰲ";s:4:"في";s:3:"ﰳ";s:4:"قح";s:3:"ﰴ";s:4:"قم";s:3:"ﰵ";s:4:"قى";s:3:"ﰶ";s:4:"قي";s:3:"ﰷ";s:4:"كا";s:3:"ﰸ";s:4:"كج";s:3:"ﰹ";s:4:"كح";s:3:"ﰺ";s:4:"كخ";s:3:"ﰻ";s:4:"كل";s:3:"ﰼ";s:4:"كم";s:3:"ﰽ";s:4:"كى";s:3:"ﰾ";s:4:"كي";s:3:"ﰿ";s:4:"لج";s:3:"ﱀ";s:4:"لح";s:3:"ﱁ";s:4:"لخ";s:3:"ﱂ";s:4:"لم";s:3:"ﱃ";s:4:"لى";s:3:"ﱄ";s:4:"لي";s:3:"ﱅ";s:4:"مج";s:3:"ﱆ";s:4:"مح";s:3:"ﱇ";s:4:"مخ";s:3:"ﱈ";s:4:"مم";s:3:"ﱉ";s:4:"مى";s:3:"ﱊ";s:4:"مي";s:3:"ﱋ";s:4:"نج";s:3:"ﱌ";s:4:"نح";s:3:"ﱍ";s:4:"نخ";s:3:"ﱎ";s:4:"نم";s:3:"ﱏ";s:4:"نى";s:3:"ﱐ";s:4:"ني";s:3:"ﱑ";s:4:"هج";s:3:"ﱒ";s:4:"هم";s:3:"ﱓ";s:4:"هى";s:3:"ﱔ";s:4:"هي";s:3:"ﱕ";s:4:"يج";s:3:"ﱖ";s:4:"يح";s:3:"ﱗ";s:4:"يخ";s:3:"ﱘ";s:4:"يم";s:3:"ﱙ";s:4:"يى";s:3:"ﱚ";s:4:"يي";s:3:"ﱛ";s:4:"ذٰ";s:3:"ﱜ";s:4:"رٰ";s:3:"ﱝ";s:4:"ىٰ";s:3:"ﱞ";s:5:" ٌّ";s:3:"ﱟ";s:5:" ٍّ";s:3:"ﱠ";s:5:" َّ";s:3:"ﱡ";s:5:" ُّ";s:3:"ﱢ";s:5:" ِّ";s:3:"ﱣ";s:5:" ّٰ";s:3:"ﱤ";s:6:"ئر";s:3:"ﱥ";s:6:"ئز";s:3:"ﱦ";s:6:"ئم";s:3:"ﱧ";s:6:"ئن";s:3:"ﱨ";s:6:"ئى";s:3:"ﱩ";s:6:"ئي";s:3:"ﱪ";s:4:"بر";s:3:"ﱫ";s:4:"بز";s:3:"ﱬ";s:4:"بم";s:3:"ﱭ";s:4:"بن";s:3:"ﱮ";s:4:"بى";s:3:"ﱯ";s:4:"بي";s:3:"ﱰ";s:4:"تر";s:3:"ﱱ";s:4:"تز";s:3:"ﱲ";s:4:"تم";s:3:"ﱳ";s:4:"تن";s:3:"ﱴ";s:4:"تى";s:3:"ﱵ";s:4:"تي";s:3:"ﱶ";s:4:"ثر";s:3:"ﱷ";s:4:"ثز";s:3:"ﱸ";s:4:"ثم";s:3:"ﱹ";s:4:"ثن";s:3:"ﱺ";s:4:"ثى";s:3:"ﱻ";s:4:"ثي";s:3:"ﱼ";s:4:"فى";s:3:"ﱽ";s:4:"في";s:3:"ﱾ";s:4:"قى";s:3:"ﱿ";s:4:"قي";s:3:"ﲀ";s:4:"كا";s:3:"ﲁ";s:4:"كل";s:3:"ﲂ";s:4:"كم";s:3:"ﲃ";s:4:"كى";s:3:"ﲄ";s:4:"كي";s:3:"ﲅ";s:4:"لم";s:3:"ﲆ";s:4:"لى";s:3:"ﲇ";s:4:"لي";s:3:"ﲈ";s:4:"ما";s:3:"ﲉ";s:4:"مم";s:3:"ﲊ";s:4:"نر";s:3:"ﲋ";s:4:"نز";s:3:"ﲌ";s:4:"نم";s:3:"ﲍ";s:4:"نن";s:3:"ﲎ";s:4:"نى";s:3:"ﲏ";s:4:"ني";s:3:"ﲐ";s:4:"ىٰ";s:3:"ﲑ";s:4:"ير";s:3:"ﲒ";s:4:"يز";s:3:"ﲓ";s:4:"يم";s:3:"ﲔ";s:4:"ين";s:3:"ﲕ";s:4:"يى";s:3:"ﲖ";s:4:"يي";s:3:"ﲗ";s:6:"ئج";s:3:"ﲘ";s:6:"ئح";s:3:"ﲙ";s:6:"ئخ";s:3:"ﲚ";s:6:"ئم";s:3:"ﲛ";s:6:"ئه";s:3:"ﲜ";s:4:"بج";s:3:"ﲝ";s:4:"بح";s:3:"ﲞ";s:4:"بخ";s:3:"ﲟ";s:4:"بم";s:3:"ﲠ";s:4:"به";s:3:"ﲡ";s:4:"تج";s:3:"ﲢ";s:4:"تح";s:3:"ﲣ";s:4:"تخ";s:3:"ﲤ";s:4:"تم";s:3:"ﲥ";s:4:"ته";s:3:"ﲦ";s:4:"ثم";s:3:"ﲧ";s:4:"جح";s:3:"ﲨ";s:4:"جم";s:3:"ﲩ";s:4:"حج";s:3:"ﲪ";s:4:"حم";s:3:"ﲫ";s:4:"خج";s:3:"ﲬ";s:4:"خم";s:3:"ﲭ";s:4:"سج";s:3:"ﲮ";s:4:"سح";s:3:"ﲯ";s:4:"سخ";s:3:"ﲰ";s:4:"سم";s:3:"ﲱ";s:4:"صح";s:3:"ﲲ";s:4:"صخ";s:3:"ﲳ";s:4:"صم";s:3:"ﲴ";s:4:"ضج";s:3:"ﲵ";s:4:"ضح";s:3:"ﲶ";s:4:"ضخ";s:3:"ﲷ";s:4:"ضم";s:3:"ﲸ";s:4:"طح";s:3:"ﲹ";s:4:"ظم";s:3:"ﲺ";s:4:"عج";s:3:"ﲻ";s:4:"عم";s:3:"ﲼ";s:4:"غج";s:3:"ﲽ";s:4:"غم";s:3:"ﲾ";s:4:"فج";s:3:"ﲿ";s:4:"فح";s:3:"ﳀ";s:4:"فخ";s:3:"ﳁ";s:4:"فم";s:3:"ﳂ";s:4:"قح";s:3:"ﳃ";s:4:"قم";s:3:"ﳄ";s:4:"كج";s:3:"ﳅ";s:4:"كح";s:3:"ﳆ";s:4:"كخ";s:3:"ﳇ";s:4:"كل";s:3:"ﳈ";s:4:"كم";s:3:"ﳉ";s:4:"لج";s:3:"ﳊ";s:4:"لح";s:3:"ﳋ";s:4:"لخ";s:3:"ﳌ";s:4:"لم";s:3:"ﳍ";s:4:"له";s:3:"ﳎ";s:4:"مج";s:3:"ﳏ";s:4:"مح";s:3:"ﳐ";s:4:"مخ";s:3:"ﳑ";s:4:"مم";s:3:"ﳒ";s:4:"نج";s:3:"ﳓ";s:4:"نح";s:3:"ﳔ";s:4:"نخ";s:3:"ﳕ";s:4:"نم";s:3:"ﳖ";s:4:"نه";s:3:"ﳗ";s:4:"هج";s:3:"ﳘ";s:4:"هم";s:3:"ﳙ";s:4:"هٰ";s:3:"ﳚ";s:4:"يج";s:3:"ﳛ";s:4:"يح";s:3:"ﳜ";s:4:"يخ";s:3:"ﳝ";s:4:"يم";s:3:"ﳞ";s:4:"يه";s:3:"ﳟ";s:6:"ئم";s:3:"ﳠ";s:6:"ئه";s:3:"ﳡ";s:4:"بم";s:3:"ﳢ";s:4:"به";s:3:"ﳣ";s:4:"تم";s:3:"ﳤ";s:4:"ته";s:3:"ﳥ";s:4:"ثم";s:3:"ﳦ";s:4:"ثه";s:3:"ﳧ";s:4:"سم";s:3:"ﳨ";s:4:"سه";s:3:"ﳩ";s:4:"شم";s:3:"ﳪ";s:4:"شه";s:3:"ﳫ";s:4:"كل";s:3:"ﳬ";s:4:"كم";s:3:"ﳭ";s:4:"لم";s:3:"ﳮ";s:4:"نم";s:3:"ﳯ";s:4:"نه";s:3:"ﳰ";s:4:"يم";s:3:"ﳱ";s:4:"يه";s:3:"ﳲ";s:6:"ـَّ";s:3:"ﳳ";s:6:"ـُّ";s:3:"ﳴ";s:6:"ـِّ";s:3:"ﳵ";s:4:"طى";s:3:"ﳶ";s:4:"طي";s:3:"ﳷ";s:4:"عى";s:3:"ﳸ";s:4:"عي";s:3:"ﳹ";s:4:"غى";s:3:"ﳺ";s:4:"غي";s:3:"ﳻ";s:4:"سى";s:3:"ﳼ";s:4:"سي";s:3:"ﳽ";s:4:"شى";s:3:"ﳾ";s:4:"شي";s:3:"ﳿ";s:4:"حى";s:3:"ﴀ";s:4:"حي";s:3:"ﴁ";s:4:"جى";s:3:"ﴂ";s:4:"جي";s:3:"ﴃ";s:4:"خى";s:3:"ﴄ";s:4:"خي";s:3:"ﴅ";s:4:"صى";s:3:"ﴆ";s:4:"صي";s:3:"ﴇ";s:4:"ضى";s:3:"ﴈ";s:4:"ضي";s:3:"ﴉ";s:4:"شج";s:3:"ﴊ";s:4:"شح";s:3:"ﴋ";s:4:"شخ";s:3:"ﴌ";s:4:"شم";s:3:"ﴍ";s:4:"شر";s:3:"ﴎ";s:4:"سر";s:3:"ﴏ";s:4:"صر";s:3:"ﴐ";s:4:"ضر";s:3:"ﴑ";s:4:"طى";s:3:"ﴒ";s:4:"طي";s:3:"ﴓ";s:4:"عى";s:3:"ﴔ";s:4:"عي";s:3:"ﴕ";s:4:"غى";s:3:"ﴖ";s:4:"غي";s:3:"ﴗ";s:4:"سى";s:3:"ﴘ";s:4:"سي";s:3:"ﴙ";s:4:"شى";s:3:"ﴚ";s:4:"شي";s:3:"ﴛ";s:4:"حى";s:3:"ﴜ";s:4:"حي";s:3:"ﴝ";s:4:"جى";s:3:"ﴞ";s:4:"جي";s:3:"ﴟ";s:4:"خى";s:3:"ﴠ";s:4:"خي";s:3:"ﴡ";s:4:"صى";s:3:"ﴢ";s:4:"صي";s:3:"ﴣ";s:4:"ضى";s:3:"ﴤ";s:4:"ضي";s:3:"ﴥ";s:4:"شج";s:3:"ﴦ";s:4:"شح";s:3:"ﴧ";s:4:"شخ";s:3:"ﴨ";s:4:"شم";s:3:"ﴩ";s:4:"شر";s:3:"ﴪ";s:4:"سر";s:3:"ﴫ";s:4:"صر";s:3:"ﴬ";s:4:"ضر";s:3:"ﴭ";s:4:"شج";s:3:"ﴮ";s:4:"شح";s:3:"ﴯ";s:4:"شخ";s:3:"ﴰ";s:4:"شم";s:3:"ﴱ";s:4:"سه";s:3:"ﴲ";s:4:"شه";s:3:"ﴳ";s:4:"طم";s:3:"ﴴ";s:4:"سج";s:3:"ﴵ";s:4:"سح";s:3:"ﴶ";s:4:"سخ";s:3:"ﴷ";s:4:"شج";s:3:"ﴸ";s:4:"شح";s:3:"ﴹ";s:4:"شخ";s:3:"ﴺ";s:4:"طم";s:3:"ﴻ";s:4:"ظم";s:3:"ﴼ";s:4:"اً";s:3:"ﴽ";s:4:"اً";s:3:"ﵐ";s:6:"تجم";s:3:"ﵑ";s:6:"تحج";s:3:"ﵒ";s:6:"تحج";s:3:"ﵓ";s:6:"تحم";s:3:"ﵔ";s:6:"تخم";s:3:"ﵕ";s:6:"تمج";s:3:"ﵖ";s:6:"تمح";s:3:"ﵗ";s:6:"تمخ";s:3:"ﵘ";s:6:"جمح";s:3:"ﵙ";s:6:"جمح";s:3:"ﵚ";s:6:"حمي";s:3:"ﵛ";s:6:"حمى";s:3:"ﵜ";s:6:"سحج";s:3:"ﵝ";s:6:"سجح";s:3:"ﵞ";s:6:"سجى";s:3:"ﵟ";s:6:"سمح";s:3:"ﵠ";s:6:"سمح";s:3:"ﵡ";s:6:"سمج";s:3:"ﵢ";s:6:"سمم";s:3:"ﵣ";s:6:"سمم";s:3:"ﵤ";s:6:"صحح";s:3:"ﵥ";s:6:"صحح";s:3:"ﵦ";s:6:"صمم";s:3:"ﵧ";s:6:"شحم";s:3:"ﵨ";s:6:"شحم";s:3:"ﵩ";s:6:"شجي";s:3:"ﵪ";s:6:"شمخ";s:3:"ﵫ";s:6:"شمخ";s:3:"ﵬ";s:6:"شمم";s:3:"ﵭ";s:6:"شمم";s:3:"ﵮ";s:6:"ضحى";s:3:"ﵯ";s:6:"ضخم";s:3:"ﵰ";s:6:"ضخم";s:3:"ﵱ";s:6:"طمح";s:3:"ﵲ";s:6:"طمح";s:3:"ﵳ";s:6:"طمم";s:3:"ﵴ";s:6:"طمي";s:3:"ﵵ";s:6:"عجم";s:3:"ﵶ";s:6:"عمم";s:3:"ﵷ";s:6:"عمم";s:3:"ﵸ";s:6:"عمى";s:3:"ﵹ";s:6:"غمم";s:3:"ﵺ";s:6:"غمي";s:3:"ﵻ";s:6:"غمى";s:3:"ﵼ";s:6:"فخم";s:3:"ﵽ";s:6:"فخم";s:3:"ﵾ";s:6:"قمح";s:3:"ﵿ";s:6:"قمم";s:3:"ﶀ";s:6:"لحم";s:3:"ﶁ";s:6:"لحي";s:3:"ﶂ";s:6:"لحى";s:3:"ﶃ";s:6:"لجج";s:3:"ﶄ";s:6:"لجج";s:3:"ﶅ";s:6:"لخم";s:3:"ﶆ";s:6:"لخم";s:3:"ﶇ";s:6:"لمح";s:3:"ﶈ";s:6:"لمح";s:3:"ﶉ";s:6:"محج";s:3:"ﶊ";s:6:"محم";s:3:"ﶋ";s:6:"محي";s:3:"ﶌ";s:6:"مجح";s:3:"ﶍ";s:6:"مجم";s:3:"ﶎ";s:6:"مخج";s:3:"ﶏ";s:6:"مخم";s:3:"ﶒ";s:6:"مجخ";s:3:"ﶓ";s:6:"همج";s:3:"ﶔ";s:6:"همم";s:3:"ﶕ";s:6:"نحم";s:3:"ﶖ";s:6:"نحى";s:3:"ﶗ";s:6:"نجم";s:3:"ﶘ";s:6:"نجم";s:3:"ﶙ";s:6:"نجى";s:3:"ﶚ";s:6:"نمي";s:3:"ﶛ";s:6:"نمى";s:3:"ﶜ";s:6:"يمم";s:3:"ﶝ";s:6:"يمم";s:3:"ﶞ";s:6:"بخي";s:3:"ﶟ";s:6:"تجي";s:3:"ﶠ";s:6:"تجى";s:3:"ﶡ";s:6:"تخي";s:3:"ﶢ";s:6:"تخى";s:3:"ﶣ";s:6:"تمي";s:3:"ﶤ";s:6:"تمى";s:3:"ﶥ";s:6:"جمي";s:3:"ﶦ";s:6:"جحى";s:3:"ﶧ";s:6:"جمى";s:3:"ﶨ";s:6:"سخى";s:3:"ﶩ";s:6:"صحي";s:3:"ﶪ";s:6:"شحي";s:3:"ﶫ";s:6:"ضحي";s:3:"ﶬ";s:6:"لجي";s:3:"ﶭ";s:6:"لمي";s:3:"ﶮ";s:6:"يحي";s:3:"ﶯ";s:6:"يجي";s:3:"ﶰ";s:6:"يمي";s:3:"ﶱ";s:6:"ممي";s:3:"ﶲ";s:6:"قمي";s:3:"ﶳ";s:6:"نحي";s:3:"ﶴ";s:6:"قمح";s:3:"ﶵ";s:6:"لحم";s:3:"ﶶ";s:6:"عمي";s:3:"ﶷ";s:6:"كمي";s:3:"ﶸ";s:6:"نجح";s:3:"ﶹ";s:6:"مخي";s:3:"ﶺ";s:6:"لجم";s:3:"ﶻ";s:6:"كمم";s:3:"ﶼ";s:6:"لجم";s:3:"ﶽ";s:6:"نجح";s:3:"ﶾ";s:6:"جحي";s:3:"ﶿ";s:6:"حجي";s:3:"ﷀ";s:6:"مجي";s:3:"ﷁ";s:6:"فمي";s:3:"ﷂ";s:6:"بحي";s:3:"ﷃ";s:6:"كمم";s:3:"ﷄ";s:6:"عجم";s:3:"ﷅ";s:6:"صمم";s:3:"ﷆ";s:6:"سخي";s:3:"ﷇ";s:6:"نجي";s:3:"ﷰ";s:6:"صلے";s:3:"ﷱ";s:6:"قلے";s:3:"ﷲ";s:8:"الله";s:3:"ﷳ";s:8:"اكبر";s:3:"ﷴ";s:8:"محمد";s:3:"ﷵ";s:8:"صلعم";s:3:"ﷶ";s:8:"رسول";s:3:"ﷷ";s:8:"عليه";s:3:"ﷸ";s:8:"وسلم";s:3:"ﷹ";s:6:"صلى";s:3:"ﷺ";s:33:"صلى الله عليه وسلم";s:3:"ﷻ";s:15:"جل جلاله";s:3:"﷼";s:8:"ریال";s:3:"︐";s:1:",";s:3:"︑";s:3:"、";s:3:"︒";s:3:"。";s:3:"︓";s:1:":";s:3:"︔";s:1:";";s:3:"︕";s:1:"!";s:3:"︖";s:1:"?";s:3:"︗";s:3:"〖";s:3:"︘";s:3:"〗";s:3:"︙";s:3:"...";s:3:"︰";s:2:"..";s:3:"︱";s:3:"—";s:3:"︲";s:3:"–";s:3:"︳";s:1:"_";s:3:"︴";s:1:"_";s:3:"︵";s:1:"(";s:3:"︶";s:1:")";s:3:"︷";s:1:"{";s:3:"︸";s:1:"}";s:3:"︹";s:3:"〔";s:3:"︺";s:3:"〕";s:3:"︻";s:3:"【";s:3:"︼";s:3:"】";s:3:"︽";s:3:"《";s:3:"︾";s:3:"》";s:3:"︿";s:3:"〈";s:3:"﹀";s:3:"〉";s:3:"﹁";s:3:"「";s:3:"﹂";s:3:"」";s:3:"﹃";s:3:"『";s:3:"﹄";s:3:"』";s:3:"﹇";s:1:"[";s:3:"﹈";s:1:"]";s:3:"﹉";s:3:" ̅";s:3:"﹊";s:3:" ̅";s:3:"﹋";s:3:" ̅";s:3:"﹌";s:3:" ̅";s:3:"﹍";s:1:"_";s:3:"﹎";s:1:"_";s:3:"﹏";s:1:"_";s:3:"﹐";s:1:",";s:3:"﹑";s:3:"、";s:3:"﹒";s:1:".";s:3:"﹔";s:1:";";s:3:"﹕";s:1:":";s:3:"﹖";s:1:"?";s:3:"﹗";s:1:"!";s:3:"﹘";s:3:"—";s:3:"﹙";s:1:"(";s:3:"﹚";s:1:")";s:3:"﹛";s:1:"{";s:3:"﹜";s:1:"}";s:3:"﹝";s:3:"〔";s:3:"﹞";s:3:"〕";s:3:"﹟";s:1:"#";s:3:"﹠";s:1:"&";s:3:"﹡";s:1:"*";s:3:"﹢";s:1:"+";s:3:"﹣";s:1:"-";s:3:"﹤";s:1:"<";s:3:"﹥";s:1:">";s:3:"﹦";s:1:"=";s:3:"﹨";s:1:"\\";s:3:"﹩";s:1:"$";s:3:"﹪";s:1:"%";s:3:"﹫";s:1:"@";s:3:"ﹰ";s:3:" ً";s:3:"ﹱ";s:4:"ـً";s:3:"ﹲ";s:3:" ٌ";s:3:"ﹴ";s:3:" ٍ";s:3:"ﹶ";s:3:" َ";s:3:"ﹷ";s:4:"ـَ";s:3:"ﹸ";s:3:" ُ";s:3:"ﹹ";s:4:"ـُ";s:3:"ﹺ";s:3:" ِ";s:3:"ﹻ";s:4:"ـِ";s:3:"ﹼ";s:3:" ّ";s:3:"ﹽ";s:4:"ـّ";s:3:"ﹾ";s:3:" ْ";s:3:"ﹿ";s:4:"ـْ";s:3:"ﺀ";s:2:"ء";s:3:"ﺁ";s:4:"آ";s:3:"ﺂ";s:4:"آ";s:3:"ﺃ";s:4:"أ";s:3:"ﺄ";s:4:"أ";s:3:"ﺅ";s:4:"ؤ";s:3:"ﺆ";s:4:"ؤ";s:3:"ﺇ";s:4:"إ";s:3:"ﺈ";s:4:"إ";s:3:"ﺉ";s:4:"ئ";s:3:"ﺊ";s:4:"ئ";s:3:"ﺋ";s:4:"ئ";s:3:"ﺌ";s:4:"ئ";s:3:"ﺍ";s:2:"ا";s:3:"ﺎ";s:2:"ا";s:3:"ﺏ";s:2:"ب";s:3:"ﺐ";s:2:"ب";s:3:"ﺑ";s:2:"ب";s:3:"ﺒ";s:2:"ب";s:3:"ﺓ";s:2:"ة";s:3:"ﺔ";s:2:"ة";s:3:"ﺕ";s:2:"ت";s:3:"ﺖ";s:2:"ت";s:3:"ﺗ";s:2:"ت";s:3:"ﺘ";s:2:"ت";s:3:"ﺙ";s:2:"ث";s:3:"ﺚ";s:2:"ث";s:3:"ﺛ";s:2:"ث";s:3:"ﺜ";s:2:"ث";s:3:"ﺝ";s:2:"ج";s:3:"ﺞ";s:2:"ج";s:3:"ﺟ";s:2:"ج";s:3:"ﺠ";s:2:"ج";s:3:"ﺡ";s:2:"ح";s:3:"ﺢ";s:2:"ح";s:3:"ﺣ";s:2:"ح";s:3:"ﺤ";s:2:"ح";s:3:"ﺥ";s:2:"خ";s:3:"ﺦ";s:2:"خ";s:3:"ﺧ";s:2:"خ";s:3:"ﺨ";s:2:"خ";s:3:"ﺩ";s:2:"د";s:3:"ﺪ";s:2:"د";s:3:"ﺫ";s:2:"ذ";s:3:"ﺬ";s:2:"ذ";s:3:"ﺭ";s:2:"ر";s:3:"ﺮ";s:2:"ر";s:3:"ﺯ";s:2:"ز";s:3:"ﺰ";s:2:"ز";s:3:"ﺱ";s:2:"س";s:3:"ﺲ";s:2:"س";s:3:"ﺳ";s:2:"س";s:3:"ﺴ";s:2:"س";s:3:"ﺵ";s:2:"ش";s:3:"ﺶ";s:2:"ش";s:3:"ﺷ";s:2:"ش";s:3:"ﺸ";s:2:"ش";s:3:"ﺹ";s:2:"ص";s:3:"ﺺ";s:2:"ص";s:3:"ﺻ";s:2:"ص";s:3:"ﺼ";s:2:"ص";s:3:"ﺽ";s:2:"ض";s:3:"ﺾ";s:2:"ض";s:3:"ﺿ";s:2:"ض";s:3:"ﻀ";s:2:"ض";s:3:"ﻁ";s:2:"ط";s:3:"ﻂ";s:2:"ط";s:3:"ﻃ";s:2:"ط";s:3:"ﻄ";s:2:"ط";s:3:"ﻅ";s:2:"ظ";s:3:"ﻆ";s:2:"ظ";s:3:"ﻇ";s:2:"ظ";s:3:"ﻈ";s:2:"ظ";s:3:"ﻉ";s:2:"ع";s:3:"ﻊ";s:2:"ع";s:3:"ﻋ";s:2:"ع";s:3:"ﻌ";s:2:"ع";s:3:"ﻍ";s:2:"غ";s:3:"ﻎ";s:2:"غ";s:3:"ﻏ";s:2:"غ";s:3:"ﻐ";s:2:"غ";s:3:"ﻑ";s:2:"ف";s:3:"ﻒ";s:2:"ف";s:3:"ﻓ";s:2:"ف";s:3:"ﻔ";s:2:"ف";s:3:"ﻕ";s:2:"ق";s:3:"ﻖ";s:2:"ق";s:3:"ﻗ";s:2:"ق";s:3:"ﻘ";s:2:"ق";s:3:"ﻙ";s:2:"ك";s:3:"ﻚ";s:2:"ك";s:3:"ﻛ";s:2:"ك";s:3:"ﻜ";s:2:"ك";s:3:"ﻝ";s:2:"ل";s:3:"ﻞ";s:2:"ل";s:3:"ﻟ";s:2:"ل";s:3:"ﻠ";s:2:"ل";s:3:"ﻡ";s:2:"م";s:3:"ﻢ";s:2:"م";s:3:"ﻣ";s:2:"م";s:3:"ﻤ";s:2:"م";s:3:"ﻥ";s:2:"ن";s:3:"ﻦ";s:2:"ن";s:3:"ﻧ";s:2:"ن";s:3:"ﻨ";s:2:"ن";s:3:"ﻩ";s:2:"ه";s:3:"ﻪ";s:2:"ه";s:3:"ﻫ";s:2:"ه";s:3:"ﻬ";s:2:"ه";s:3:"ﻭ";s:2:"و";s:3:"ﻮ";s:2:"و";s:3:"ﻯ";s:2:"ى";s:3:"ﻰ";s:2:"ى";s:3:"ﻱ";s:2:"ي";s:3:"ﻲ";s:2:"ي";s:3:"ﻳ";s:2:"ي";s:3:"ﻴ";s:2:"ي";s:3:"ﻵ";s:6:"لآ";s:3:"ﻶ";s:6:"لآ";s:3:"ﻷ";s:6:"لأ";s:3:"ﻸ";s:6:"لأ";s:3:"ﻹ";s:6:"لإ";s:3:"ﻺ";s:6:"لإ";s:3:"ﻻ";s:4:"لا";s:3:"ﻼ";s:4:"لا";s:3:"!";s:1:"!";s:3:""";s:1:""";s:3:"#";s:1:"#";s:3:"$";s:1:"$";s:3:"%";s:1:"%";s:3:"&";s:1:"&";s:3:"'";s:1:"\'";s:3:"(";s:1:"(";s:3:")";s:1:")";s:3:"*";s:1:"*";s:3:"+";s:1:"+";s:3:",";s:1:",";s:3:"-";s:1:"-";s:3:".";s:1:".";s:3:"/";s:1:"/";s:3:"0";s:1:"0";s:3:"1";s:1:"1";s:3:"2";s:1:"2";s:3:"3";s:1:"3";s:3:"4";s:1:"4";s:3:"5";s:1:"5";s:3:"6";s:1:"6";s:3:"7";s:1:"7";s:3:"8";s:1:"8";s:3:"9";s:1:"9";s:3:":";s:1:":";s:3:";";s:1:";";s:3:"<";s:1:"<";s:3:"=";s:1:"=";s:3:">";s:1:">";s:3:"?";s:1:"?";s:3:"@";s:1:"@";s:3:"A";s:1:"A";s:3:"B";s:1:"B";s:3:"C";s:1:"C";s:3:"D";s:1:"D";s:3:"E";s:1:"E";s:3:"F";s:1:"F";s:3:"G";s:1:"G";s:3:"H";s:1:"H";s:3:"I";s:1:"I";s:3:"J";s:1:"J";s:3:"K";s:1:"K";s:3:"L";s:1:"L";s:3:"M";s:1:"M";s:3:"N";s:1:"N";s:3:"O";s:1:"O";s:3:"P";s:1:"P";s:3:"Q";s:1:"Q";s:3:"R";s:1:"R";s:3:"S";s:1:"S";s:3:"T";s:1:"T";s:3:"U";s:1:"U";s:3:"V";s:1:"V";s:3:"W";s:1:"W";s:3:"X";s:1:"X";s:3:"Y";s:1:"Y";s:3:"Z";s:1:"Z";s:3:"[";s:1:"[";s:3:"\";s:1:"\\";s:3:"]";s:1:"]";s:3:"^";s:1:"^";s:3:"_";s:1:"_";s:3:"`";s:1:"`";s:3:"a";s:1:"a";s:3:"b";s:1:"b";s:3:"c";s:1:"c";s:3:"d";s:1:"d";s:3:"e";s:1:"e";s:3:"f";s:1:"f";s:3:"g";s:1:"g";s:3:"h";s:1:"h";s:3:"i";s:1:"i";s:3:"j";s:1:"j";s:3:"k";s:1:"k";s:3:"l";s:1:"l";s:3:"m";s:1:"m";s:3:"n";s:1:"n";s:3:"o";s:1:"o";s:3:"p";s:1:"p";s:3:"q";s:1:"q";s:3:"r";s:1:"r";s:3:"s";s:1:"s";s:3:"t";s:1:"t";s:3:"u";s:1:"u";s:3:"v";s:1:"v";s:3:"w";s:1:"w";s:3:"x";s:1:"x";s:3:"y";s:1:"y";s:3:"z";s:1:"z";s:3:"{";s:1:"{";s:3:"|";s:1:"|";s:3:"}";s:1:"}";s:3:"~";s:1:"~";s:3:"⦅";s:3:"⦅";s:3:"⦆";s:3:"⦆";s:3:"。";s:3:"。";s:3:"「";s:3:"「";s:3:"」";s:3:"」";s:3:"、";s:3:"、";s:3:"・";s:3:"・";s:3:"ヲ";s:3:"ヲ";s:3:"ァ";s:3:"ァ";s:3:"ィ";s:3:"ィ";s:3:"ゥ";s:3:"ゥ";s:3:"ェ";s:3:"ェ";s:3:"ォ";s:3:"ォ";s:3:"ャ";s:3:"ャ";s:3:"ュ";s:3:"ュ";s:3:"ョ";s:3:"ョ";s:3:"ッ";s:3:"ッ";s:3:"ー";s:3:"ー";s:3:"ア";s:3:"ア";s:3:"イ";s:3:"イ";s:3:"ウ";s:3:"ウ";s:3:"エ";s:3:"エ";s:3:"オ";s:3:"オ";s:3:"カ";s:3:"カ";s:3:"キ";s:3:"キ";s:3:"ク";s:3:"ク";s:3:"ケ";s:3:"ケ";s:3:"コ";s:3:"コ";s:3:"サ";s:3:"サ";s:3:"シ";s:3:"シ";s:3:"ス";s:3:"ス";s:3:"セ";s:3:"セ";s:3:"ソ";s:3:"ソ";s:3:"タ";s:3:"タ";s:3:"チ";s:3:"チ";s:3:"ツ";s:3:"ツ";s:3:"テ";s:3:"テ";s:3:"ト";s:3:"ト";s:3:"ナ";s:3:"ナ";s:3:"ニ";s:3:"ニ";s:3:"ヌ";s:3:"ヌ";s:3:"ネ";s:3:"ネ";s:3:"ノ";s:3:"ノ";s:3:"ハ";s:3:"ハ";s:3:"ヒ";s:3:"ヒ";s:3:"フ";s:3:"フ";s:3:"ヘ";s:3:"ヘ";s:3:"ホ";s:3:"ホ";s:3:"マ";s:3:"マ";s:3:"ミ";s:3:"ミ";s:3:"ム";s:3:"ム";s:3:"メ";s:3:"メ";s:3:"モ";s:3:"モ";s:3:"ヤ";s:3:"ヤ";s:3:"ユ";s:3:"ユ";s:3:"ヨ";s:3:"ヨ";s:3:"ラ";s:3:"ラ";s:3:"リ";s:3:"リ";s:3:"ル";s:3:"ル";s:3:"レ";s:3:"レ";s:3:"ロ";s:3:"ロ";s:3:"ワ";s:3:"ワ";s:3:"ン";s:3:"ン";s:3:"゙";s:3:"゙";s:3:"゚";s:3:"゚";s:3:"ᅠ";s:3:"ᅠ";s:3:"ᄀ";s:3:"ᄀ";s:3:"ᄁ";s:3:"ᄁ";s:3:"ᆪ";s:3:"ᆪ";s:3:"ᄂ";s:3:"ᄂ";s:3:"ᆬ";s:3:"ᆬ";s:3:"ᆭ";s:3:"ᆭ";s:3:"ᄃ";s:3:"ᄃ";s:3:"ᄄ";s:3:"ᄄ";s:3:"ᄅ";s:3:"ᄅ";s:3:"ᆰ";s:3:"ᆰ";s:3:"ᆱ";s:3:"ᆱ";s:3:"ᆲ";s:3:"ᆲ";s:3:"ᆳ";s:3:"ᆳ";s:3:"ᆴ";s:3:"ᆴ";s:3:"ᆵ";s:3:"ᆵ";s:3:"ᄚ";s:3:"ᄚ";s:3:"ᄆ";s:3:"ᄆ";s:3:"ᄇ";s:3:"ᄇ";s:3:"ᄈ";s:3:"ᄈ";s:3:"ᄡ";s:3:"ᄡ";s:3:"ᄉ";s:3:"ᄉ";s:3:"ᄊ";s:3:"ᄊ";s:3:"ᄋ";s:3:"ᄋ";s:3:"ᄌ";s:3:"ᄌ";s:3:"ᄍ";s:3:"ᄍ";s:3:"ᄎ";s:3:"ᄎ";s:3:"ᄏ";s:3:"ᄏ";s:3:"ᄐ";s:3:"ᄐ";s:3:"ᄑ";s:3:"ᄑ";s:3:"ᄒ";s:3:"ᄒ";s:3:"ᅡ";s:3:"ᅡ";s:3:"ᅢ";s:3:"ᅢ";s:3:"ᅣ";s:3:"ᅣ";s:3:"ᅤ";s:3:"ᅤ";s:3:"ᅥ";s:3:"ᅥ";s:3:"ᅦ";s:3:"ᅦ";s:3:"ᅧ";s:3:"ᅧ";s:3:"ᅨ";s:3:"ᅨ";s:3:"ᅩ";s:3:"ᅩ";s:3:"ᅪ";s:3:"ᅪ";s:3:"ᅫ";s:3:"ᅫ";s:3:"ᅬ";s:3:"ᅬ";s:3:"ᅭ";s:3:"ᅭ";s:3:"ᅮ";s:3:"ᅮ";s:3:"ᅯ";s:3:"ᅯ";s:3:"ᅰ";s:3:"ᅰ";s:3:"ᅱ";s:3:"ᅱ";s:3:"ᅲ";s:3:"ᅲ";s:3:"ᅳ";s:3:"ᅳ";s:3:"ᅴ";s:3:"ᅴ";s:3:"ᅵ";s:3:"ᅵ";s:3:"¢";s:2:"¢";s:3:"£";s:2:"£";s:3:"¬";s:2:"¬";s:3:" ̄";s:3:" ̄";s:3:"¦";s:2:"¦";s:3:"¥";s:2:"¥";s:3:"₩";s:3:"₩";s:3:"│";s:3:"│";s:3:"←";s:3:"←";s:3:"↑";s:3:"↑";s:3:"→";s:3:"→";s:3:"↓";s:3:"↓";s:3:"■";s:3:"■";s:3:"○";s:3:"○";s:4:"𝅗𝅥";s:8:"𝅗𝅥";s:4:"𝅘𝅥";s:8:"𝅘𝅥";s:4:"𝅘𝅥𝅮";s:12:"𝅘𝅥𝅮";s:4:"𝅘𝅥𝅯";s:12:"𝅘𝅥𝅯";s:4:"𝅘𝅥𝅰";s:12:"𝅘𝅥𝅰";s:4:"𝅘𝅥𝅱";s:12:"𝅘𝅥𝅱";s:4:"𝅘𝅥𝅲";s:12:"𝅘𝅥𝅲";s:4:"𝆹𝅥";s:8:"𝆹𝅥";s:4:"𝆺𝅥";s:8:"𝆺𝅥";s:4:"𝆹𝅥𝅮";s:12:"𝆹𝅥𝅮";s:4:"𝆺𝅥𝅮";s:12:"𝆺𝅥𝅮";s:4:"𝆹𝅥𝅯";s:12:"𝆹𝅥𝅯";s:4:"𝆺𝅥𝅯";s:12:"𝆺𝅥𝅯";s:4:"𝐀";s:1:"A";s:4:"𝐁";s:1:"B";s:4:"𝐂";s:1:"C";s:4:"𝐃";s:1:"D";s:4:"𝐄";s:1:"E";s:4:"𝐅";s:1:"F";s:4:"𝐆";s:1:"G";s:4:"𝐇";s:1:"H";s:4:"𝐈";s:1:"I";s:4:"𝐉";s:1:"J";s:4:"𝐊";s:1:"K";s:4:"𝐋";s:1:"L";s:4:"𝐌";s:1:"M";s:4:"𝐍";s:1:"N";s:4:"𝐎";s:1:"O";s:4:"𝐏";s:1:"P";s:4:"𝐐";s:1:"Q";s:4:"𝐑";s:1:"R";s:4:"𝐒";s:1:"S";s:4:"𝐓";s:1:"T";s:4:"𝐔";s:1:"U";s:4:"𝐕";s:1:"V";s:4:"𝐖";s:1:"W";s:4:"𝐗";s:1:"X";s:4:"𝐘";s:1:"Y";s:4:"𝐙";s:1:"Z";s:4:"𝐚";s:1:"a";s:4:"𝐛";s:1:"b";s:4:"𝐜";s:1:"c";s:4:"𝐝";s:1:"d";s:4:"𝐞";s:1:"e";s:4:"𝐟";s:1:"f";s:4:"𝐠";s:1:"g";s:4:"𝐡";s:1:"h";s:4:"𝐢";s:1:"i";s:4:"𝐣";s:1:"j";s:4:"𝐤";s:1:"k";s:4:"𝐥";s:1:"l";s:4:"𝐦";s:1:"m";s:4:"𝐧";s:1:"n";s:4:"𝐨";s:1:"o";s:4:"𝐩";s:1:"p";s:4:"𝐪";s:1:"q";s:4:"𝐫";s:1:"r";s:4:"𝐬";s:1:"s";s:4:"𝐭";s:1:"t";s:4:"𝐮";s:1:"u";s:4:"𝐯";s:1:"v";s:4:"𝐰";s:1:"w";s:4:"𝐱";s:1:"x";s:4:"𝐲";s:1:"y";s:4:"𝐳";s:1:"z";s:4:"𝐴";s:1:"A";s:4:"𝐵";s:1:"B";s:4:"𝐶";s:1:"C";s:4:"𝐷";s:1:"D";s:4:"𝐸";s:1:"E";s:4:"𝐹";s:1:"F";s:4:"𝐺";s:1:"G";s:4:"𝐻";s:1:"H";s:4:"𝐼";s:1:"I";s:4:"𝐽";s:1:"J";s:4:"𝐾";s:1:"K";s:4:"𝐿";s:1:"L";s:4:"𝑀";s:1:"M";s:4:"𝑁";s:1:"N";s:4:"𝑂";s:1:"O";s:4:"𝑃";s:1:"P";s:4:"𝑄";s:1:"Q";s:4:"𝑅";s:1:"R";s:4:"𝑆";s:1:"S";s:4:"𝑇";s:1:"T";s:4:"𝑈";s:1:"U";s:4:"𝑉";s:1:"V";s:4:"𝑊";s:1:"W";s:4:"𝑋";s:1:"X";s:4:"𝑌";s:1:"Y";s:4:"𝑍";s:1:"Z";s:4:"𝑎";s:1:"a";s:4:"𝑏";s:1:"b";s:4:"𝑐";s:1:"c";s:4:"𝑑";s:1:"d";s:4:"𝑒";s:1:"e";s:4:"𝑓";s:1:"f";s:4:"𝑔";s:1:"g";s:4:"𝑖";s:1:"i";s:4:"𝑗";s:1:"j";s:4:"𝑘";s:1:"k";s:4:"𝑙";s:1:"l";s:4:"𝑚";s:1:"m";s:4:"𝑛";s:1:"n";s:4:"𝑜";s:1:"o";s:4:"𝑝";s:1:"p";s:4:"𝑞";s:1:"q";s:4:"𝑟";s:1:"r";s:4:"𝑠";s:1:"s";s:4:"𝑡";s:1:"t";s:4:"𝑢";s:1:"u";s:4:"𝑣";s:1:"v";s:4:"𝑤";s:1:"w";s:4:"𝑥";s:1:"x";s:4:"𝑦";s:1:"y";s:4:"𝑧";s:1:"z";s:4:"𝑨";s:1:"A";s:4:"𝑩";s:1:"B";s:4:"𝑪";s:1:"C";s:4:"𝑫";s:1:"D";s:4:"𝑬";s:1:"E";s:4:"𝑭";s:1:"F";s:4:"𝑮";s:1:"G";s:4:"𝑯";s:1:"H";s:4:"𝑰";s:1:"I";s:4:"𝑱";s:1:"J";s:4:"𝑲";s:1:"K";s:4:"𝑳";s:1:"L";s:4:"𝑴";s:1:"M";s:4:"𝑵";s:1:"N";s:4:"𝑶";s:1:"O";s:4:"𝑷";s:1:"P";s:4:"𝑸";s:1:"Q";s:4:"𝑹";s:1:"R";s:4:"𝑺";s:1:"S";s:4:"𝑻";s:1:"T";s:4:"𝑼";s:1:"U";s:4:"𝑽";s:1:"V";s:4:"𝑾";s:1:"W";s:4:"𝑿";s:1:"X";s:4:"𝒀";s:1:"Y";s:4:"𝒁";s:1:"Z";s:4:"𝒂";s:1:"a";s:4:"𝒃";s:1:"b";s:4:"𝒄";s:1:"c";s:4:"𝒅";s:1:"d";s:4:"𝒆";s:1:"e";s:4:"𝒇";s:1:"f";s:4:"𝒈";s:1:"g";s:4:"𝒉";s:1:"h";s:4:"𝒊";s:1:"i";s:4:"𝒋";s:1:"j";s:4:"𝒌";s:1:"k";s:4:"𝒍";s:1:"l";s:4:"𝒎";s:1:"m";s:4:"𝒏";s:1:"n";s:4:"𝒐";s:1:"o";s:4:"𝒑";s:1:"p";s:4:"𝒒";s:1:"q";s:4:"𝒓";s:1:"r";s:4:"𝒔";s:1:"s";s:4:"𝒕";s:1:"t";s:4:"𝒖";s:1:"u";s:4:"𝒗";s:1:"v";s:4:"𝒘";s:1:"w";s:4:"𝒙";s:1:"x";s:4:"𝒚";s:1:"y";s:4:"𝒛";s:1:"z";s:4:"𝒜";s:1:"A";s:4:"𝒞";s:1:"C";s:4:"𝒟";s:1:"D";s:4:"𝒢";s:1:"G";s:4:"𝒥";s:1:"J";s:4:"𝒦";s:1:"K";s:4:"𝒩";s:1:"N";s:4:"𝒪";s:1:"O";s:4:"𝒫";s:1:"P";s:4:"𝒬";s:1:"Q";s:4:"𝒮";s:1:"S";s:4:"𝒯";s:1:"T";s:4:"𝒰";s:1:"U";s:4:"𝒱";s:1:"V";s:4:"𝒲";s:1:"W";s:4:"𝒳";s:1:"X";s:4:"𝒴";s:1:"Y";s:4:"𝒵";s:1:"Z";s:4:"𝒶";s:1:"a";s:4:"𝒷";s:1:"b";s:4:"𝒸";s:1:"c";s:4:"𝒹";s:1:"d";s:4:"𝒻";s:1:"f";s:4:"𝒽";s:1:"h";s:4:"𝒾";s:1:"i";s:4:"𝒿";s:1:"j";s:4:"𝓀";s:1:"k";s:4:"𝓁";s:1:"l";s:4:"𝓂";s:1:"m";s:4:"𝓃";s:1:"n";s:4:"𝓅";s:1:"p";s:4:"𝓆";s:1:"q";s:4:"𝓇";s:1:"r";s:4:"𝓈";s:1:"s";s:4:"𝓉";s:1:"t";s:4:"𝓊";s:1:"u";s:4:"𝓋";s:1:"v";s:4:"𝓌";s:1:"w";s:4:"𝓍";s:1:"x";s:4:"𝓎";s:1:"y";s:4:"𝓏";s:1:"z";s:4:"𝓐";s:1:"A";s:4:"𝓑";s:1:"B";s:4:"𝓒";s:1:"C";s:4:"𝓓";s:1:"D";s:4:"𝓔";s:1:"E";s:4:"𝓕";s:1:"F";s:4:"𝓖";s:1:"G";s:4:"𝓗";s:1:"H";s:4:"𝓘";s:1:"I";s:4:"𝓙";s:1:"J";s:4:"𝓚";s:1:"K";s:4:"𝓛";s:1:"L";s:4:"𝓜";s:1:"M";s:4:"𝓝";s:1:"N";s:4:"𝓞";s:1:"O";s:4:"𝓟";s:1:"P";s:4:"𝓠";s:1:"Q";s:4:"𝓡";s:1:"R";s:4:"𝓢";s:1:"S";s:4:"𝓣";s:1:"T";s:4:"𝓤";s:1:"U";s:4:"𝓥";s:1:"V";s:4:"𝓦";s:1:"W";s:4:"𝓧";s:1:"X";s:4:"𝓨";s:1:"Y";s:4:"𝓩";s:1:"Z";s:4:"𝓪";s:1:"a";s:4:"𝓫";s:1:"b";s:4:"𝓬";s:1:"c";s:4:"𝓭";s:1:"d";s:4:"𝓮";s:1:"e";s:4:"𝓯";s:1:"f";s:4:"𝓰";s:1:"g";s:4:"𝓱";s:1:"h";s:4:"𝓲";s:1:"i";s:4:"𝓳";s:1:"j";s:4:"𝓴";s:1:"k";s:4:"𝓵";s:1:"l";s:4:"𝓶";s:1:"m";s:4:"𝓷";s:1:"n";s:4:"𝓸";s:1:"o";s:4:"𝓹";s:1:"p";s:4:"𝓺";s:1:"q";s:4:"𝓻";s:1:"r";s:4:"𝓼";s:1:"s";s:4:"𝓽";s:1:"t";s:4:"𝓾";s:1:"u";s:4:"𝓿";s:1:"v";s:4:"𝔀";s:1:"w";s:4:"𝔁";s:1:"x";s:4:"𝔂";s:1:"y";s:4:"𝔃";s:1:"z";s:4:"𝔄";s:1:"A";s:4:"𝔅";s:1:"B";s:4:"𝔇";s:1:"D";s:4:"𝔈";s:1:"E";s:4:"𝔉";s:1:"F";s:4:"𝔊";s:1:"G";s:4:"𝔍";s:1:"J";s:4:"𝔎";s:1:"K";s:4:"𝔏";s:1:"L";s:4:"𝔐";s:1:"M";s:4:"𝔑";s:1:"N";s:4:"𝔒";s:1:"O";s:4:"𝔓";s:1:"P";s:4:"𝔔";s:1:"Q";s:4:"𝔖";s:1:"S";s:4:"𝔗";s:1:"T";s:4:"𝔘";s:1:"U";s:4:"𝔙";s:1:"V";s:4:"𝔚";s:1:"W";s:4:"𝔛";s:1:"X";s:4:"𝔜";s:1:"Y";s:4:"𝔞";s:1:"a";s:4:"𝔟";s:1:"b";s:4:"𝔠";s:1:"c";s:4:"𝔡";s:1:"d";s:4:"𝔢";s:1:"e";s:4:"𝔣";s:1:"f";s:4:"𝔤";s:1:"g";s:4:"𝔥";s:1:"h";s:4:"𝔦";s:1:"i";s:4:"𝔧";s:1:"j";s:4:"𝔨";s:1:"k";s:4:"𝔩";s:1:"l";s:4:"𝔪";s:1:"m";s:4:"𝔫";s:1:"n";s:4:"𝔬";s:1:"o";s:4:"𝔭";s:1:"p";s:4:"𝔮";s:1:"q";s:4:"𝔯";s:1:"r";s:4:"𝔰";s:1:"s";s:4:"𝔱";s:1:"t";s:4:"𝔲";s:1:"u";s:4:"𝔳";s:1:"v";s:4:"𝔴";s:1:"w";s:4:"𝔵";s:1:"x";s:4:"𝔶";s:1:"y";s:4:"𝔷";s:1:"z";s:4:"𝔸";s:1:"A";s:4:"𝔹";s:1:"B";s:4:"𝔻";s:1:"D";s:4:"𝔼";s:1:"E";s:4:"𝔽";s:1:"F";s:4:"𝔾";s:1:"G";s:4:"𝕀";s:1:"I";s:4:"𝕁";s:1:"J";s:4:"𝕂";s:1:"K";s:4:"𝕃";s:1:"L";s:4:"𝕄";s:1:"M";s:4:"𝕆";s:1:"O";s:4:"𝕊";s:1:"S";s:4:"𝕋";s:1:"T";s:4:"𝕌";s:1:"U";s:4:"𝕍";s:1:"V";s:4:"𝕎";s:1:"W";s:4:"𝕏";s:1:"X";s:4:"𝕐";s:1:"Y";s:4:"𝕒";s:1:"a";s:4:"𝕓";s:1:"b";s:4:"𝕔";s:1:"c";s:4:"𝕕";s:1:"d";s:4:"𝕖";s:1:"e";s:4:"𝕗";s:1:"f";s:4:"𝕘";s:1:"g";s:4:"𝕙";s:1:"h";s:4:"𝕚";s:1:"i";s:4:"𝕛";s:1:"j";s:4:"𝕜";s:1:"k";s:4:"𝕝";s:1:"l";s:4:"𝕞";s:1:"m";s:4:"𝕟";s:1:"n";s:4:"𝕠";s:1:"o";s:4:"𝕡";s:1:"p";s:4:"𝕢";s:1:"q";s:4:"𝕣";s:1:"r";s:4:"𝕤";s:1:"s";s:4:"𝕥";s:1:"t";s:4:"𝕦";s:1:"u";s:4:"𝕧";s:1:"v";s:4:"𝕨";s:1:"w";s:4:"𝕩";s:1:"x";s:4:"𝕪";s:1:"y";s:4:"𝕫";s:1:"z";s:4:"𝕬";s:1:"A";s:4:"𝕭";s:1:"B";s:4:"𝕮";s:1:"C";s:4:"𝕯";s:1:"D";s:4:"𝕰";s:1:"E";s:4:"𝕱";s:1:"F";s:4:"𝕲";s:1:"G";s:4:"𝕳";s:1:"H";s:4:"𝕴";s:1:"I";s:4:"𝕵";s:1:"J";s:4:"𝕶";s:1:"K";s:4:"𝕷";s:1:"L";s:4:"𝕸";s:1:"M";s:4:"𝕹";s:1:"N";s:4:"𝕺";s:1:"O";s:4:"𝕻";s:1:"P";s:4:"𝕼";s:1:"Q";s:4:"𝕽";s:1:"R";s:4:"𝕾";s:1:"S";s:4:"𝕿";s:1:"T";s:4:"𝖀";s:1:"U";s:4:"𝖁";s:1:"V";s:4:"𝖂";s:1:"W";s:4:"𝖃";s:1:"X";s:4:"𝖄";s:1:"Y";s:4:"𝖅";s:1:"Z";s:4:"𝖆";s:1:"a";s:4:"𝖇";s:1:"b";s:4:"𝖈";s:1:"c";s:4:"𝖉";s:1:"d";s:4:"𝖊";s:1:"e";s:4:"𝖋";s:1:"f";s:4:"𝖌";s:1:"g";s:4:"𝖍";s:1:"h";s:4:"𝖎";s:1:"i";s:4:"𝖏";s:1:"j";s:4:"𝖐";s:1:"k";s:4:"𝖑";s:1:"l";s:4:"𝖒";s:1:"m";s:4:"𝖓";s:1:"n";s:4:"𝖔";s:1:"o";s:4:"𝖕";s:1:"p";s:4:"𝖖";s:1:"q";s:4:"𝖗";s:1:"r";s:4:"𝖘";s:1:"s";s:4:"𝖙";s:1:"t";s:4:"𝖚";s:1:"u";s:4:"𝖛";s:1:"v";s:4:"𝖜";s:1:"w";s:4:"𝖝";s:1:"x";s:4:"𝖞";s:1:"y";s:4:"𝖟";s:1:"z";s:4:"𝖠";s:1:"A";s:4:"𝖡";s:1:"B";s:4:"𝖢";s:1:"C";s:4:"𝖣";s:1:"D";s:4:"𝖤";s:1:"E";s:4:"𝖥";s:1:"F";s:4:"𝖦";s:1:"G";s:4:"𝖧";s:1:"H";s:4:"𝖨";s:1:"I";s:4:"𝖩";s:1:"J";s:4:"𝖪";s:1:"K";s:4:"𝖫";s:1:"L";s:4:"𝖬";s:1:"M";s:4:"𝖭";s:1:"N";s:4:"𝖮";s:1:"O";s:4:"𝖯";s:1:"P";s:4:"𝖰";s:1:"Q";s:4:"𝖱";s:1:"R";s:4:"𝖲";s:1:"S";s:4:"𝖳";s:1:"T";s:4:"𝖴";s:1:"U";s:4:"𝖵";s:1:"V";s:4:"𝖶";s:1:"W";s:4:"𝖷";s:1:"X";s:4:"𝖸";s:1:"Y";s:4:"𝖹";s:1:"Z";s:4:"𝖺";s:1:"a";s:4:"𝖻";s:1:"b";s:4:"𝖼";s:1:"c";s:4:"𝖽";s:1:"d";s:4:"𝖾";s:1:"e";s:4:"𝖿";s:1:"f";s:4:"𝗀";s:1:"g";s:4:"𝗁";s:1:"h";s:4:"𝗂";s:1:"i";s:4:"𝗃";s:1:"j";s:4:"𝗄";s:1:"k";s:4:"𝗅";s:1:"l";s:4:"𝗆";s:1:"m";s:4:"𝗇";s:1:"n";s:4:"𝗈";s:1:"o";s:4:"𝗉";s:1:"p";s:4:"𝗊";s:1:"q";s:4:"𝗋";s:1:"r";s:4:"𝗌";s:1:"s";s:4:"𝗍";s:1:"t";s:4:"𝗎";s:1:"u";s:4:"𝗏";s:1:"v";s:4:"𝗐";s:1:"w";s:4:"𝗑";s:1:"x";s:4:"𝗒";s:1:"y";s:4:"𝗓";s:1:"z";s:4:"𝗔";s:1:"A";s:4:"𝗕";s:1:"B";s:4:"𝗖";s:1:"C";s:4:"𝗗";s:1:"D";s:4:"𝗘";s:1:"E";s:4:"𝗙";s:1:"F";s:4:"𝗚";s:1:"G";s:4:"𝗛";s:1:"H";s:4:"𝗜";s:1:"I";s:4:"𝗝";s:1:"J";s:4:"𝗞";s:1:"K";s:4:"𝗟";s:1:"L";s:4:"𝗠";s:1:"M";s:4:"𝗡";s:1:"N";s:4:"𝗢";s:1:"O";s:4:"𝗣";s:1:"P";s:4:"𝗤";s:1:"Q";s:4:"𝗥";s:1:"R";s:4:"𝗦";s:1:"S";s:4:"𝗧";s:1:"T";s:4:"𝗨";s:1:"U";s:4:"𝗩";s:1:"V";s:4:"𝗪";s:1:"W";s:4:"𝗫";s:1:"X";s:4:"𝗬";s:1:"Y";s:4:"𝗭";s:1:"Z";s:4:"𝗮";s:1:"a";s:4:"𝗯";s:1:"b";s:4:"𝗰";s:1:"c";s:4:"𝗱";s:1:"d";s:4:"𝗲";s:1:"e";s:4:"𝗳";s:1:"f";s:4:"𝗴";s:1:"g";s:4:"𝗵";s:1:"h";s:4:"𝗶";s:1:"i";s:4:"𝗷";s:1:"j";s:4:"𝗸";s:1:"k";s:4:"𝗹";s:1:"l";s:4:"𝗺";s:1:"m";s:4:"𝗻";s:1:"n";s:4:"𝗼";s:1:"o";s:4:"𝗽";s:1:"p";s:4:"𝗾";s:1:"q";s:4:"𝗿";s:1:"r";s:4:"𝘀";s:1:"s";s:4:"𝘁";s:1:"t";s:4:"𝘂";s:1:"u";s:4:"𝘃";s:1:"v";s:4:"𝘄";s:1:"w";s:4:"𝘅";s:1:"x";s:4:"𝘆";s:1:"y";s:4:"𝘇";s:1:"z";s:4:"𝘈";s:1:"A";s:4:"𝘉";s:1:"B";s:4:"𝘊";s:1:"C";s:4:"𝘋";s:1:"D";s:4:"𝘌";s:1:"E";s:4:"𝘍";s:1:"F";s:4:"𝘎";s:1:"G";s:4:"𝘏";s:1:"H";s:4:"𝘐";s:1:"I";s:4:"𝘑";s:1:"J";s:4:"𝘒";s:1:"K";s:4:"𝘓";s:1:"L";s:4:"𝘔";s:1:"M";s:4:"𝘕";s:1:"N";s:4:"𝘖";s:1:"O";s:4:"𝘗";s:1:"P";s:4:"𝘘";s:1:"Q";s:4:"𝘙";s:1:"R";s:4:"𝘚";s:1:"S";s:4:"𝘛";s:1:"T";s:4:"𝘜";s:1:"U";s:4:"𝘝";s:1:"V";s:4:"𝘞";s:1:"W";s:4:"𝘟";s:1:"X";s:4:"𝘠";s:1:"Y";s:4:"𝘡";s:1:"Z";s:4:"𝘢";s:1:"a";s:4:"𝘣";s:1:"b";s:4:"𝘤";s:1:"c";s:4:"𝘥";s:1:"d";s:4:"𝘦";s:1:"e";s:4:"𝘧";s:1:"f";s:4:"𝘨";s:1:"g";s:4:"𝘩";s:1:"h";s:4:"𝘪";s:1:"i";s:4:"𝘫";s:1:"j";s:4:"𝘬";s:1:"k";s:4:"𝘭";s:1:"l";s:4:"𝘮";s:1:"m";s:4:"𝘯";s:1:"n";s:4:"𝘰";s:1:"o";s:4:"𝘱";s:1:"p";s:4:"𝘲";s:1:"q";s:4:"𝘳";s:1:"r";s:4:"𝘴";s:1:"s";s:4:"𝘵";s:1:"t";s:4:"𝘶";s:1:"u";s:4:"𝘷";s:1:"v";s:4:"𝘸";s:1:"w";s:4:"𝘹";s:1:"x";s:4:"𝘺";s:1:"y";s:4:"𝘻";s:1:"z";s:4:"𝘼";s:1:"A";s:4:"𝘽";s:1:"B";s:4:"𝘾";s:1:"C";s:4:"𝘿";s:1:"D";s:4:"𝙀";s:1:"E";s:4:"𝙁";s:1:"F";s:4:"𝙂";s:1:"G";s:4:"𝙃";s:1:"H";s:4:"𝙄";s:1:"I";s:4:"𝙅";s:1:"J";s:4:"𝙆";s:1:"K";s:4:"𝙇";s:1:"L";s:4:"𝙈";s:1:"M";s:4:"𝙉";s:1:"N";s:4:"𝙊";s:1:"O";s:4:"𝙋";s:1:"P";s:4:"𝙌";s:1:"Q";s:4:"𝙍";s:1:"R";s:4:"𝙎";s:1:"S";s:4:"𝙏";s:1:"T";s:4:"𝙐";s:1:"U";s:4:"𝙑";s:1:"V";s:4:"𝙒";s:1:"W";s:4:"𝙓";s:1:"X";s:4:"𝙔";s:1:"Y";s:4:"𝙕";s:1:"Z";s:4:"𝙖";s:1:"a";s:4:"𝙗";s:1:"b";s:4:"𝙘";s:1:"c";s:4:"𝙙";s:1:"d";s:4:"𝙚";s:1:"e";s:4:"𝙛";s:1:"f";s:4:"𝙜";s:1:"g";s:4:"𝙝";s:1:"h";s:4:"𝙞";s:1:"i";s:4:"𝙟";s:1:"j";s:4:"𝙠";s:1:"k";s:4:"𝙡";s:1:"l";s:4:"𝙢";s:1:"m";s:4:"𝙣";s:1:"n";s:4:"𝙤";s:1:"o";s:4:"𝙥";s:1:"p";s:4:"𝙦";s:1:"q";s:4:"𝙧";s:1:"r";s:4:"𝙨";s:1:"s";s:4:"𝙩";s:1:"t";s:4:"𝙪";s:1:"u";s:4:"𝙫";s:1:"v";s:4:"𝙬";s:1:"w";s:4:"𝙭";s:1:"x";s:4:"𝙮";s:1:"y";s:4:"𝙯";s:1:"z";s:4:"𝙰";s:1:"A";s:4:"𝙱";s:1:"B";s:4:"𝙲";s:1:"C";s:4:"𝙳";s:1:"D";s:4:"𝙴";s:1:"E";s:4:"𝙵";s:1:"F";s:4:"𝙶";s:1:"G";s:4:"𝙷";s:1:"H";s:4:"𝙸";s:1:"I";s:4:"𝙹";s:1:"J";s:4:"𝙺";s:1:"K";s:4:"𝙻";s:1:"L";s:4:"𝙼";s:1:"M";s:4:"𝙽";s:1:"N";s:4:"𝙾";s:1:"O";s:4:"𝙿";s:1:"P";s:4:"𝚀";s:1:"Q";s:4:"𝚁";s:1:"R";s:4:"𝚂";s:1:"S";s:4:"𝚃";s:1:"T";s:4:"𝚄";s:1:"U";s:4:"𝚅";s:1:"V";s:4:"𝚆";s:1:"W";s:4:"𝚇";s:1:"X";s:4:"𝚈";s:1:"Y";s:4:"𝚉";s:1:"Z";s:4:"𝚊";s:1:"a";s:4:"𝚋";s:1:"b";s:4:"𝚌";s:1:"c";s:4:"𝚍";s:1:"d";s:4:"𝚎";s:1:"e";s:4:"𝚏";s:1:"f";s:4:"𝚐";s:1:"g";s:4:"𝚑";s:1:"h";s:4:"𝚒";s:1:"i";s:4:"𝚓";s:1:"j";s:4:"𝚔";s:1:"k";s:4:"𝚕";s:1:"l";s:4:"𝚖";s:1:"m";s:4:"𝚗";s:1:"n";s:4:"𝚘";s:1:"o";s:4:"𝚙";s:1:"p";s:4:"𝚚";s:1:"q";s:4:"𝚛";s:1:"r";s:4:"𝚜";s:1:"s";s:4:"𝚝";s:1:"t";s:4:"𝚞";s:1:"u";s:4:"𝚟";s:1:"v";s:4:"𝚠";s:1:"w";s:4:"𝚡";s:1:"x";s:4:"𝚢";s:1:"y";s:4:"𝚣";s:1:"z";s:4:"𝚤";s:2:"ı";s:4:"𝚥";s:2:"ȷ";s:4:"𝚨";s:2:"Α";s:4:"𝚩";s:2:"Β";s:4:"𝚪";s:2:"Γ";s:4:"𝚫";s:2:"Δ";s:4:"𝚬";s:2:"Ε";s:4:"𝚭";s:2:"Ζ";s:4:"𝚮";s:2:"Η";s:4:"𝚯";s:2:"Θ";s:4:"𝚰";s:2:"Ι";s:4:"𝚱";s:2:"Κ";s:4:"𝚲";s:2:"Λ";s:4:"𝚳";s:2:"Μ";s:4:"𝚴";s:2:"Ν";s:4:"𝚵";s:2:"Ξ";s:4:"𝚶";s:2:"Ο";s:4:"𝚷";s:2:"Π";s:4:"𝚸";s:2:"Ρ";s:4:"𝚹";s:2:"Θ";s:4:"𝚺";s:2:"Σ";s:4:"𝚻";s:2:"Τ";s:4:"𝚼";s:2:"Υ";s:4:"𝚽";s:2:"Φ";s:4:"𝚾";s:2:"Χ";s:4:"𝚿";s:2:"Ψ";s:4:"𝛀";s:2:"Ω";s:4:"𝛁";s:3:"∇";s:4:"𝛂";s:2:"α";s:4:"𝛃";s:2:"β";s:4:"𝛄";s:2:"γ";s:4:"𝛅";s:2:"δ";s:4:"𝛆";s:2:"ε";s:4:"𝛇";s:2:"ζ";s:4:"𝛈";s:2:"η";s:4:"𝛉";s:2:"θ";s:4:"𝛊";s:2:"ι";s:4:"𝛋";s:2:"κ";s:4:"𝛌";s:2:"λ";s:4:"𝛍";s:2:"μ";s:4:"𝛎";s:2:"ν";s:4:"𝛏";s:2:"ξ";s:4:"𝛐";s:2:"ο";s:4:"𝛑";s:2:"π";s:4:"𝛒";s:2:"ρ";s:4:"𝛓";s:2:"ς";s:4:"𝛔";s:2:"σ";s:4:"𝛕";s:2:"τ";s:4:"𝛖";s:2:"υ";s:4:"𝛗";s:2:"φ";s:4:"𝛘";s:2:"χ";s:4:"𝛙";s:2:"ψ";s:4:"𝛚";s:2:"ω";s:4:"𝛛";s:3:"∂";s:4:"𝛜";s:2:"ε";s:4:"𝛝";s:2:"θ";s:4:"𝛞";s:2:"κ";s:4:"𝛟";s:2:"φ";s:4:"𝛠";s:2:"ρ";s:4:"𝛡";s:2:"π";s:4:"𝛢";s:2:"Α";s:4:"𝛣";s:2:"Β";s:4:"𝛤";s:2:"Γ";s:4:"𝛥";s:2:"Δ";s:4:"𝛦";s:2:"Ε";s:4:"𝛧";s:2:"Ζ";s:4:"𝛨";s:2:"Η";s:4:"𝛩";s:2:"Θ";s:4:"𝛪";s:2:"Ι";s:4:"𝛫";s:2:"Κ";s:4:"𝛬";s:2:"Λ";s:4:"𝛭";s:2:"Μ";s:4:"𝛮";s:2:"Ν";s:4:"𝛯";s:2:"Ξ";s:4:"𝛰";s:2:"Ο";s:4:"𝛱";s:2:"Π";s:4:"𝛲";s:2:"Ρ";s:4:"𝛳";s:2:"Θ";s:4:"𝛴";s:2:"Σ";s:4:"𝛵";s:2:"Τ";s:4:"𝛶";s:2:"Υ";s:4:"𝛷";s:2:"Φ";s:4:"𝛸";s:2:"Χ";s:4:"𝛹";s:2:"Ψ";s:4:"𝛺";s:2:"Ω";s:4:"𝛻";s:3:"∇";s:4:"𝛼";s:2:"α";s:4:"𝛽";s:2:"β";s:4:"𝛾";s:2:"γ";s:4:"𝛿";s:2:"δ";s:4:"𝜀";s:2:"ε";s:4:"𝜁";s:2:"ζ";s:4:"𝜂";s:2:"η";s:4:"𝜃";s:2:"θ";s:4:"𝜄";s:2:"ι";s:4:"𝜅";s:2:"κ";s:4:"𝜆";s:2:"λ";s:4:"𝜇";s:2:"μ";s:4:"𝜈";s:2:"ν";s:4:"𝜉";s:2:"ξ";s:4:"𝜊";s:2:"ο";s:4:"𝜋";s:2:"π";s:4:"𝜌";s:2:"ρ";s:4:"𝜍";s:2:"ς";s:4:"𝜎";s:2:"σ";s:4:"𝜏";s:2:"τ";s:4:"𝜐";s:2:"υ";s:4:"𝜑";s:2:"φ";s:4:"𝜒";s:2:"χ";s:4:"𝜓";s:2:"ψ";s:4:"𝜔";s:2:"ω";s:4:"𝜕";s:3:"∂";s:4:"𝜖";s:2:"ε";s:4:"𝜗";s:2:"θ";s:4:"𝜘";s:2:"κ";s:4:"𝜙";s:2:"φ";s:4:"𝜚";s:2:"ρ";s:4:"𝜛";s:2:"π";s:4:"𝜜";s:2:"Α";s:4:"𝜝";s:2:"Β";s:4:"𝜞";s:2:"Γ";s:4:"𝜟";s:2:"Δ";s:4:"𝜠";s:2:"Ε";s:4:"𝜡";s:2:"Ζ";s:4:"𝜢";s:2:"Η";s:4:"𝜣";s:2:"Θ";s:4:"𝜤";s:2:"Ι";s:4:"𝜥";s:2:"Κ";s:4:"𝜦";s:2:"Λ";s:4:"𝜧";s:2:"Μ";s:4:"𝜨";s:2:"Ν";s:4:"𝜩";s:2:"Ξ";s:4:"𝜪";s:2:"Ο";s:4:"𝜫";s:2:"Π";s:4:"𝜬";s:2:"Ρ";s:4:"𝜭";s:2:"Θ";s:4:"𝜮";s:2:"Σ";s:4:"𝜯";s:2:"Τ";s:4:"𝜰";s:2:"Υ";s:4:"𝜱";s:2:"Φ";s:4:"𝜲";s:2:"Χ";s:4:"𝜳";s:2:"Ψ";s:4:"𝜴";s:2:"Ω";s:4:"𝜵";s:3:"∇";s:4:"𝜶";s:2:"α";s:4:"𝜷";s:2:"β";s:4:"𝜸";s:2:"γ";s:4:"𝜹";s:2:"δ";s:4:"𝜺";s:2:"ε";s:4:"𝜻";s:2:"ζ";s:4:"𝜼";s:2:"η";s:4:"𝜽";s:2:"θ";s:4:"𝜾";s:2:"ι";s:4:"𝜿";s:2:"κ";s:4:"𝝀";s:2:"λ";s:4:"𝝁";s:2:"μ";s:4:"𝝂";s:2:"ν";s:4:"𝝃";s:2:"ξ";s:4:"𝝄";s:2:"ο";s:4:"𝝅";s:2:"π";s:4:"𝝆";s:2:"ρ";s:4:"𝝇";s:2:"ς";s:4:"𝝈";s:2:"σ";s:4:"𝝉";s:2:"τ";s:4:"𝝊";s:2:"υ";s:4:"𝝋";s:2:"φ";s:4:"𝝌";s:2:"χ";s:4:"𝝍";s:2:"ψ";s:4:"𝝎";s:2:"ω";s:4:"𝝏";s:3:"∂";s:4:"𝝐";s:2:"ε";s:4:"𝝑";s:2:"θ";s:4:"𝝒";s:2:"κ";s:4:"𝝓";s:2:"φ";s:4:"𝝔";s:2:"ρ";s:4:"𝝕";s:2:"π";s:4:"𝝖";s:2:"Α";s:4:"𝝗";s:2:"Β";s:4:"𝝘";s:2:"Γ";s:4:"𝝙";s:2:"Δ";s:4:"𝝚";s:2:"Ε";s:4:"𝝛";s:2:"Ζ";s:4:"𝝜";s:2:"Η";s:4:"𝝝";s:2:"Θ";s:4:"𝝞";s:2:"Ι";s:4:"𝝟";s:2:"Κ";s:4:"𝝠";s:2:"Λ";s:4:"𝝡";s:2:"Μ";s:4:"𝝢";s:2:"Ν";s:4:"𝝣";s:2:"Ξ";s:4:"𝝤";s:2:"Ο";s:4:"𝝥";s:2:"Π";s:4:"𝝦";s:2:"Ρ";s:4:"𝝧";s:2:"Θ";s:4:"𝝨";s:2:"Σ";s:4:"𝝩";s:2:"Τ";s:4:"𝝪";s:2:"Υ";s:4:"𝝫";s:2:"Φ";s:4:"𝝬";s:2:"Χ";s:4:"𝝭";s:2:"Ψ";s:4:"𝝮";s:2:"Ω";s:4:"𝝯";s:3:"∇";s:4:"𝝰";s:2:"α";s:4:"𝝱";s:2:"β";s:4:"𝝲";s:2:"γ";s:4:"𝝳";s:2:"δ";s:4:"𝝴";s:2:"ε";s:4:"𝝵";s:2:"ζ";s:4:"𝝶";s:2:"η";s:4:"𝝷";s:2:"θ";s:4:"𝝸";s:2:"ι";s:4:"𝝹";s:2:"κ";s:4:"𝝺";s:2:"λ";s:4:"𝝻";s:2:"μ";s:4:"𝝼";s:2:"ν";s:4:"𝝽";s:2:"ξ";s:4:"𝝾";s:2:"ο";s:4:"𝝿";s:2:"π";s:4:"𝞀";s:2:"ρ";s:4:"𝞁";s:2:"ς";s:4:"𝞂";s:2:"σ";s:4:"𝞃";s:2:"τ";s:4:"𝞄";s:2:"υ";s:4:"𝞅";s:2:"φ";s:4:"𝞆";s:2:"χ";s:4:"𝞇";s:2:"ψ";s:4:"𝞈";s:2:"ω";s:4:"𝞉";s:3:"∂";s:4:"𝞊";s:2:"ε";s:4:"𝞋";s:2:"θ";s:4:"𝞌";s:2:"κ";s:4:"𝞍";s:2:"φ";s:4:"𝞎";s:2:"ρ";s:4:"𝞏";s:2:"π";s:4:"𝞐";s:2:"Α";s:4:"𝞑";s:2:"Β";s:4:"𝞒";s:2:"Γ";s:4:"𝞓";s:2:"Δ";s:4:"𝞔";s:2:"Ε";s:4:"𝞕";s:2:"Ζ";s:4:"𝞖";s:2:"Η";s:4:"𝞗";s:2:"Θ";s:4:"𝞘";s:2:"Ι";s:4:"𝞙";s:2:"Κ";s:4:"𝞚";s:2:"Λ";s:4:"𝞛";s:2:"Μ";s:4:"𝞜";s:2:"Ν";s:4:"𝞝";s:2:"Ξ";s:4:"𝞞";s:2:"Ο";s:4:"𝞟";s:2:"Π";s:4:"𝞠";s:2:"Ρ";s:4:"𝞡";s:2:"Θ";s:4:"𝞢";s:2:"Σ";s:4:"𝞣";s:2:"Τ";s:4:"𝞤";s:2:"Υ";s:4:"𝞥";s:2:"Φ";s:4:"𝞦";s:2:"Χ";s:4:"𝞧";s:2:"Ψ";s:4:"𝞨";s:2:"Ω";s:4:"𝞩";s:3:"∇";s:4:"𝞪";s:2:"α";s:4:"𝞫";s:2:"β";s:4:"𝞬";s:2:"γ";s:4:"𝞭";s:2:"δ";s:4:"𝞮";s:2:"ε";s:4:"𝞯";s:2:"ζ";s:4:"𝞰";s:2:"η";s:4:"𝞱";s:2:"θ";s:4:"𝞲";s:2:"ι";s:4:"𝞳";s:2:"κ";s:4:"𝞴";s:2:"λ";s:4:"𝞵";s:2:"μ";s:4:"𝞶";s:2:"ν";s:4:"𝞷";s:2:"ξ";s:4:"𝞸";s:2:"ο";s:4:"𝞹";s:2:"π";s:4:"𝞺";s:2:"ρ";s:4:"𝞻";s:2:"ς";s:4:"𝞼";s:2:"σ";s:4:"𝞽";s:2:"τ";s:4:"𝞾";s:2:"υ";s:4:"𝞿";s:2:"φ";s:4:"𝟀";s:2:"χ";s:4:"𝟁";s:2:"ψ";s:4:"𝟂";s:2:"ω";s:4:"𝟃";s:3:"∂";s:4:"𝟄";s:2:"ε";s:4:"𝟅";s:2:"θ";s:4:"𝟆";s:2:"κ";s:4:"𝟇";s:2:"φ";s:4:"𝟈";s:2:"ρ";s:4:"𝟉";s:2:"π";s:4:"𝟎";s:1:"0";s:4:"𝟏";s:1:"1";s:4:"𝟐";s:1:"2";s:4:"𝟑";s:1:"3";s:4:"𝟒";s:1:"4";s:4:"𝟓";s:1:"5";s:4:"𝟔";s:1:"6";s:4:"𝟕";s:1:"7";s:4:"𝟖";s:1:"8";s:4:"𝟗";s:1:"9";s:4:"𝟘";s:1:"0";s:4:"𝟙";s:1:"1";s:4:"𝟚";s:1:"2";s:4:"𝟛";s:1:"3";s:4:"𝟜";s:1:"4";s:4:"𝟝";s:1:"5";s:4:"𝟞";s:1:"6";s:4:"𝟟";s:1:"7";s:4:"𝟠";s:1:"8";s:4:"𝟡";s:1:"9";s:4:"𝟢";s:1:"0";s:4:"𝟣";s:1:"1";s:4:"𝟤";s:1:"2";s:4:"𝟥";s:1:"3";s:4:"𝟦";s:1:"4";s:4:"𝟧";s:1:"5";s:4:"𝟨";s:1:"6";s:4:"𝟩";s:1:"7";s:4:"𝟪";s:1:"8";s:4:"𝟫";s:1:"9";s:4:"𝟬";s:1:"0";s:4:"𝟭";s:1:"1";s:4:"𝟮";s:1:"2";s:4:"𝟯";s:1:"3";s:4:"𝟰";s:1:"4";s:4:"𝟱";s:1:"5";s:4:"𝟲";s:1:"6";s:4:"𝟳";s:1:"7";s:4:"𝟴";s:1:"8";s:4:"𝟵";s:1:"9";s:4:"𝟶";s:1:"0";s:4:"𝟷";s:1:"1";s:4:"𝟸";s:1:"2";s:4:"𝟹";s:1:"3";s:4:"𝟺";s:1:"4";s:4:"𝟻";s:1:"5";s:4:"𝟼";s:1:"6";s:4:"𝟽";s:1:"7";s:4:"𝟾";s:1:"8";s:4:"𝟿";s:1:"9";s:4:"丽";s:3:"丽";s:4:"丸";s:3:"丸";s:4:"乁";s:3:"乁";s:4:"𠄢";s:4:"𠄢";s:4:"你";s:3:"你";s:4:"侮";s:3:"侮";s:4:"侻";s:3:"侻";s:4:"倂";s:3:"倂";s:4:"偺";s:3:"偺";s:4:"備";s:3:"備";s:4:"僧";s:3:"僧";s:4:"像";s:3:"像";s:4:"㒞";s:3:"㒞";s:4:"𠘺";s:4:"𠘺";s:4:"免";s:3:"免";s:4:"兔";s:3:"兔";s:4:"兤";s:3:"兤";s:4:"具";s:3:"具";s:4:"𠔜";s:4:"𠔜";s:4:"㒹";s:3:"㒹";s:4:"內";s:3:"內";s:4:"再";s:3:"再";s:4:"𠕋";s:4:"𠕋";s:4:"冗";s:3:"冗";s:4:"冤";s:3:"冤";s:4:"仌";s:3:"仌";s:4:"冬";s:3:"冬";s:4:"况";s:3:"况";s:4:"𩇟";s:4:"𩇟";s:4:"凵";s:3:"凵";s:4:"刃";s:3:"刃";s:4:"㓟";s:3:"㓟";s:4:"刻";s:3:"刻";s:4:"剆";s:3:"剆";s:4:"割";s:3:"割";s:4:"剷";s:3:"剷";s:4:"㔕";s:3:"㔕";s:4:"勇";s:3:"勇";s:4:"勉";s:3:"勉";s:4:"勤";s:3:"勤";s:4:"勺";s:3:"勺";s:4:"包";s:3:"包";s:4:"匆";s:3:"匆";s:4:"北";s:3:"北";s:4:"卉";s:3:"卉";s:4:"卑";s:3:"卑";s:4:"博";s:3:"博";s:4:"即";s:3:"即";s:4:"卽";s:3:"卽";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"𠨬";s:4:"𠨬";s:4:"灰";s:3:"灰";s:4:"及";s:3:"及";s:4:"叟";s:3:"叟";s:4:"𠭣";s:4:"𠭣";s:4:"叫";s:3:"叫";s:4:"叱";s:3:"叱";s:4:"吆";s:3:"吆";s:4:"咞";s:3:"咞";s:4:"吸";s:3:"吸";s:4:"呈";s:3:"呈";s:4:"周";s:3:"周";s:4:"咢";s:3:"咢";s:4:"哶";s:3:"哶";s:4:"唐";s:3:"唐";s:4:"啓";s:3:"啓";s:4:"啣";s:3:"啣";s:4:"善";s:3:"善";s:4:"善";s:3:"善";s:4:"喙";s:3:"喙";s:4:"喫";s:3:"喫";s:4:"喳";s:3:"喳";s:4:"嗂";s:3:"嗂";s:4:"圖";s:3:"圖";s:4:"嘆";s:3:"嘆";s:4:"圗";s:3:"圗";s:4:"噑";s:3:"噑";s:4:"噴";s:3:"噴";s:4:"切";s:3:"切";s:4:"壮";s:3:"壮";s:4:"城";s:3:"城";s:4:"埴";s:3:"埴";s:4:"堍";s:3:"堍";s:4:"型";s:3:"型";s:4:"堲";s:3:"堲";s:4:"報";s:3:"報";s:4:"墬";s:3:"墬";s:4:"𡓤";s:4:"𡓤";s:4:"売";s:3:"売";s:4:"壷";s:3:"壷";s:4:"夆";s:3:"夆";s:4:"多";s:3:"多";s:4:"夢";s:3:"夢";s:4:"奢";s:3:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:4:"姬";s:3:"姬";s:4:"娛";s:3:"娛";s:4:"娧";s:3:"娧";s:4:"姘";s:3:"姘";s:4:"婦";s:3:"婦";s:4:"㛮";s:3:"㛮";s:4:"㛼";s:3:"㛼";s:4:"嬈";s:3:"嬈";s:4:"嬾";s:3:"嬾";s:4:"嬾";s:3:"嬾";s:4:"𡧈";s:4:"𡧈";s:4:"寃";s:3:"寃";s:4:"寘";s:3:"寘";s:4:"寧";s:3:"寧";s:4:"寳";s:3:"寳";s:4:"𡬘";s:4:"𡬘";s:4:"寿";s:3:"寿";s:4:"将";s:3:"将";s:4:"当";s:3:"当";s:4:"尢";s:3:"尢";s:4:"㞁";s:3:"㞁";s:4:"屠";s:3:"屠";s:4:"屮";s:3:"屮";s:4:"峀";s:3:"峀";s:4:"岍";s:3:"岍";s:4:"𡷤";s:4:"𡷤";s:4:"嵃";s:3:"嵃";s:4:"𡷦";s:4:"𡷦";s:4:"嵮";s:3:"嵮";s:4:"嵫";s:3:"嵫";s:4:"嵼";s:3:"嵼";s:4:"巡";s:3:"巡";s:4:"巢";s:3:"巢";s:4:"㠯";s:3:"㠯";s:4:"巽";s:3:"巽";s:4:"帨";s:3:"帨";s:4:"帽";s:3:"帽";s:4:"幩";s:3:"幩";s:4:"㡢";s:3:"㡢";s:4:"𢆃";s:4:"𢆃";s:4:"㡼";s:3:"㡼";s:4:"庰";s:3:"庰";s:4:"庳";s:3:"庳";s:4:"庶";s:3:"庶";s:4:"廊";s:3:"廊";s:4:"𪎒";s:4:"𪎒";s:4:"廾";s:3:"廾";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"舁";s:3:"舁";s:4:"弢";s:3:"弢";s:4:"弢";s:3:"弢";s:4:"㣇";s:3:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:4:"形";s:3:"形";s:4:"彫";s:3:"彫";s:4:"㣣";s:3:"㣣";s:4:"徚";s:3:"徚";s:4:"忍";s:3:"忍";s:4:"志";s:3:"志";s:4:"忹";s:3:"忹";s:4:"悁";s:3:"悁";s:4:"㤺";s:3:"㤺";s:4:"㤜";s:3:"㤜";s:4:"悔";s:3:"悔";s:4:"𢛔";s:4:"𢛔";s:4:"惇";s:3:"惇";s:4:"慈";s:3:"慈";s:4:"慌";s:3:"慌";s:4:"慎";s:3:"慎";s:4:"慌";s:3:"慌";s:4:"慺";s:3:"慺";s:4:"憎";s:3:"憎";s:4:"憲";s:3:"憲";s:4:"憤";s:3:"憤";s:4:"憯";s:3:"憯";s:4:"懞";s:3:"懞";s:4:"懲";s:3:"懲";s:4:"懶";s:3:"懶";s:4:"成";s:3:"成";s:4:"戛";s:3:"戛";s:4:"扝";s:3:"扝";s:4:"抱";s:3:"抱";s:4:"拔";s:3:"拔";s:4:"捐";s:3:"捐";s:4:"𢬌";s:4:"𢬌";s:4:"挽";s:3:"挽";s:4:"拼";s:3:"拼";s:4:"捨";s:3:"捨";s:4:"掃";s:3:"掃";s:4:"揤";s:3:"揤";s:4:"𢯱";s:4:"𢯱";s:4:"搢";s:3:"搢";s:4:"揅";s:3:"揅";s:4:"掩";s:3:"掩";s:4:"㨮";s:3:"㨮";s:4:"摩";s:3:"摩";s:4:"摾";s:3:"摾";s:4:"撝";s:3:"撝";s:4:"摷";s:3:"摷";s:4:"㩬";s:3:"㩬";s:4:"敏";s:3:"敏";s:4:"敬";s:3:"敬";s:4:"𣀊";s:4:"𣀊";s:4:"旣";s:3:"旣";s:4:"書";s:3:"書";s:4:"晉";s:3:"晉";s:4:"㬙";s:3:"㬙";s:4:"暑";s:3:"暑";s:4:"㬈";s:3:"㬈";s:4:"㫤";s:3:"㫤";s:4:"冒";s:3:"冒";s:4:"冕";s:3:"冕";s:4:"最";s:3:"最";s:4:"暜";s:3:"暜";s:4:"肭";s:3:"肭";s:4:"䏙";s:3:"䏙";s:4:"朗";s:3:"朗";s:4:"望";s:3:"望";s:4:"朡";s:3:"朡";s:4:"杞";s:3:"杞";s:4:"杓";s:3:"杓";s:4:"𣏃";s:4:"𣏃";s:4:"㭉";s:3:"㭉";s:4:"柺";s:3:"柺";s:4:"枅";s:3:"枅";s:4:"桒";s:3:"桒";s:4:"梅";s:3:"梅";s:4:"𣑭";s:4:"𣑭";s:4:"梎";s:3:"梎";s:4:"栟";s:3:"栟";s:4:"椔";s:3:"椔";s:4:"㮝";s:3:"㮝";s:4:"楂";s:3:"楂";s:4:"榣";s:3:"榣";s:4:"槪";s:3:"槪";s:4:"檨";s:3:"檨";s:4:"𣚣";s:4:"𣚣";s:4:"櫛";s:3:"櫛";s:4:"㰘";s:3:"㰘";s:4:"次";s:3:"次";s:4:"𣢧";s:4:"𣢧";s:4:"歔";s:3:"歔";s:4:"㱎";s:3:"㱎";s:4:"歲";s:3:"歲";s:4:"殟";s:3:"殟";s:4:"殺";s:3:"殺";s:4:"殻";s:3:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:4:"汎";s:3:"汎";s:4:"𣲼";s:4:"𣲼";s:4:"沿";s:3:"沿";s:4:"泍";s:3:"泍";s:4:"汧";s:3:"汧";s:4:"洖";s:3:"洖";s:4:"派";s:3:"派";s:4:"海";s:3:"海";s:4:"流";s:3:"流";s:4:"浩";s:3:"浩";s:4:"浸";s:3:"浸";s:4:"涅";s:3:"涅";s:4:"𣴞";s:4:"𣴞";s:4:"洴";s:3:"洴";s:4:"港";s:3:"港";s:4:"湮";s:3:"湮";s:4:"㴳";s:3:"㴳";s:4:"滋";s:3:"滋";s:4:"滇";s:3:"滇";s:4:"𣻑";s:4:"𣻑";s:4:"淹";s:3:"淹";s:4:"潮";s:3:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:4:"濆";s:3:"濆";s:4:"瀹";s:3:"瀹";s:4:"瀞";s:3:"瀞";s:4:"瀛";s:3:"瀛";s:4:"㶖";s:3:"㶖";s:4:"灊";s:3:"灊";s:4:"災";s:3:"災";s:4:"灷";s:3:"灷";s:4:"炭";s:3:"炭";s:4:"𠔥";s:4:"𠔥";s:4:"煅";s:3:"煅";s:4:"𤉣";s:4:"𤉣";s:4:"熜";s:3:"熜";s:4:"𤎫";s:4:"𤎫";s:4:"爨";s:3:"爨";s:4:"爵";s:3:"爵";s:4:"牐";s:3:"牐";s:4:"𤘈";s:4:"𤘈";s:4:"犀";s:3:"犀";s:4:"犕";s:3:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:4:"獺";s:3:"獺";s:4:"王";s:3:"王";s:4:"㺬";s:3:"㺬";s:4:"玥";s:3:"玥";s:4:"㺸";s:3:"㺸";s:4:"㺸";s:3:"㺸";s:4:"瑇";s:3:"瑇";s:4:"瑜";s:3:"瑜";s:4:"瑱";s:3:"瑱";s:4:"璅";s:3:"璅";s:4:"瓊";s:3:"瓊";s:4:"㼛";s:3:"㼛";s:4:"甤";s:3:"甤";s:4:"𤰶";s:4:"𤰶";s:4:"甾";s:3:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"異";s:3:"異";s:4:"𢆟";s:4:"𢆟";s:4:"瘐";s:3:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:4:"㿼";s:3:"㿼";s:4:"䀈";s:3:"䀈";s:4:"直";s:3:"直";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:4:"眞";s:3:"眞";s:4:"真";s:3:"真";s:4:"真";s:3:"真";s:4:"睊";s:3:"睊";s:4:"䀹";s:3:"䀹";s:4:"瞋";s:3:"瞋";s:4:"䁆";s:3:"䁆";s:4:"䂖";s:3:"䂖";s:4:"𥐝";s:4:"𥐝";s:4:"硎";s:3:"硎";s:4:"碌";s:3:"碌";s:4:"磌";s:3:"磌";s:4:"䃣";s:3:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"祖";s:3:"祖";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:4:"福";s:3:"福";s:4:"秫";s:3:"秫";s:4:"䄯";s:3:"䄯";s:4:"穀";s:3:"穀";s:4:"穊";s:3:"穊";s:4:"穏";s:3:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"竮";s:3:"竮";s:4:"䈂";s:3:"䈂";s:4:"𥮫";s:4:"𥮫";s:4:"篆";s:3:"篆";s:4:"築";s:3:"築";s:4:"䈧";s:3:"䈧";s:4:"𥲀";s:4:"𥲀";s:4:"糒";s:3:"糒";s:4:"䊠";s:3:"䊠";s:4:"糨";s:3:"糨";s:4:"糣";s:3:"糣";s:4:"紀";s:3:"紀";s:4:"𥾆";s:4:"𥾆";s:4:"絣";s:3:"絣";s:4:"䌁";s:3:"䌁";s:4:"緇";s:3:"緇";s:4:"縂";s:3:"縂";s:4:"繅";s:3:"繅";s:4:"䌴";s:3:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:4:"䍙";s:3:"䍙";s:4:"𦋙";s:4:"𦋙";s:4:"罺";s:3:"罺";s:4:"𦌾";s:4:"𦌾";s:4:"羕";s:3:"羕";s:4:"翺";s:3:"翺";s:4:"者";s:3:"者";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:4:"聠";s:3:"聠";s:4:"𦖨";s:4:"𦖨";s:4:"聰";s:3:"聰";s:4:"𣍟";s:4:"𣍟";s:4:"䏕";s:3:"䏕";s:4:"育";s:3:"育";s:4:"脃";s:3:"脃";s:4:"䐋";s:3:"䐋";s:4:"脾";s:3:"脾";s:4:"媵";s:3:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:4:"舁";s:3:"舁";s:4:"舄";s:3:"舄";s:4:"辞";s:3:"辞";s:4:"䑫";s:3:"䑫";s:4:"芑";s:3:"芑";s:4:"芋";s:3:"芋";s:4:"芝";s:3:"芝";s:4:"劳";s:3:"劳";s:4:"花";s:3:"花";s:4:"芳";s:3:"芳";s:4:"芽";s:3:"芽";s:4:"苦";s:3:"苦";s:4:"𦬼";s:4:"𦬼";s:4:"若";s:3:"若";s:4:"茝";s:3:"茝";s:4:"荣";s:3:"荣";s:4:"莭";s:3:"莭";s:4:"茣";s:3:"茣";s:4:"莽";s:3:"莽";s:4:"菧";s:3:"菧";s:4:"著";s:3:"著";s:4:"荓";s:3:"荓";s:4:"菊";s:3:"菊";s:4:"菌";s:3:"菌";s:4:"菜";s:3:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:4:"䔫";s:3:"䔫";s:4:"蓱";s:3:"蓱";s:4:"蓳";s:3:"蓳";s:4:"蔖";s:3:"蔖";s:4:"𧏊";s:4:"𧏊";s:4:"蕤";s:3:"蕤";s:4:"𦼬";s:4:"𦼬";s:4:"䕝";s:3:"䕝";s:4:"䕡";s:3:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:4:"䕫";s:3:"䕫";s:4:"虐";s:3:"虐";s:4:"虜";s:3:"虜";s:4:"虧";s:3:"虧";s:4:"虩";s:3:"虩";s:4:"蚩";s:3:"蚩";s:4:"蚈";s:3:"蚈";s:4:"蜎";s:3:"蜎";s:4:"蛢";s:3:"蛢";s:4:"蝹";s:3:"蝹";s:4:"蜨";s:3:"蜨";s:4:"蝫";s:3:"蝫";s:4:"螆";s:3:"螆";s:4:"䗗";s:3:"䗗";s:4:"蟡";s:3:"蟡";s:4:"蠁";s:3:"蠁";s:4:"䗹";s:3:"䗹";s:4:"衠";s:3:"衠";s:4:"衣";s:3:"衣";s:4:"𧙧";s:4:"𧙧";s:4:"裗";s:3:"裗";s:4:"裞";s:3:"裞";s:4:"䘵";s:3:"䘵";s:4:"裺";s:3:"裺";s:4:"㒻";s:3:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:4:"䚾";s:3:"䚾";s:4:"䛇";s:3:"䛇";s:4:"誠";s:3:"誠";s:4:"諭";s:3:"諭";s:4:"變";s:3:"變";s:4:"豕";s:3:"豕";s:4:"𧲨";s:4:"𧲨";s:4:"貫";s:3:"貫";s:4:"賁";s:3:"賁";s:4:"贛";s:3:"贛";s:4:"起";s:3:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:4:"跋";s:3:"跋";s:4:"趼";s:3:"趼";s:4:"跰";s:3:"跰";s:4:"𠣞";s:4:"𠣞";s:4:"軔";s:3:"軔";s:4:"輸";s:3:"輸";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:4:"邔";s:3:"邔";s:4:"郱";s:3:"郱";s:4:"鄑";s:3:"鄑";s:4:"𨜮";s:4:"𨜮";s:4:"鄛";s:3:"鄛";s:4:"鈸";s:3:"鈸";s:4:"鋗";s:3:"鋗";s:4:"鋘";s:3:"鋘";s:4:"鉼";s:3:"鉼";s:4:"鏹";s:3:"鏹";s:4:"鐕";s:3:"鐕";s:4:"𨯺";s:4:"𨯺";s:4:"開";s:3:"開";s:4:"䦕";s:3:"䦕";s:4:"閷";s:3:"閷";s:4:"𨵷";s:4:"𨵷";s:4:"䧦";s:3:"䧦";s:4:"雃";s:3:"雃";s:4:"嶲";s:3:"嶲";s:4:"霣";s:3:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:4:"䩮";s:3:"䩮";s:4:"䩶";s:3:"䩶";s:4:"韠";s:3:"韠";s:4:"𩐊";s:4:"𩐊";s:4:"䪲";s:3:"䪲";s:4:"𩒖";s:4:"𩒖";s:4:"頋";s:3:"頋";s:4:"頋";s:3:"頋";s:4:"頩";s:3:"頩";s:4:"𩖶";s:4:"𩖶";s:4:"飢";s:3:"飢";s:4:"䬳";s:3:"䬳";s:4:"餩";s:3:"餩";s:4:"馧";s:3:"馧";s:4:"駂";s:3:"駂";s:4:"駾";s:3:"駾";s:4:"䯎";s:3:"䯎";s:4:"𩬰";s:4:"𩬰";s:4:"鬒";s:3:"鬒";s:4:"鱀";s:3:"鱀";s:4:"鳽";s:3:"鳽";s:4:"䳎";s:3:"䳎";s:4:"䳭";s:3:"䳭";s:4:"鵧";s:3:"鵧";s:4:"𪃎";s:4:"𪃎";s:4:"䳸";s:3:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:4:"麻";s:3:"麻";s:4:"䵖";s:3:"䵖";s:4:"黹";s:3:"黹";s:4:"黾";s:3:"黾";s:4:"鼅";s:3:"鼅";s:4:"鼏";s:3:"鼏";s:4:"鼖";s:3:"鼖";s:4:"鼻";s:3:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
+?>
diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php
new file mode 100644
index 00000000..688a80f1
--- /dev/null
+++ b/includes/normal/UtfNormalGenerate.php
@@ -0,0 +1,235 @@
+<?php
+# 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
+
+/**
+ * This script generates UniNormalData.inc from the Unicode Character Database
+ * and supplementary files.
+ *
+ * @package UtfNormal
+ * @access private
+ */
+
+/** */
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+require_once 'UtfNormalUtil.php';
+
+$in = fopen("DerivedNormalizationProps.txt", "rt" );
+if( !$in ) {
+ print "Can't open DerivedNormalizationProps.txt for reading.\n";
+ print "If necessary, fetch this file from the internet:\n";
+ print "http://www.unicode.org/Public/UNIDATA/CompositionExclusions.txt\n";
+ exit(-1);
+}
+print "Initializing normalization quick check tables...\n";
+$checkNFC = array();
+while( false !== ($line = fgets( $in ) ) ) {
+ if( preg_match( '/^([0-9A-F]+)(?:..([0-9A-F]+))?\s*;\s*(NFC_QC)\s*;\s*([MN])/', $line, $matches ) ) {
+ list( $junk, $first, $last, $prop, $value ) = $matches;
+ #print "$first $last $prop $value\n";
+ if( !$last ) $last = $first;
+ for( $i = hexdec( $first ); $i <= hexdec( $last ); $i++) {
+ $char = codepointToUtf8( $i );
+ $checkNFC[$char] = $value;
+ }
+ }
+}
+fclose( $in );
+
+$in = fopen("CompositionExclusions.txt", "rt" );
+if( !$in ) {
+ print "Can't open CompositionExclusions.txt for reading.\n";
+ print "If necessary, fetch this file from the internet:\n";
+ print "http://www.unicode.org/Public/UNIDATA/CompositionExclusions.txt\n";
+ exit(-1);
+}
+$exclude = array();
+while( false !== ($line = fgets( $in ) ) ) {
+ if( preg_match( '/^([0-9A-F]+)/i', $line, $matches ) ) {
+ $codepoint = $matches[1];
+ $source = codepointToUtf8( hexdec( $codepoint ) );
+ $exclude[$source] = true;
+ }
+}
+fclose($in);
+
+$in = fopen("UnicodeData.txt", "rt" );
+if( !$in ) {
+ print "Can't open UnicodeData.txt for reading.\n";
+ print "If necessary, fetch this file from the internet:\n";
+ print "http://www.unicode.org/Public/UNIDATA/UnicodeData.txt\n";
+ exit(-1);
+}
+
+$compatibilityDecomp = array();
+$canonicalDecomp = array();
+$canonicalComp = array();
+$combiningClass = array();
+$total = 0;
+$compat = 0;
+$canon = 0;
+
+print "Reading character definitions...\n";
+while( false !== ($line = fgets( $in ) ) ) {
+ $columns = split(';', $line);
+ $codepoint = $columns[0];
+ $name = $columns[1];
+ $canonicalCombiningClass = $columns[3];
+ $decompositionMapping = $columns[5];
+
+ $source = codepointToUtf8( hexdec( $codepoint ) );
+
+ if( $canonicalCombiningClass != 0 ) {
+ $combiningClass[$source] = intval( $canonicalCombiningClass );
+ }
+
+ if( $decompositionMapping === '' ) continue;
+ if( preg_match( '/^<(.+)> (.*)$/', $decompositionMapping, $matches ) ) {
+ # Compatibility decomposition
+ $canonical = false;
+ $decompositionMapping = $matches[2];
+ $compat++;
+ } else {
+ $canonical = true;
+ $canon++;
+ }
+ $total++;
+ $dest = hexSequenceToUtf8( $decompositionMapping );
+
+ $compatibilityDecomp[$source] = $dest;
+ if( $canonical ) {
+ $canonicalDecomp[$source] = $dest;
+ if( empty( $exclude[$source] ) ) {
+ $canonicalComp[$dest] = $source;
+ }
+ }
+ #print "$codepoint | $canonicalCombiningClasses | $decompositionMapping\n";
+}
+fclose( $in );
+
+print "Recursively expanding canonical mappings...\n";
+$changed = 42;
+$pass = 1;
+while( $changed > 0 ) {
+ print "pass $pass\n";
+ $changed = 0;
+ foreach( $canonicalDecomp as $source => $dest ) {
+ $newDest = preg_replace_callback(
+ '/([\xc0-\xff][\x80-\xbf]+)/',
+ 'callbackCanonical',
+ $dest);
+ if( $newDest === $dest ) continue;
+ $changed++;
+ $canonicalDecomp[$source] = $newDest;
+ }
+ $pass++;
+}
+
+print "Recursively expanding compatibility mappings...\n";
+$changed = 42;
+$pass = 1;
+while( $changed > 0 ) {
+ print "pass $pass\n";
+ $changed = 0;
+ foreach( $compatibilityDecomp as $source => $dest ) {
+ $newDest = preg_replace_callback(
+ '/([\xc0-\xff][\x80-\xbf]+)/',
+ 'callbackCompat',
+ $dest);
+ if( $newDest === $dest ) continue;
+ $changed++;
+ $compatibilityDecomp[$source] = $newDest;
+ }
+ $pass++;
+}
+
+print "$total decomposition mappings ($canon canonical, $compat compatibility)\n";
+
+$out = fopen("UtfNormalData.inc", "wt");
+if( $out ) {
+ $serCombining = escapeSingleString( serialize( $combiningClass ) );
+ $serComp = escapeSingleString( serialize( $canonicalComp ) );
+ $serCanon = escapeSingleString( serialize( $canonicalDecomp ) );
+ $serCheckNFC = escapeSingleString( serialize( $checkNFC ) );
+ $outdata = "<" . "?php
+/**
+ * This file was automatically generated -- do not edit!
+ * Run UtfNormalGenerate.php to create this file again (make clean && make)
+ * @package MediaWiki
+ */
+/** */
+global \$utfCombiningClass, \$utfCanonicalComp, \$utfCanonicalDecomp, \$utfCheckNFC;
+\$utfCombiningClass = unserialize( '$serCombining' );
+\$utfCanonicalComp = unserialize( '$serComp' );
+\$utfCanonicalDecomp = unserialize( '$serCanon' );
+\$utfCheckNFC = unserialize( '$serCheckNFC' );
+?" . ">\n";
+ fputs( $out, $outdata );
+ fclose( $out );
+ print "Wrote out UtfNormalData.inc\n";
+} else {
+ print "Can't create file UtfNormalData.inc\n";
+ exit(-1);
+}
+
+
+$out = fopen("UtfNormalDataK.inc", "wt");
+if( $out ) {
+ $serCompat = escapeSingleString( serialize( $compatibilityDecomp ) );
+ $outdata = "<" . "?php
+/**
+ * This file was automatically generated -- do not edit!
+ * Run UtfNormalGenerate.php to create this file again (make clean && make)
+ * @package MediaWiki
+ */
+/** */
+global \$utfCompatibilityDecomp;
+\$utfCompatibilityDecomp = unserialize( '$serCompat' );
+?" . ">\n";
+ fputs( $out, $outdata );
+ fclose( $out );
+ print "Wrote out UtfNormalDataK.inc\n";
+ exit(0);
+} else {
+ print "Can't create file UtfNormalDataK.inc\n";
+ exit(-1);
+}
+
+# ---------------
+
+function callbackCanonical( $matches ) {
+ global $canonicalDecomp;
+ if( isset( $canonicalDecomp[$matches[1]] ) ) {
+ return $canonicalDecomp[$matches[1]];
+ }
+ return $matches[1];
+}
+
+function callbackCompat( $matches ) {
+ global $compatibilityDecomp;
+ if( isset( $compatibilityDecomp[$matches[1]] ) ) {
+ return $compatibilityDecomp[$matches[1]];
+ }
+ return $matches[1];
+}
+
+?>
diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php
new file mode 100644
index 00000000..6d95bf85
--- /dev/null
+++ b/includes/normal/UtfNormalTest.php
@@ -0,0 +1,249 @@
+<?php
+# 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
+
+/**
+ * Implements the conformance test at:
+ * http://www.unicode.org/Public/UNIDATA/NormalizationTest.txt
+ * @package UtfNormal
+ */
+
+/** */
+$verbose = true;
+#define( 'PRETTY_UTF8', true );
+
+if( defined( 'PRETTY_UTF8' ) ) {
+ function pretty( $string ) {
+ return preg_replace( '/([\x00-\xff])/e',
+ 'sprintf("%02X", ord("$1"))',
+ $string );
+ }
+} else {
+ /**
+ * @ignore
+ */
+ function pretty( $string ) {
+ return trim( preg_replace( '/(.)/use',
+ 'sprintf("%04X ", utf8ToCodepoint("$1"))',
+ $string ) );
+ }
+}
+
+if( isset( $_SERVER['argv'] ) && in_array( '--icu', $_SERVER['argv'] ) ) {
+ dl( 'php_utfnormal.so' );
+}
+
+require_once 'UtfNormalUtil.php';
+require_once 'UtfNormal.php';
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+$in = fopen("NormalizationTest.txt", "rt");
+if( !$in ) {
+ print "Couldn't open NormalizationTest.txt -- can't run tests.\n";
+ print "If necessary, manually download this file. It can be obtained at\n";
+ print "http://www.unicode.org/Public/UNIDATA/NormalizationTest.txt";
+ exit(-1);
+}
+
+$normalizer = new UtfNormal;
+
+$total = 0;
+$success = 0;
+$failure = 0;
+$ok = true;
+$testedChars = array();
+while( false !== ( $line = fgets( $in ) ) ) {
+ list( $data, $comment ) = explode( '#', $line );
+ if( $data === '' ) continue;
+ if( preg_match( '/@Part([\d])/', $data, $matches ) ) {
+ if( $matches[1] > 0 ) {
+ $ok = reportResults( $total, $success, $failure ) && $ok;
+ }
+ print "Part {$matches[1]}: $comment";
+ continue;
+ }
+
+ $columns = array_map( "hexSequenceToUtf8", explode( ";", $data ) );
+ array_unshift( $columns, '' );
+
+ $testedChars[$columns[1]] = true;
+ $total++;
+ if( testNormals( $normalizer, $columns, $comment ) ) {
+ $success++;
+ } else {
+ $failure++;
+ # print "FAILED: $comment";
+ }
+ if( $total % 100 == 0 ) print "$total ";
+}
+fclose( $in );
+
+$ok = reportResults( $total, $success, $failure ) && $ok;
+
+$in = fopen("UnicodeData.txt", "rt" );
+if( !$in ) {
+ print "Can't open UnicodeData.txt for reading.\n";
+ print "If necessary, fetch this file from the internet:\n";
+ print "http://www.unicode.org/Public/UNIDATA/UnicodeData.txt\n";
+ exit(-1);
+}
+print "Now testing invariants...\n";
+while( false !== ($line = fgets( $in ) ) ) {
+ $cols = explode( ';', $line );
+ $char = codepointToUtf8( hexdec( $cols[0] ) );
+ $desc = $cols[0] . ": " . $cols[1];
+ if( $char < "\x20" || $char >= UTF8_SURROGATE_FIRST && $char <= UTF8_SURROGATE_LAST ) {
+ # Can't check NULL with the ICU plugin, as null bytes fail in C land.
+ # Skip other control characters, as we strip them for XML safety.
+ # Surrogates are illegal on their own or in UTF-8, ignore.
+ continue;
+ }
+ if( empty( $testedChars[$char] ) ) {
+ $total++;
+ if( testInvariant( $normalizer, $char, $desc ) ) {
+ $success++;
+ } else {
+ $failure++;
+ }
+ if( $total % 100 == 0 ) print "$total ";
+ }
+}
+fclose( $in );
+
+$ok = reportResults( $total, $success, $failure ) && $ok;
+
+if( $ok ) {
+ print "TEST SUCCEEDED!\n";
+ exit(0);
+} else {
+ print "TEST FAILED!\n";
+ exit(-1);
+}
+
+## ------
+
+function reportResults( &$total, &$success, &$failure ) {
+ $percSucc = intval( $success * 100 / $total );
+ $percFail = intval( $failure * 100 / $total );
+ print "\n";
+ print "$success tests successful ($percSucc%)\n";
+ print "$failure tests failed ($percFail%)\n\n";
+ $ok = ($success > 0 && $failure == 0);
+ $total = 0;
+ $success = 0;
+ $failure = 0;
+ return $ok;
+}
+
+function testNormals( &$u, $c, $comment, $reportFailure = false ) {
+ $result = testNFC( $u, $c, $comment, $reportFailure );
+ $result = testNFD( $u, $c, $comment, $reportFailure ) && $result;
+ $result = testNFKC( $u, $c, $comment, $reportFailure ) && $result;
+ $result = testNFKD( $u, $c, $comment, $reportFailure ) && $result;
+ $result = testCleanUp( $u, $c, $comment, $reportFailure ) && $result;
+
+ global $verbose;
+ if( $verbose && !$result && !$reportFailure ) {
+ print $comment;
+ testNormals( $u, $c, $comment, true );
+ }
+ return $result;
+}
+
+function verbosify( $a, $b, $col, $form, $verbose ) {
+ #$result = ($a === $b);
+ $result = (strcmp( $a, $b ) == 0);
+ if( $verbose ) {
+ $aa = pretty( $a );
+ $bb = pretty( $b );
+ $ok = $result ? "succeed" : " failed";
+ $eq = $result ? "==" : "!=";
+ print " $ok $form c$col '$aa' $eq '$bb'\n";
+ }
+ return $result;
+}
+
+function testNFC( &$u, $c, $comment, $verbose ) {
+ $result = verbosify( $c[2], $u->toNFC( $c[1] ), 1, 'NFC', $verbose );
+ $result = verbosify( $c[2], $u->toNFC( $c[2] ), 2, 'NFC', $verbose ) && $result;
+ $result = verbosify( $c[2], $u->toNFC( $c[3] ), 3, 'NFC', $verbose ) && $result;
+ $result = verbosify( $c[4], $u->toNFC( $c[4] ), 4, 'NFC', $verbose ) && $result;
+ $result = verbosify( $c[4], $u->toNFC( $c[5] ), 5, 'NFC', $verbose ) && $result;
+ return $result;
+}
+
+function testCleanUp( &$u, $c, $comment, $verbose ) {
+ $x = $c[1];
+ $result = verbosify( $c[2], $u->cleanUp( $x ), 1, 'cleanUp', $verbose );
+ $x = $c[2];
+ $result = verbosify( $c[2], $u->cleanUp( $x ), 2, 'cleanUp', $verbose ) && $result;
+ $x = $c[3];
+ $result = verbosify( $c[2], $u->cleanUp( $x ), 3, 'cleanUp', $verbose ) && $result;
+ $x = $c[4];
+ $result = verbosify( $c[4], $u->cleanUp( $x ), 4, 'cleanUp', $verbose ) && $result;
+ $x = $c[5];
+ $result = verbosify( $c[4], $u->cleanUp( $x ), 5, 'cleanUp', $verbose ) && $result;
+ return $result;
+}
+
+function testNFD( &$u, $c, $comment, $verbose ) {
+ $result = verbosify( $c[3], $u->toNFD( $c[1] ), 1, 'NFD', $verbose );
+ $result = verbosify( $c[3], $u->toNFD( $c[2] ), 2, 'NFD', $verbose ) && $result;
+ $result = verbosify( $c[3], $u->toNFD( $c[3] ), 3, 'NFD', $verbose ) && $result;
+ $result = verbosify( $c[5], $u->toNFD( $c[4] ), 4, 'NFD', $verbose ) && $result;
+ $result = verbosify( $c[5], $u->toNFD( $c[5] ), 5, 'NFD', $verbose ) && $result;
+ return $result;
+}
+
+function testNFKC( &$u, $c, $comment, $verbose ) {
+ $result = verbosify( $c[4], $u->toNFKC( $c[1] ), 1, 'NFKC', $verbose );
+ $result = verbosify( $c[4], $u->toNFKC( $c[2] ), 2, 'NFKC', $verbose ) && $result;
+ $result = verbosify( $c[4], $u->toNFKC( $c[3] ), 3, 'NFKC', $verbose ) && $result;
+ $result = verbosify( $c[4], $u->toNFKC( $c[4] ), 4, 'NFKC', $verbose ) && $result;
+ $result = verbosify( $c[4], $u->toNFKC( $c[5] ), 5, 'NFKC', $verbose ) && $result;
+ return $result;
+}
+
+function testNFKD( &$u, $c, $comment, $verbose ) {
+ $result = verbosify( $c[5], $u->toNFKD( $c[1] ), 1, 'NFKD', $verbose );
+ $result = verbosify( $c[5], $u->toNFKD( $c[2] ), 2, 'NFKD', $verbose ) && $result;
+ $result = verbosify( $c[5], $u->toNFKD( $c[3] ), 3, 'NFKD', $verbose ) && $result;
+ $result = verbosify( $c[5], $u->toNFKD( $c[4] ), 4, 'NFKD', $verbose ) && $result;
+ $result = verbosify( $c[5], $u->toNFKD( $c[5] ), 5, 'NFKD', $verbose ) && $result;
+ return $result;
+}
+
+function testInvariant( &$u, $char, $desc, $reportFailure = false ) {
+ $result = verbosify( $char, $u->toNFC( $char ), 1, 'NFC', $reportFailure );
+ $result = verbosify( $char, $u->toNFD( $char ), 1, 'NFD', $reportFailure ) && $result;
+ $result = verbosify( $char, $u->toNFKC( $char ), 1, 'NFKC', $reportFailure ) && $result;
+ $result = verbosify( $char, $u->toNFKD( $char ), 1, 'NFKD', $reportFailure ) && $result;
+ $c = $char;
+ $result = verbosify( $char, $u->cleanUp( $char ), 1, 'cleanUp', $reportFailure ) && $result;
+ global $verbose;
+ if( $verbose && !$result && !$reportFailure ) {
+ print $desc;
+ testInvariant( $u, $char, $desc, true );
+ }
+ return $result;
+}
+
+?>
diff --git a/includes/normal/UtfNormalUtil.php b/includes/normal/UtfNormalUtil.php
new file mode 100644
index 00000000..94224e3d
--- /dev/null
+++ b/includes/normal/UtfNormalUtil.php
@@ -0,0 +1,142 @@
+<?php
+# 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
+
+/**
+ * Some of these functions are adapted from places in MediaWiki.
+ * Should probably merge them for consistency.
+ *
+ * @package UtfNormal
+ * @public
+ */
+
+/** */
+
+/**
+ * Return UTF-8 sequence for a given Unicode code point.
+ * May die if fed out of range data.
+ *
+ * @param $codepoint Integer:
+ * @return String
+ * @public
+ */
+function codepointToUtf8( $codepoint ) {
+ if($codepoint < 0x80) return chr($codepoint);
+ if($codepoint < 0x800) return chr($codepoint >> 6 & 0x3f | 0xc0) .
+ chr($codepoint & 0x3f | 0x80);
+ if($codepoint < 0x10000) return chr($codepoint >> 12 & 0x0f | 0xe0) .
+ chr($codepoint >> 6 & 0x3f | 0x80) .
+ chr($codepoint & 0x3f | 0x80);
+ if($codepoint < 0x110000) return chr($codepoint >> 18 & 0x07 | 0xf0) .
+ chr($codepoint >> 12 & 0x3f | 0x80) .
+ chr($codepoint >> 6 & 0x3f | 0x80) .
+ chr($codepoint & 0x3f | 0x80);
+
+ echo "Asked for code outside of range ($codepoint)\n";
+ die( -1 );
+}
+
+/**
+ * Take a series of space-separated hexadecimal numbers representing
+ * Unicode code points and return a UTF-8 string composed of those
+ * characters. Used by UTF-8 data generation and testing routines.
+ *
+ * @param $sequence String
+ * @return String
+ * @private
+ */
+function hexSequenceToUtf8( $sequence ) {
+ $utf = '';
+ foreach( explode( ' ', $sequence ) as $hex ) {
+ $n = hexdec( $hex );
+ $utf .= codepointToUtf8( $n );
+ }
+ return $utf;
+}
+
+/**
+ * Take a UTF-8 string and return a space-separated series of hex
+ * numbers representing Unicode code points. For debugging.
+ *
+ * @param $str String: UTF-8 string.
+ * @return string
+ * @private
+ */
+function utf8ToHexSequence( $str ) {
+ return rtrim( preg_replace( '/(.)/uSe',
+ 'sprintf("%04x ", utf8ToCodepoint("$1"))',
+ $str ) );
+}
+
+/**
+ * Determine the Unicode codepoint of a single-character UTF-8 sequence.
+ * Does not check for invalid input data.
+ *
+ * @param $char String
+ * @return Integer
+ * @public
+ */
+function utf8ToCodepoint( $char ) {
+ # Find the length
+ $z = ord( $char{0} );
+ if ( $z & 0x80 ) {
+ $length = 0;
+ while ( $z & 0x80 ) {
+ $length++;
+ $z <<= 1;
+ }
+ } else {
+ $length = 1;
+ }
+
+ if ( $length != strlen( $char ) ) {
+ return false;
+ }
+ if ( $length == 1 ) {
+ return ord( $char );
+ }
+
+ # Mask off the length-determining bits and shift back to the original location
+ $z &= 0xff;
+ $z >>= $length;
+
+ # Add in the free bits from subsequent bytes
+ for ( $i=1; $i<$length; $i++ ) {
+ $z <<= 6;
+ $z |= ord( $char{$i} ) & 0x3f;
+ }
+
+ return $z;
+}
+
+/**
+ * Escape a string for inclusion in a PHP single-quoted string literal.
+ *
+ * @param $string String: string to be escaped.
+ * @return String: escaped string.
+ * @public
+ */
+function escapeSingleString( $string ) {
+ return strtr( $string,
+ array(
+ '\\' => '\\\\',
+ '\'' => '\\\''
+ ));
+}
+
+?>
diff --git a/includes/proxy_check.php b/includes/proxy_check.php
new file mode 100644
index 00000000..fb7fdb50
--- /dev/null
+++ b/includes/proxy_check.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Command line script to check for an open proxy at a specified location
+ * @package MediaWiki
+ */
+
+if( php_sapi_name() != 'cli' ) {
+ die( 1 );
+}
+
+/**
+ *
+ */
+$output = '';
+
+/**
+ * Exit if there are not enough parameters, or if it's not command line mode
+ */
+if ( ( isset( $_REQUEST ) && array_key_exists( 'argv', $_REQUEST ) ) || count( $argv ) < 4 ) {
+ $output .= "Incorrect parameters\n";
+} else {
+ /**
+ * Get parameters
+ */
+ $ip = $argv[1];
+ $port = $argv[2];
+ $url = $argv[3];
+ $host = trim(`hostname`);
+ $output = "Connecting to $ip:$port, target $url, this hostname $host\n";
+
+ # Open socket
+ $sock = @fsockopen($ip, $port, $errno, $errstr, 5);
+ if ($errno == 0 ) {
+ $output .= "Connected\n";
+ # Send payload
+ $request = "GET $url HTTP/1.0\r\n";
+# $request .= "Proxy-Connection: Keep-Alive\r\n";
+# $request .= "Pragma: no-cache\r\n";
+# $request .= "Host: ".$url."\r\n";
+# $request .= "User-Agent: MediaWiki open proxy check\r\n";
+ $request .= "\r\n";
+ @fputs($sock, $request);
+ $response = fgets($sock, 65536);
+ $output .= $response;
+ @fclose($sock);
+ } else {
+ $output .= "No connection\n";
+ }
+}
+
+$output = escapeshellarg( $output );
+
+#`echo $output >> /home/tstarling/open/proxy.log`;
+
+?>
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
new file mode 100644
index 00000000..66368669
--- /dev/null
+++ b/includes/templates/Userlogin.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage Templates
+ */
+if( !defined( 'MEDIAWIKI' ) ) die( -1 );
+
+/** */
+require_once( 'includes/SkinTemplate.php' );
+
+/**
+ * HTML template for Special:Userlogin form
+ * @package MediaWiki
+ * @subpackage Templates
+ */
+class UserloginTemplate extends QuickTemplate {
+ function execute() {
+ if( $this->data['message'] ) {
+?>
+ <div class="<?php $this->text('messagetype') ?>box">
+ <?php if ( $this->data['messagetype'] == 'error' ) { ?>
+ <h2><?php $this->msg('loginerror') ?>:</h2>
+ <?php } ?>
+ <?php $this->html('message') ?>
+ </div>
+ <div class="visualClear"></div>
+<?php } ?>
+
+<div id="userloginForm">
+<form name="userlogin" method="post" action="<?php $this->text('action') ?>">
+ <h2><?php $this->msg('login') ?></h2>
+ <p id="userloginlink"><?php $this->html('link') ?></p>
+ <div id="userloginprompt"><?php $this->msgWiki('loginprompt') ?></div>
+ <?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
+ <table>
+ <tr>
+ <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?>:</label></td>
+ <td align='left'>
+ <input type='text' class='loginText' name="wpName" id="wpName1"
+ value="<?php $this->text('name') ?>" size='20' />
+ </td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?>:</label></td>
+ <td align='left'>
+ <input type='password' class='loginPassword' name="wpPassword" id="wpPassword1"
+ value="<?php $this->text('password') ?>" size='20' />
+ </td>
+ </tr>
+ <?php if( $this->data['usedomain'] ) {
+ $doms = "";
+ foreach( $this->data['domainnames'] as $dom ) {
+ $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
+ }
+ ?>
+ <tr>
+ <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td>
+ <td align='left'>
+ <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>">
+ <?php echo $doms ?>
+ </select>
+ </td>
+ </tr>
+ <?php } ?>
+ <tr>
+ <td></td>
+ <td align='left'>
+ <input type='checkbox' name="wpRemember"
+ value="1" id="wpRemember"
+ <?php if( $this->data['remember'] ) { ?>checked="checked"<?php } ?>
+ /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align='left' style="white-space:nowrap">
+ <input type='submit' name="wpLoginattempt" id="wpLoginattempt" value="<?php $this->msg('login') ?>" />&nbsp;<?php if( $this->data['useemail'] ) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword"
+ value="<?php $this->msg('mailmypassword') ?>" />
+ <?php } ?>
+ </td>
+ </tr>
+ </table>
+<?php if( @$this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
+</form>
+</div>
+<div id="loginend"><?php $this->msgWiki( 'loginend' ); ?></div>
+<?php
+
+ }
+}
+
+class UsercreateTemplate extends QuickTemplate {
+ function execute() {
+ if( $this->data['message'] ) {
+?>
+ <div class="<?php $this->text('messagetype') ?>box">
+ <?php if ( $this->data['messagetype'] == 'error' ) { ?>
+ <h2><?php $this->msg('loginerror') ?>:</h2>
+ <?php } ?>
+ <?php $this->html('message') ?>
+ </div>
+ <div class="visualClear"></div>
+<?php } ?>
+<div id="userlogin">
+
+<form name="userlogin2" id="userlogin2" method="post" action="<?php $this->text('action') ?>">
+ <h2><?php $this->msg('createaccount') ?></h2>
+ <p id="userloginlink"><?php $this->html('link') ?></p>
+ <?php $this->html('header'); /* pre-table point for form plugins... */ ?>
+ <?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
+ <table>
+ <tr>
+ <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?>:</label></td>
+ <td align='left'>
+ <input type='text' class='loginText' name="wpName" id="wpName2"
+ value="<?php $this->text('name') ?>" size='20' />
+ </td>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?>:</label></td>
+ <td align='left'>
+ <input type='password' class='loginPassword' name="wpPassword" id="wpPassword2"
+ value="<?php $this->text('password') ?>" size='20' />
+ </td>
+ </tr>
+ <?php if( $this->data['usedomain'] ) {
+ $doms = "";
+ foreach( $this->data['domainnames'] as $dom ) {
+ $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
+ }
+ ?>
+ <tr>
+ <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td>
+ <td align='left'>
+ <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>">
+ <?php echo $doms ?>
+ </select>
+ </td>
+ </tr>
+ <?php } ?>
+ <tr>
+ <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?>:</label></td>
+ <td align='left'>
+ <input type='password' class='loginPassword' name="wpRetype" id="wpRetype"
+ value="<?php $this->text('retype') ?>"
+ size='20' />
+ </td>
+ </tr>
+ <tr>
+ <?php if( $this->data['useemail'] ) { ?>
+ <td align='right'><label for='wpEmail'><?php $this->msg('youremail') ?>:</label></td>
+ <td align='left'>
+ <input type='text' class='loginText' name="wpEmail" id="wpEmail"
+ value="<?php $this->text('email') ?>" size='20' />
+ </td>
+ <?php } ?>
+ <?php if( $this->data['userealname'] ) { ?>
+ </tr>
+ <tr>
+ <td align='right'><label for='wpRealName'><?php $this->msg('yourrealname') ?>:</label></td>
+ <td align='left'>
+ <input type='text' class='loginText' name="wpRealName" id="wpRealName"
+ value="<?php $this->text('realname') ?>" size='20' />
+ </td>
+ <?php } ?>
+ </tr>
+ <tr>
+ <td></td>
+ <td align='left'>
+ <input type='checkbox' name="wpRemember"
+ value="1" id="wpRemember"
+ <?php if( $this->data['remember'] ) { ?>checked="checked"<?php } ?>
+ /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align='left'>
+ <input type='submit' name="wpCreateaccount" id="wpCreateaccount"
+ value="<?php $this->msg('createaccount') ?>" />
+ <?php if( $this->data['createemail'] ) { ?>
+ <input type='submit' name="wpCreateaccountMail" id="wpCreateaccountMail"
+ value="<?php $this->msg('createaccountmail') ?>" />
+ <?php } ?>
+ </td>
+ </tr>
+ </table>
+ <?php
+
+ if ($this->data['userealname'] || $this->data['useemail']) {
+ echo '<div id="login-sectiontip">';
+ if ( $this->data['useemail'] ) {
+ echo '<div>';
+ $this->msgHtml('prefs-help-email');
+ echo '</div>';
+ }
+ if ( $this->data['userealname'] ) {
+ echo '<div>';
+ $this->msgHtml('prefs-help-realname');
+ echo '</div>';
+ }
+ echo '</div>';
+ }
+
+ ?>
+<?php if( @$this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
+</form>
+</div>
+<div id="signupend"><?php $this->msgWiki( 'signupend' ); ?></div>
+<?php
+
+ }
+}
+
+?>
diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile
new file mode 100644
index 00000000..30679fbb
--- /dev/null
+++ b/includes/zhtable/Makefile
@@ -0,0 +1,268 @@
+#
+# Creating the file ZhConversion.php used for Simplified/Traditional
+# Chinese conversion. It gets the basic conversion table from the Unihan
+# database, and construct the phrase tables using phrase libraries in
+# the SCIM packages and the libtabe package. There are also special
+# tables used to for adjustment.
+#
+
+GREP = LANG=zh_CN.UTF8 grep
+SED = LANG=zh_CN.UTF8 sed
+DIFF = LANG=zh_CN.UTF8 diff
+CC ?= gcc
+
+#installation directory
+INSTDIR = /usr/local/share/zhdaemons/
+
+all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toCN.dict toTW.dict toHK.dict toSG.dict
+
+Unihan.txt:
+ wget -nc ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip
+ unzip -q Unihan.zip
+
+EZ.txt.in:
+ wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-tables-0.5.1.tar.gz
+ tar -xzf scim-tables-0.5.1.tar.gz -O scim-tables-0.5.1/zh/EZ.txt.in > EZ.txt.in
+
+phrase_lib.txt:
+ wget -nc http://easynews.dl.sourceforge.net/sourceforge/scim/scim-pinyin-0.5.0.tar.gz
+ tar -xzf scim-pinyin-0.5.0.tar.gz -O scim-pinyin-0.5.0/data/phrase_lib.txt > phrase_lib.txt
+
+tsi.src:
+ wget -nc http://unc.dl.sourceforge.net/sourceforge/libtabe/libtabe-0.2.3.tgz
+ tar -xzf libtabe-0.2.3.tgz -O libtabe/tsi-src/tsi.src > tsi.src
+
+wordlist: phrase_lib.txt EZ.txt.in tsi.src
+ iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/# //g' | $(SED) 's/[ ][0-9].*//' > wordlist
+ $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >>wordlist
+ $(SED) '1,/BEGIN_TABLE/d' EZ.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" >> wordlist
+ sort wordlist | uniq | $(SED) 's/ //g' > t
+ mv t wordlist
+
+printutf8: printutf8.c
+ $(CC) -o printutf8 printutf8.c
+
+unihan.t2s.t: Unihan.txt printutf8
+ $(GREP) kSimplifiedVariant Unihan.txt | $(SED) '/#/d' | $(SED) 's/kSimplifiedVariant//' | ./printutf8 > unihan.t2s.t
+
+trad2simp.t: trad2simp.manual unihan.t2s.t
+ cp unihan.t2s.t tmp1
+ for I in `colrm 11 < trad2simp.manual` ; do $(SED) "/^$$I/d" tmp1 > tmp2; mv tmp2 tmp1; done
+ cat trad2simp.manual tmp1 > trad2simp.t
+
+unihan.s2t.t: Unihan.txt printutf8
+ $(GREP) kTraditionalVariant Unihan.txt | $(SED) '/#/d' | $(SED) 's/kTraditionalVariant//' | ./printutf8 > unihan.s2t.t
+
+simp2trad.t: unihan.s2t.t simp2trad.manual
+ cp unihan.s2t.t tmp1
+ for I in `colrm 11 < simp2trad.manual` ; do $(SED) "/^$$I/d" tmp1 > tmp2; mv tmp2 tmp1; done
+ cat simp2trad.manual tmp1 > simp2trad.t
+
+t2s_1tomany.t: trad2simp.t
+ $(GREP) -s ".\{19,\}" trad2simp.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > t2s_1tomany.t
+
+t2s_1to1.t: trad2simp.t s2t_1tomany.t
+ $(SED) "/.*|.*|.*|.*/d" trad2simp.t | $(SED) 's/U+[0-9a-z][0-9a-z]*/"/' | $(SED) 's/|U+[0-9a-z][0-9a-z]*/"=>"/' | $(SED) 's/|/",/' > t2s_1to1.t
+ $(GREP) '"."=>"..",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
+ $(GREP) '"."=>"...",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\).",/"\2"=>\1,/' >> t2s_1to1.t
+ $(GREP) '"."=>"...",' s2t_1tomany.t | $(SED) 's/\("."\)=>"..\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
+ $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\)..",/"\2"=>\1,/' >> t2s_1to1.t
+ $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>"..\(.\).",/"\2"=>\1,/' >> t2s_1to1.t
+ $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>"...\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
+ sort t2s_1to1.t | uniq > t
+ mv t t2s_1to1.t
+
+
+s2t_1tomany.t: simp2trad.t
+ $(GREP) -s ".\{19,\}" simp2trad.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > s2t_1tomany.t
+
+s2t_1to1.t: simp2trad.t t2s_1tomany.t
+ $(SED) "/.*|.*|.*|.*/d" simp2trad.t | $(SED) 's/U+[0-9a-z][0-9a-z]*/"/' | $(SED) 's/|U+[0-9a-z][0-9a-z]*/"=>"/' | $(SED) 's/|/",/' > s2t_1to1.t
+ $(GREP) '"."=>"..",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
+ $(GREP) '"."=>"...",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\).",/"\2"=>\1,/' >> s2t_1to1.t
+ $(GREP) '"."=>"...",' t2s_1tomany.t | $(SED) 's/\("."\)=>"..\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
+ $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\)..",/"\2"=>\1,/' >> s2t_1to1.t
+ $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>"..\(.\).",/"\2"=>\1,/' >> s2t_1to1.t
+ $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>"...\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
+ sort s2t_1to1.t | uniq > t
+ mv t s2t_1to1.t
+
+tphrase.t: EZ.txt.in tsi.src
+ colrm 1 8 < EZ.txt.in | $(SED) 's/\t//g' | $(GREP) "^.\{2,4\}[0-9]" | $(SED) 's/[0-9]//g' > t
+ iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/ [0-9].*//g' | $(SED) 's/[# ]//g'| $(GREP) "^.\{2,4\}" >> t
+ sort t | uniq > tphrase.t
+
+alltradphrases.t: tphrase.t s2t_1tomany.t
+ for i in `cat s2t_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' |$(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' |sort | uniq`; do $(GREP) -s $$i tphrase.t ; done > alltradphrases.t || true
+
+
+tradphrases_2.t: alltradphrases.t
+ cat alltradphrases.t | $(GREP) "^..$$" | sort | uniq > tradphrases_2.t
+
+tradphrases_3.t: alltradphrases.t
+ cat alltradphrases.t | $(GREP) "^...$$" | sort | uniq > tradphrases_3.t
+ for i in `cat tradphrases_2.t`; do $(GREP) $$i tradphrases_3.t ; done | sort | uniq > t3 || true
+ $(DIFF) t3 tradphrases_3.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t tradphrases_3.t
+
+
+tradphrases_4.t: alltradphrases.t
+ cat alltradphrases.t | $(GREP) "^....$$" | sort | uniq > tradphrases_4.t
+ for i in `cat tradphrases_2.t`; do $(GREP) $$i tradphrases_4.t ; done | sort | uniq > t3 || true
+ $(DIFF) t3 tradphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t tradphrases_4.t
+ for i in `cat tradphrases_3.t`; do $(GREP) $$i tradphrases_4.t ; done | sort | uniq > t3 || true
+ $(DIFF) t3 tradphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t tradphrases_4.t
+
+tradphrases.t: tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
+ cat tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t |sort | uniq > tradphrases.t
+ for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i tradphrases.t ; done | $(DIFF) tradphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
+ mv t tradphrases.t
+
+tradphrases.notsure: tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
+ cat tradphrases_2.t tradphrases_3.t tradphrases_4.t |sort | uniq > t
+ for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i t; done | $(DIFF) t - | $(GREP) '>' | $(SED) 's/> //' > tradphrases.notsure
+
+
+ph.t: phrase_lib.txt
+ $(SED) 's/[\t0-9a-zA-Z]//g' phrase_lib.txt | $(GREP) "^.\{2,4\}$$" > ph.t
+
+allsimpphrases.t: ph.t
+ rm -f allsimpphrases.t
+ for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i ph.t >> allsimpphrases.t; done
+
+simpphrases_2.t: allsimpphrases.t
+ cat allsimpphrases.t | $(GREP) "^..$$" | sort | uniq > simpphrases_2.t
+
+simpphrases_3.t: allsimpphrases.t
+ cat allsimpphrases.t | $(GREP) "^...$$" | sort | uniq > simpphrases_3.t
+ for i in `cat simpphrases_2.t`; do $(GREP) $$i simpphrases_3.t ; done | sort | uniq > t3 || true
+ $(DIFF) t3 simpphrases_3.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t simpphrases_3.t
+
+simpphrases_4.t: allsimpphrases.t
+ cat allsimpphrases.t | $(GREP) "^....$$" | sort | uniq > simpphrases_4.t
+ rm -f t
+ for i in `cat simpphrases_2.t`; do $(GREP) $$i simpphrases_4.t >> t; done || true
+ sort t | uniq > t3
+ $(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t simpphrases_4.t
+ for i in `cat simpphrases_3.t`; do $(GREP) $$i simpphrases_4.t; done | sort | uniq > t3 || true
+ $(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
+ mv t simpphrases_4.t
+
+simpphrases.t:simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
+ cat simpphrases_2.t simpphrases_3.t simpphrases_4.t > simpphrases.t
+ for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i simpphrases.t ; done | $(DIFF) simpphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
+ mv t simpphrases.t
+
+
+simpphrases.notsure:simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
+ cat simpphrases_2.t simpphrases_3.t simpphrases_4.t > t
+ for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i t ; done | $(DIFF) t - | $(GREP) '>' | $(SED) 's/> //' > simpphrases.notsure
+
+trad2simp1to1.t: t2s_1tomany.t t2s_1to1.t
+ $(SED) 's/\(.......\).*/\1",/' t2s_1tomany.t > trad2simp1to1.t
+ cat t2s_1to1.t >> trad2simp1to1.t
+
+simp2trad1to1.t: s2t_1tomany.t s2t_1to1.t
+ $(SED) 's/\(.......\).*/\1",/' s2t_1tomany.t > simp2trad1to1.t
+ cat s2t_1to1.t >> simp2trad1to1.t
+
+trad2simp.php: trad2simp1to1.t tradphrases.t
+ printf '<?php\n$$trad2simp=array(' > trad2simp.php
+ cat trad2simp1to1.t >> trad2simp.php
+ printf ');\n$$str=\n"' >> trad2simp.php
+ cat tradphrases.t >> trad2simp.php
+ printf '";\n$$t=strtr($$str, $$trad2simp);\necho $$t;\n?>' >> trad2simp.php
+
+simp2trad.php: simp2trad1to1.t simpphrases.t
+ printf '<?php\n$$simp2trad=array(' > simp2trad.php
+ cat simp2trad1to1.t >> simp2trad.php
+ printf ');\n$$str=\n"' >> simp2trad.php
+ cat simpphrases.t >> simp2trad.php
+ printf '";\n$$t=strtr($$str, $$simp2trad);\necho $$t;\n?>' >> simp2trad.php
+
+simp2trad.phrases.t: trad2simp.php tradphrases.t toTW.manual
+ php -f trad2simp.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
+ cat tradphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
+ paste tmp1 tmp2 > simp2trad.phrases.t
+ $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toTW.manual >> simp2trad.phrases.t
+
+trad2simp.phrases.t: simp2trad.php simpphrases.t toCN.manual
+ php -f simp2trad.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
+ cat simpphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
+ paste tmp1 tmp2 > trad2simp.phrases.t
+ $(SED) 's/\(.*\)\t\(.*\)/"\1"=>"\2",/' toCN.manual >> trad2simp.phrases.t
+
+toCN.dict: trad2simp1to1.t trad2simp.phrases.t
+ cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toCN.dict
+ cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toCN.dict
+
+toTW.dict: simp2trad1to1.t simp2trad.phrases.t
+ cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toTW.dict
+ cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toTW.dict
+
+toHK.dict: toHK.manual
+ cat toHK.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toHK.dict
+
+toSG.dict: toSG.manual
+ cat toSG.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toSG.dict
+
+
+
+ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toHK.manual toSG.manual
+ printf '<?php\n/**\n * Simplified/Traditional Chinese conversion tables\n' > ZhConversion.php
+ printf ' *\n * Automatically generated using code and data in includes/zhtable/\n' >> ZhConversion.php
+ printf ' * Do not modify directly! \n *\n * @package MediaWiki\n*/\n\n' >> ZhConversion.php
+ printf '$$zh2TW=array(\n' >> ZhConversion.php
+ cat simp2trad1to1.t >> ZhConversion.php
+ echo >> ZhConversion.php
+ cat simp2trad.phrases.t >> ZhConversion.php
+ echo >> ZhConversion.php
+ echo ');' >> ZhConversion.php
+ echo >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf '$$zh2CN=array(\n' >> ZhConversion.php
+ cat trad2simp1to1.t >> ZhConversion.php
+ echo >> ZhConversion.php
+ cat trad2simp.phrases.t >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf ');' >> ZhConversion.php
+ echo >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf '$$zh2HK=array(\n' >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toHK.manual >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf ');' >> ZhConversion.php
+ echo >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf '$$zh2SG=array(\n' >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSG.manual >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf ');' >> ZhConversion.php
+ echo >> ZhConversion.php
+ printf '?>' >> ZhConversion.php
+
+
+clean: cleantmp cleandl
+
+cleantmp:
+ # Stuff unpacked from the files fetched by wget
+ rm -f \
+ Unihan.txt \
+ EZ.txt.in \
+ phrase_lib.txt \
+ tsi.src
+ # Temporary files and other trash
+ rm -f ZhConversion.php tmp1 tmp2 tmp3 t3 *.t trad2simp.php simp2trad.php *.dict printutf8 *~ \
+ simpphrases.notsure tradphrases.notsure wordlist
+
+cleandl:
+ rm -f \
+ Unihan.zip \
+ scim-tables-0.5.1.tar.gz \
+ scim-pinyin-0.5.0.tar.gz \
+ libtabe-0.2.3.tgz
+
diff --git a/includes/zhtable/README b/includes/zhtable/README
new file mode 100644
index 00000000..94dd341d
--- /dev/null
+++ b/includes/zhtable/README
@@ -0,0 +1,16 @@
+The various .manual files contains special mappings not included in the
+unihan database, and phrases not included in the SCIM package.
+
+- simp2trad.manual: Simplified to Traditional character mapping. Most
+ data adapted from
+
+ 冯寿忠,“非对称繁简字”对照表, 《语文建设通讯》1997-9第53期.
+ /http://www.yywzw.com/jt/feng/fengb01.htm
+
+- tradphrases.manual: Phrases in Traditional Chinese. A portition is obtained
+ from the TongWen package (http://tongwen.mozdev.org/)
+
+- toTW.manual, toCN.manual, toSG.manual and toHK.manual: special phrase
+ mappings.
+
+zhengzhu at gmail.com \ No newline at end of file
diff --git a/includes/zhtable/printutf8.c b/includes/zhtable/printutf8.c
new file mode 100644
index 00000000..b6ccf17c
--- /dev/null
+++ b/includes/zhtable/printutf8.c
@@ -0,0 +1,99 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+/*
+ Unicode UTF8
+0x00000000 - 0x0000007F: 0xxxxxxx
+0x00000080 - 0x000007FF: 110xxx xx 10xx xxxx
+0x00000800 - 0x0000FFFF: 1110xxxx 10xxxx xx 10xx xxxx
+0x00010000 - 0x001FFFFF: 11110x xx 10xx xxxx 10xxxx xx 10xx xxxx
+0x00200000 - 0x03FFFFFF: 111110xx 10xxxx xx 10xx xxxx 10xxxx xx 10xx xxxx
+0x04000000 - 0x7FFFFFFF: 1111110x 10xx xxxx 10xxxx xx 10xx xxxx 10xxxx xx 10xx xxxx
+
+0000 0 1001 9
+0001 1 1010 A
+0010 2 1011 B
+0011 3 1100 C
+0100 4 1101 D
+0101 5 1110 E
+0110 6 1111 F
+0111 7
+1000 8
+*/
+void printUTF8(long long u) {
+ long long m;
+ if(u<0x80) {
+ printf("%c", (unsigned char)u);
+ }
+ else if(u<0x800) {
+ m = ((u&0x7c0)>>6) | 0xc0;
+ printf("%c", (unsigned char)m);
+ m = (u&0x3f) | 0x80;
+ printf("%c", (unsigned char)m);
+ }
+ else if(u<0x10000) {
+ m = ((u&0xf000)>>12) | 0xe0;
+ printf("%c",(unsigned char)m);
+ m = ((u&0xfc0)>>6) | 0x80;
+ printf("%c",(unsigned char)m);
+ m = (u & 0x3f) | 0x80;
+ printf("%c",(unsigned char)m);
+ }
+ else if(u<0x200000) {
+ m = ((u&0x1c0000)>>18) | 0xf0;
+ printf("%c", (unsigned char)m);
+ m = ((u& 0x3f000)>>12) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u& 0xfc0)>>6) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = (u&0x3f) | 0x80;
+ printf("%c", (unsigned char)m);
+ }
+ else if(u<0x4000000){
+ m = ((u&0x3000000)>>24) | 0xf8;
+ printf("%c", (unsigned char)m);
+ m = ((u&0xfc0000)>>18) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u&0x3f000)>>12) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u&0xfc00)>>6) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = (u&0x3f) | 0x80;
+ printf("%c", (unsigned char)m);
+ }
+ else {
+ m = ((u&0x40000000)>>30) | 0xfc;
+ printf("%c", (unsigned char)m);
+ m = ((u&0x3f000000)>>24) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u&0xfc0000)>>18) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u&0x3f000)>>12) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = ((u&0xfc0)>>6) | 0x80;
+ printf("%c", (unsigned char)m);
+ m = (u&0x3f)| 0x80;
+ printf("%c", (unsigned char)m);
+ }
+}
+
+int main() {
+ int i,j;
+ long long n1, n2;
+ unsigned char b1[15], b2[15];
+ unsigned char buf[1024];
+ i=0;
+ while(fgets(buf, 1024, stdin)) {
+ // printf("read %s\n", buf);
+ for(i=0;i<strlen(buf); i++)
+ if(buf[i]=='U') {
+ if(buf[i+1] == '+') {
+ n1 = strtoll(buf+i+2,0,16);
+ printf("U+%05x", n1);
+ printUTF8(n1);printf("|");
+ }
+ }
+ printf("\n");
+ }
+}
+
diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual
new file mode 100644
index 00000000..b5e1c3ae
--- /dev/null
+++ b/includes/zhtable/simp2trad.manual
@@ -0,0 +1,178 @@
+U+0753b画|U+0756b畫|U+07575畵|
+U+0677f板|U+0677f板|U+095c6闆|
+U+08868表|U+08868表|U+09336錶|
+U+0624d才|U+0624d才|U+07e94纔|
+U+04e11丑|U+0919c醜|U+04e11丑|
+U+051fa出|U+051fa出|U+09f63齣|
+U+06dc0淀|U+06fb1澱|U+06dc0淀|
+U+051ac冬|U+051ac冬|U+09f15鼕|
+U+08303范|U+07bc4範|U+08303范|
+U+04e30丰|U+08c50豐|U+04e30丰|
+U+0522e刮|U+0522e刮|U+098b3颳|
+U+0540e后|U+05f8c後|U+0540e后|
+U+080e1胡|U+080e1胡|U+09b0d鬍|U+0885a衚|
+U+056de回|U+056de回|U+08ff4迴|
+U+04f19伙|U+05925夥|U+04f19伙|
+U+059dc姜|U+08591薑|U+059dc姜|
+U+0501f借|U+0501f借|U+085c9藉|
+U+0514b克|U+0514b克|U+0524b剋|
+U+056f0困|U+056f0困|U+0774f睏|
+U+06f13漓|U+06f13漓|U+07055灕|
+U+091cc里|U+091cc里|U+088e1裡|U+088cf裏|
+U+05e18帘|U+07c3e簾|U+05e18帘|
+U+09709霉|U+09709霉|U+09ef4黴|
+U+09762面|U+09762面|U+09eb5麵|
+U+08511蔑|U+08511蔑|U+0884a衊|
+U+05343千|U+05343千|U+097c6韆|
+U+079cb秋|U+079cb秋|U+097a6鞦|
+U+0677e松|U+0677e松|U+09b06鬆|
+U+054b8咸|U+054b8咸|U+09e79鹹|
+U+05411向|U+05411向|U+056ae嚮|U+066cf曏|
+U+04f59余|U+09918餘|U+04f59余|
+U+090c1郁|U+09b31鬱|U+090c1郁|
+U+05fa1御|U+05fa1御|U+079a6禦|
+U+0613f愿|U+09858願|U+0613f愿|
+U+04e91云|U+096f2雲|U+04e91云|
+U+082b8芸|U+082b8芸|U+08553蕓|
+U+06c84沄|U+06c84沄|U+06f90澐|
+U+081f4致|U+081f4致|U+07dfb緻|
+U+05236制|U+05236制|U+088fd製|
+U+06731朱|U+06731朱|U+07843硃|
+U+07b51筑|U+07bc9築|U+07b51筑|
+U+051c6准|U+06e96準|U+051c6准|
+U+05382厂|U+05ee0廠|U+05382厂|
+U+05e7f广|U+05ee3廣|U+05e7f广|
+U+08f9f辟|U+095e2闢|U+08f9f辟|
+U+0522b别|U+05225別|U+05f46彆|
+U+0535c卜|U+0535c卜|U+08514蔔|
+U+06c88沈|U+06c88沈|U+0700b瀋|
+U+051b2冲|U+06c96沖|U+0885d衝|
+U+079cd种|U+07a2e種|U+079cd种|
+U+0866b虫|U+087f2蟲|U+0866b虫|
+U+062c5担|U+064d4擔|U+062c5担|
+U+0515a党|U+09ee8黨|U+0515a党|
+U+06597斗|U+09b25鬥|U+06597斗|
+U+0513f儿|U+05152兒|U+0513f儿|
+U+05e72干|U+04e7e乾|U+05e79幹|U+05e72干|U+069a6%G榦%@|
+U+08c37谷|U+08c37谷|U+07a40穀|
+U+067dc柜|U+06ac3櫃|U+067dc柜|
+U+05408合|U+05408合|U+095a4閤|
+U+05212划|U+05283劃|U+05212划|
+U+0574f坏|U+058de壞|U+0574f坏|
+U+051e0几|U+05e7e幾|U+051e0几|
+U+07cfb系|U+07cfb系|U+07e6b繫|U+04fc2係|
+U+05bb6家|U+05bb6家|U+050a2傢|
+U+04ef7价|U+050f9價|U+04ef7价|
+U+0636e据|U+064da據|U+0636e据|
+U+05377卷|U+06372捲|U+05377卷|
+U+09002适|U+09069適|U+09002适|
+U+08721蜡|U+0881f蠟|U+08721蜡|
+U+0814a腊|U+081d8臘|U+0814a腊|
+U+04e86了|U+04e86了|U+077ad瞭|
+U+07d2f累|U+07d2f累|U+07e8d纍|
+U+04e48么|U+09ebd麽|U+04e48么|U+05e7a幺|U+09ebc麼|
+U+08499蒙|U+08499蒙|U+077c7矇|U+06fdb濛|U+061de懞|
+U+04e07万|U+0842c萬|U+04e07万|
+U+05b81宁|U+05be7寧|U+05b81宁|
+U+06734朴|U+06a38樸|U+06734朴|
+U+082f9苹|U+0860b蘋|U+082f9苹|
+U+04ec6仆|U+050d5僕|U+04ec6仆|
+U+066f2曲|U+066f2曲|U+09eaf麯|
+U+0786e确|U+078ba確|U+0786e确|
+U+0820d舍|U+0820d舍|U+06368捨|
+U+080dc胜|U+052dd勝|U+080dc胜|
+U+0672f术|U+08853術|U+0672f术|U+0672e朮|
+U+053f0台|U+053f0台|U+081fa臺|U+06aaf檯|U+098b1颱|
+U+04f53体|U+09ad4體|U+04f53体|
+U+06d82涂|U+05857塗|U+06d82涂|
+U+053f6叶|U+08449葉|U+053f6叶|
+U+05401吁|U+05401吁|U+07c72籲|
+U+065cb旋|U+065cb旋|U+0955f镟|
+U+04f63佣|U+050ad傭|U+04f63佣|
+U+04e0e与|U+08207與|U+04e0e与|
+U+06298折|U+06298折|U+0647a摺|
+U+05f81征|U+05fb5徵|U+05f81征|
+U+075c7症|U+075c7症|U+07665癥|
+U+06076恶|U+060e1惡|U+05641噁|
+U+053d1发|U+0767c發|U+09aee髮|
+U+0590d复|U+05fa9復|U+08907複|U+08986覆|
+U+06c47汇|U+0532f匯|U+05f59彙|
+U+083b7获|U+07372獲|U+07a6b穫|
+U+09965饥|U+098e2飢|U+09951饑|
+U+05c3d尽|U+076e1盡|U+05118儘|
+U+05386历|U+06b77歷|U+066c6曆|
+U+05364卤|U+06ef7滷|U+09e75鹵|
+U+05f25弥|U+05f4c彌|U+07030瀰|
+U+07b7e签|U+07c3d簽|U+07c56籖|
+U+07ea4纤|U+07e96纖|U+07e34縴|
+U+082cf苏|U+08607蘇|U+056cc囌|
+U+0575b坛|U+058c7壇|U+07f48罈|
+U+056e2团|U+05718團|U+07cf0糰|
+U+0987b须|U+09808須|U+09b1a鬚|
+U+0810f脏|U+081df臟|U+09ad2髒|
+U+053ea只|U+053ea只|U+096bb隻|
+U+0949f钟|U+09418鐘|U+0937e鍾|
+U+0836f药|U+085e5藥|U+0846f葯|
+U+0540c同|U+0540c同|U+08855衕|
+U+05fd7志|U+05fd7志|U+08a8c誌|
+U+0676f杯|U+0676f杯|U+076c3盃|
+U+05cb3岳|U+05cb3岳|U+05dbd嶽|
+U+05e03布|U+05e03布|U+04f48佈|
+U+05f53当|U+07576當|U+05679噹|
+U+0540a吊|U+05f14弔|U+0540a吊|
+U+04ec7仇|U+04ec7仇|U+08b8e讎|
+U+08574蕴|U+0860a蘊|U+085f4藴|
+U+07ebf线|U+07dda線|U+07dab綫|
+U+04e3a为|U+070ba為|U+07232爲|
+U+04ea7产|U+07522產|U+07523産|
+U+04f17众|U+0773e眾|U+08846衆|
+U+04f2a伪|U+0507d偽|U+050de僞|
+U+051eb凫|U+09ce7鳧|U+09cec鳬|
+U+05395厕|U+05ec1廁|U+053a0厠|
+U+0542f启|U+0555f啟|U+05553啓|
+U+05899墙|U+07246牆|U+058bb墻|
+U+058f3壳|U+06bbc殼|U+06bbb殻|
+U+05956奖|U+0734e獎|U+0596c奬|
+U+059ab妫|U+05aaf媯|U+05b00嬀|
+U+05e76并|U+04e26並|U+04f75併|
+U+05f55录|U+09304錄|U+09332録|
+U+060ab悫|U+06128愨|U+06164慤|
+U+06781极|U+06975極|U+06781极|
+U+06ca9沩|U+06e88溈|U+06f59潙|
+U+07618瘘|U+0763a瘺|U+0763b瘻|
+U+07877硷|U+09e7c鹼|U+07906礆|
+U+07ad6竖|U+08c4e豎|U+07aea竪|
+U+07edd绝|U+07d55絕|U+07d76絶|
+U+07ee3绣|U+07e61繡|U+07d89綉|
+U+07ee6绦|U+07d5b絛|U+07e27縧|
+U+07ef1绱|U+07dd4緔|U+0979d鞝|
+U+07ef7绷|U+07db3綳|U+07e43繃|
+U+07eff绿|U+07da0綠|U+07dd1緑|
+U+07f30缰|U+097c1韁|U+07e6e繮|
+U+082e7苧|U+082ce苎|U+085b4薴|
+U+083bc莼|U+08493蒓|U+084f4蓴|
+U+08bf4说|U+08aaa說|U+08aac説|
+U+08c23谣|U+08b20謠|U+08b21謡|
+U+08c2b谫|U+08b7e譾|U+08b2d謭|
+U+08d43赃|U+08d13贓|U+08d1c贜|
+U+08d4d赍|U+09f4e齎|U+08ceb賫|
+U+08d5d赝|U+08d17贗|U+08d0b贋|
+U+0915d酝|U+0919e醞|U+09196醖|
+U+091c7采|U+063a1採|U+091c7采|U+057f0埰|
+U+094a9钩|U+09264鉤|U+0920e鈎|
+U+094b5钵|U+07f3d缽|U+09262鉢|
+U+09508锈|U+092b9銹|U+093fd鏽|
+U+09510锐|U+092b3銳|U+092ed鋭|
+U+09528锨|U+06774杴|U+09341鍁|
+U+0954c镌|U+0942b鐫|U+093b8鎸|
+U+09562镢|U+09481钁|U+0941d鐝|
+U+09605阅|U+095b1閱|U+095b2閲|
+U+09893颓|U+09839頹|U+0983d頽|
+U+0989c颜|U+0984f顏|U+09854顔|
+U+09980馀|U+09918餘|
+U+09a82骂|U+07f75罵|U+099e1駡|
+U+09c87鲇|U+09bf0鯰|U+09b8e鮎|
+U+09c9e鲞|U+09bd7鯗|U+09b9d鮝|
+U+09cc4鳄|U+09c77鱷|U+09c10鰐|
+U+09e21鸡|U+096de雞|U+09dc4鷄|
+U+09e5a鹚|U+09dbf鶿|U+09dc0鷀|
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
new file mode 100644
index 00000000..caff9c14
--- /dev/null
+++ b/includes/zhtable/toCN.manual
@@ -0,0 +1,331 @@
+記憶體 内存
+預設 默认
+預設 缺省
+串列 串行
+乙太網 以太网
+點陣圖 位图
+常式 例程
+通道 信道
+游標 光标
+光碟 光盘
+光碟機 光驱
+全形 全角
+共用 共享
+相容 兼容
+首碼 前缀
+尾碼 后缀
+載入 加载
+半形 半角
+變數 变量
+雜訊 噪声
+因數 因子
+線上 在线
+離線 脱机
+功能變數名稱 域名
+音效卡 声卡
+字型大小 字号
+字型檔 字库
+欄位 字段
+字元 字符
+存檔 存盘
+定址 寻址
+章節附註 尾注
+非同步 异步
+匯流排 总线
+括弧 括号
+介面 接口
+控制項 控件
+許可權 权限
+碟片 盘片
+矽片 硅片
+矽谷 硅谷
+硬碟 硬盘
+磁碟 磁盘
+磁軌 磁道
+程式控制 程控
+埠 端口
+運算元 算子
+演算法 算法
+晶片 芯片
+晶元 芯片
+片語 词组
+解碼 译码
+軟碟機 软驱
+快閃記憶體 闪存
+滑鼠 鼠标
+進位 进制
+互動式 交互式
+模擬 仿真
+優先順序 优先级
+感測 传感
+攜帶型 便携式
+資訊理論 信息论
+迴圈 循环
+防寫 写保护
+分散式 分布式
+解析度 分辨率
+程式 程序
+伺服器 服务器
+等於 等于
+區域網 局域网
+上傳 上载
+電腦 计算机
+巨集 宏
+掃瞄器 扫瞄仪
+寬頻 宽带
+視窗 窗口
+資料庫 数据库
+西曆 公历
+乳酪 奶酪
+鉅賈 巨商
+手電筒 手电
+萬曆 万历
+永曆 永历
+辭彙 词汇
+保全 保安
+慣用 习用
+母音 元音
+自由球 任意球
+頭槌 头球
+進球 入球
+顆進球 粒入球
+射門 打门
+蓋火鍋 火锅盖帽
+印表機 打印机
+打印機 打印机
+位元組 字节
+字節 字节
+列印 打印
+打印 打印
+硬體 硬件
+二極體 二极管
+二極管 二极管
+三極體 三极管
+三極管 三极管
+數位 数码
+數碼 数码
+軟體 软件
+軟件 软件
+網路 网络
+網絡 网络
+人工智慧 人工智能
+太空梭 航天飞机
+穿梭機 航天飞机
+網際網路 因特网
+互聯網 因特网
+機械人 机器人
+機器人 机器人
+行動電話 移动电话
+流動電話 移动电话
+調制解調器 调制解调器
+數據機 调制解调器
+短訊 短信
+簡訊 短信
+烏茲別克 乌兹别克斯坦
+查德 乍得
+乍得 乍得
+也門
+葉門 也门
+伯利茲 伯利兹
+貝里斯 伯利兹
+維德角 佛得角
+佛得角 佛得角
+克羅地亞 克罗地亚
+克羅埃西亞 克罗地亚
+岡比亞 冈比亚
+甘比亞 冈比亚
+幾內亞比紹 几内亚比绍
+幾內亞比索 几内亚比绍
+列支敦斯登 列支敦士登
+列支敦士登 列支敦士登
+利比里亞 利比里亚
+賴比瑞亞 利比里亚
+加納 加纳
+迦納 加纳
+加彭 加蓬
+加蓬 加蓬
+博茨瓦納 博茨瓦纳
+波札那 博茨瓦纳
+卡塔爾 卡塔尔
+卡達 卡塔尔
+盧旺達 卢旺达
+盧安達 卢旺达
+危地馬拉 危地马拉
+瓜地馬拉 危地马拉
+厄瓜多爾 厄瓜多尔
+厄瓜多 厄瓜多尔
+厄立特里亞 厄立特里亚
+厄利垂亞 厄立特里亚
+吉布堤 吉布提
+吉布地 吉布提
+哈薩克 哈萨克斯坦
+哥斯達黎加 哥斯达黎加
+哥斯大黎加 哥斯达黎加
+圖瓦盧 图瓦卢
+吐瓦魯 图瓦卢
+土庫曼 土库曼斯坦
+聖盧西亞 圣卢西亚
+聖露西亞 圣卢西亚
+聖吉斯納域斯 圣基茨和尼维斯
+聖克里斯多福及尼維斯 圣基茨和尼维斯
+聖文森特和格林納丁斯 圣文森特和格林纳丁斯
+聖文森及格瑞那丁 圣文森特和格林纳丁斯
+聖馬力諾 圣马力诺
+聖馬利諾 圣马力诺
+圭亞那 圭亚那
+蓋亞那 圭亚那
+坦桑尼亞 坦桑尼亚
+坦尚尼亞 坦桑尼亚
+埃塞俄比亞 埃塞俄比亚
+衣索比亞 埃塞俄比亚
+吉里巴斯 基里巴斯
+基里巴斯 基里巴斯
+塔吉克 塔吉克斯坦
+獅子山 塞拉利昂
+塞拉利昂 塞拉利昂
+塞普勒斯 塞浦路斯
+塞浦路斯 塞浦路斯
+塞舌爾 塞舌尔
+塞席爾 塞舌尔
+多明尼加共和國 多米尼加
+多明尼加 多米尼加
+多明尼加聯邦 多米尼加联邦
+多米尼克 多米尼加联邦
+安提瓜和巴布達 安提瓜和巴布达
+安地卡及巴布達 安提瓜和巴布达
+尼日利亞 尼日利亚
+奈及利亞 尼日利亚
+尼日爾 尼日尔
+尼日 尼日尔
+巴貝多 巴巴多斯
+巴巴多斯 巴巴多斯
+巴布亞新畿內亞 巴布亚新几内亚
+巴布亞紐幾內亞 巴布亚新几内亚
+布基納法索 布基纳法索
+布吉納法索 布基纳法索
+蒲隆地 布隆迪
+布隆迪 布隆迪
+希臘 希腊
+帛琉 帕劳
+義大利 意大利
+意大利 意大利
+所羅門群島 所罗门群岛
+索羅門群島 所罗门群岛
+汶萊 文莱
+斯威士蘭 斯威士兰
+史瓦濟蘭 斯威士兰
+斯洛文尼亞 斯洛文尼亚
+斯洛維尼亞 斯洛文尼亚
+新西蘭 新西兰
+紐西蘭 新西兰
+北韓 朝鲜
+格林納達 格林纳达
+格瑞那達 格林纳达
+格魯吉亞 格鲁吉亚
+喬治亞 格鲁吉亚
+梵蒂岡 梵蒂冈
+教廷 梵蒂冈
+毛里塔尼亞 毛里塔尼亚
+茅利塔尼亞 毛里塔尼亚
+毛里裘斯 毛里求斯
+模里西斯 毛里求斯
+沙地阿拉伯 沙特阿拉伯
+沙烏地阿拉伯 沙特阿拉伯
+波斯尼亞黑塞哥維那 波斯尼亚和黑塞哥维那
+波士尼亞赫塞哥維納 波斯尼亚和黑塞哥维那
+津巴布韋 津巴布韦
+辛巴威 津巴布韦
+宏都拉斯 洪都拉斯
+洪都拉斯 洪都拉斯
+特立尼達和多巴哥 特立尼达和托巴哥
+千里達托貝哥 特立尼达和托巴哥
+瑙魯 瑙鲁
+諾魯 瑙鲁
+瓦努阿圖 瓦努阿图
+萬那杜 瓦努阿图
+溫納圖 瓦努阿图
+科摩羅 科摩罗
+葛摩 科摩罗
+象牙海岸 科特迪瓦
+突尼西亞 突尼斯
+索馬里 索马里
+索馬利亞 索马里
+老撾 老挝
+寮國 老挝
+肯雅 肯尼亚
+肯亞 肯尼亚
+蘇利南 苏里南
+莫三比克 莫桑比克
+莫桑比克 莫桑比克
+萊索托 莱索托
+賴索托 莱索托
+貝寧 贝宁
+貝南 贝宁
+贊比亞 赞比亚
+尚比亞 赞比亚
+亞塞拜然 阿塞拜疆
+阿塞拜疆 阿塞拜疆
+阿拉伯聯合酋長國 阿拉伯联合酋长国
+阿拉伯聯合大公國 阿拉伯联合酋长国
+南韓 韩国
+馬爾代夫 马尔代夫
+馬爾地夫 马尔代夫
+馬爾他 马耳他
+馬里 马里
+馬利 马里
+即食麵 方便面
+快速面 方便面
+速食麵 方便面
+泡麵 方便面
+笨豬跳 蹦极跳
+绑紧跳 蹦极跳
+冷盤   凉菜
+冷菜 凉菜
+散钱 零钱
+谐星 笑星    
+夜学 夜校
+华乐 民乐
+中樂 民乐
+住屋 住房
+屋价 房价
+的士 出租车
+計程車 出租车
+巴士 公共汽车
+公車 公共汽车
+單車 自行车
+節慶 节日
+芝士 乾酪
+狗隻 犬只
+士多啤梨 草莓
+忌廉 奶油
+桌球 台球
+撞球 台球
+雪糕 冰淇淋
+衞生 卫生
+衛生 卫生
+賓士 奔驰
+平治 奔驰
+捷豹 美洲虎
+積架 美洲虎
+福斯 大众
+福士 大众
+雪鐵龍 雪铁龙
+萬事得 马自达
+馬自達 马自达
+寶獅 标志
+布殊 布什
+布希 布什
+柯林頓 克林顿
+克林頓 克林顿
+薩達姆 萨达姆
+海珊 萨达姆
+梵谷 凡高
+大衛碧咸 大卫·贝克汉姆
+米高奧雲 迈克尔·欧文
+卡佩雅蒂 珍妮弗·卡普里亚蒂
+沙芬 马拉特·萨芬
+舒麥加 迈克尔·舒马赫
+希特拉 希特勒
+戴安娜 狄安娜
+黛安娜 狄安娜
+希拉 赫拉 \ No newline at end of file
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
new file mode 100644
index 00000000..ab623455
--- /dev/null
+++ b/includes/zhtable/toHK.manual
@@ -0,0 +1,211 @@
+打印机 打印機
+印表機 打印機
+字节 字節
+位元組 字節
+打印 打印
+列印 打印
+硬件 硬件
+硬體 硬件
+二极管 二極管
+二極體 二極管
+三极管 三極管
+三極體 三極管
+数码 數碼
+數位 數碼
+软件 軟件
+軟體 軟件
+网络 網絡
+網路 網絡
+人工智能 人工智能
+人工智慧 人工智能
+航天飞机 穿梭機
+太空梭 穿梭機
+因特网 互聯網
+網際網路 互聯網
+机器人 機械人
+機器人 機械人
+移动电话 流動電話
+行動電話 流動電話
+调制解调器 調制解調器
+數據機 調制解調器
+短信 短訊
+簡訊 短訊
+乍得 乍得
+查德 乍得
+也门 也門
+葉門 也門
+伯利兹 伯利茲
+貝里斯 伯利茲
+佛得角 佛得角
+維德角 佛得角
+克罗地亚 克羅地亞
+克羅埃西亞 克羅地亞
+冈比亚 岡比亞
+甘比亞 岡比亞
+几内亚比绍 幾內亞比紹
+幾內亞比索 幾內亞比紹
+列支敦士登 列支敦士登
+列支敦斯登 列支敦士登
+利比里亚 利比里亞
+賴比瑞亞 利比里亞
+加纳 加納
+迦納 加納
+加蓬 加蓬
+加彭 加蓬
+博茨瓦纳 博茨瓦納
+波札那 博茨瓦納
+卡塔尔 卡塔爾
+卡達 卡塔爾
+卢旺达 盧旺達
+盧安達 盧旺達
+危地马拉 危地馬拉
+瓜地馬拉 危地馬拉
+厄瓜多尔 厄瓜多爾
+厄瓜多 厄瓜多爾
+厄立特里亚 厄立特里亞
+厄利垂亞 厄立特里亞
+吉布提 吉布堤
+吉布地 吉布堤
+哥斯达黎加 哥斯達黎加
+哥斯大黎加 哥斯達黎加
+图瓦卢 圖瓦盧
+吐瓦魯 圖瓦盧
+圣卢西亚 聖盧西亞
+聖露西亞 聖盧西亞
+圣基茨和尼维斯 聖吉斯納域斯
+聖克里斯多福及尼維斯 聖吉斯納域斯
+圣文森特和格林纳丁斯 聖文森特和格林納丁斯
+聖文森及格瑞那丁 聖文森特和格林納丁斯
+圣马力诺 聖馬力諾
+聖馬利諾 聖馬力諾
+圭亚那 圭亞那
+蓋亞那 圭亞那
+坦桑尼亚 坦桑尼亞
+坦尚尼亞 坦桑尼亞
+埃塞俄比亚 埃塞俄比亞
+衣索比亞 埃塞俄比亞
+基里巴斯 基里巴斯
+吉里巴斯 基里巴斯
+獅子山 塞拉利昂
+塞普勒斯 塞浦路斯
+塞舌尔 塞舌爾
+塞席爾 塞舌爾
+多米尼加 多明尼加共和國
+多明尼加 多明尼加共和國
+多米尼加联邦 多明尼加聯邦
+多米尼克 多明尼加聯邦
+安提瓜和巴布达 安提瓜和巴布達
+安地卡及巴布達 安提瓜和巴布達
+尼日利亚 尼日利亞
+奈及利亞 尼日利亞
+尼日尔 尼日爾
+尼日 尼日爾
+巴巴多斯 巴巴多斯
+巴貝多 巴巴多斯
+巴布亚新几内亚 巴布亞新畿內亞
+巴布亞紐幾內亞 巴布亞新畿內亞
+布基纳法索 布基納法索
+布吉納法索 布基納法索
+布隆迪 布隆迪
+蒲隆地 布隆迪
+義大利 意大利
+所罗门群岛 所羅門群島
+索羅門群島 所羅門群島
+斯威士兰 斯威士蘭
+史瓦濟蘭 斯威士蘭
+斯洛文尼亚 斯洛文尼亞
+斯洛維尼亞 斯洛文尼亞
+新西兰 新西蘭
+紐西蘭 新西蘭
+格林纳达 格林納達
+格瑞那達 格林納達
+格鲁吉亚 格魯吉亞
+喬治亞 格魯吉亞
+梵蒂冈 梵蒂岡
+教廷 梵蒂岡
+毛里塔尼亚 毛里塔尼亞
+茅利塔尼亞 毛里塔尼亞
+毛里求斯 毛里裘斯
+模里西斯 毛里裘斯
+沙特阿拉伯 沙地阿拉伯
+沙烏地阿拉伯 沙地阿拉伯
+波斯尼亚和黑塞哥维那 波斯尼亞黑塞哥維那
+波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那
+津巴布韦 津巴布韋
+辛巴威 津巴布韋
+洪都拉斯 洪都拉斯
+宏都拉斯 洪都拉斯
+特立尼达和托巴哥 特立尼達和多巴哥
+千里達托貝哥 特立尼達和多巴哥
+瑙鲁 瑙魯
+諾魯 瑙魯
+瓦努阿图 瓦努阿圖
+萬那杜 瓦努阿圖
+科摩罗 科摩羅
+葛摩 科摩羅
+索马里 索馬里
+索馬利亞 索馬里
+老挝 老撾
+寮國 老撾
+肯尼亚 肯雅
+肯亞 肯雅
+莫桑比克 莫桑比克
+莫三比克 莫桑比克
+莱索托 萊索托
+賴索托 萊索托
+贝宁 貝寧
+貝南 貝寧
+赞比亚 贊比亞
+尚比亞 贊比亞
+阿塞拜疆 阿塞拜疆
+亞塞拜然 阿塞拜疆
+阿拉伯联合酋长国 阿拉伯聯合酋長國
+阿拉伯聯合大公國 阿拉伯聯合酋長國
+马尔代夫 馬爾代夫
+馬爾地夫 馬爾代夫
+马里 馬里
+馬利 馬里
+方便面 即食麵
+快速面 即食麵
+速食麵 即食麵
+泡麵 即食麵
+土豆 薯仔
+华乐 中樂
+民乐 中樂
+計程車 的士
+出租车 的士
+公車 巴士
+公共汽车 巴士
+自行车 單車
+节日 節慶
+犬只 狗隻
+台球 桌球
+撞球 桌球
+冰淇淋 雪糕
+冰淇淋 雪糕
+卫生 衞生
+衛生 衞生
+老人 長者
+賓士 平治
+捷豹 積架
+福斯 福士
+雪铁龙 先進
+雪鐵龍 先進
+沃尓沃 富豪
+马自达 萬事得
+馬自達 萬事得
+寶獅 標致
+布什 布殊
+布希 布殊
+克林顿 克林頓
+柯林頓 克林頓
+萨达姆 薩達姆
+海珊 薩達姆
+大卫·贝克汉姆 大衛碧咸
+迈克尔·欧文 米高奧雲
+珍妮弗·卡普里亚蒂 卡佩雅蒂
+马拉特·萨芬 沙芬
+迈克尔·舒马赫 舒麥加
+希特勒 希特拉
+狄安娜 戴安娜
+黛安娜 戴安娜 \ No newline at end of file
diff --git a/includes/zhtable/toSG.manual b/includes/zhtable/toSG.manual
new file mode 100644
index 00000000..9a399bc8
--- /dev/null
+++ b/includes/zhtable/toSG.manual
@@ -0,0 +1,15 @@
+方便面 快速面
+速食麵 快速面
+即食麵 快速面
+蹦极跳 绑紧跳
+笨豬跳 绑紧跳
+凉菜 冷菜
+冷盤 冷菜
+零钱 散钱
+散紙 散钱
+笑星 谐星
+夜校 夜学
+民乐 华乐
+住房 住屋
+房价 屋价
+泡麵 快速面 \ No newline at end of file
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
new file mode 100644
index 00000000..5c90dbe3
--- /dev/null
+++ b/includes/zhtable/toTW.manual
@@ -0,0 +1,309 @@
+内存 記憶體
+默认 預設
+缺省 預設
+串行 串列
+以太网 乙太網
+位图 點陣圖
+例程 常式
+信道 通道
+光标 游標
+光盘 光碟
+光驱 光碟機
+全角 全形
+共享 共用
+兼容 相容
+前缀 首碼
+后缀 尾碼
+加载 載入
+半角 半形
+变量 變數
+噪声 雜訊
+因子 因數
+在线 線上
+脱机 離線
+域名 功能變數名稱
+声卡 音效卡
+字号 字型大小
+字库 字型檔
+字段 欄位
+字符 字元
+存盘 存檔
+寻址 定址
+尾注 章節附註
+异步 非同步
+总线 匯流排
+括号 括弧
+接口 介面
+控件 控制項
+权限 許可權
+盘片 碟片
+硅片 矽片
+硅谷 矽谷
+硬盘 硬碟
+磁盘 磁碟
+磁道 磁軌
+程控 程式控制
+端口 埠
+算子 運算元
+算法 演算法
+芯片 晶片
+芯片 晶元
+词组 片語
+译码 解碼
+软驱 軟碟機
+闪存 快閃記憶體
+鼠标 滑鼠
+进制 進位
+交互式 互動式
+仿真 模擬
+优先级 優先順序
+传感 感測
+便携式 攜帶型
+信息论 資訊理論
+循环 迴圈
+写保护 防寫
+分布式 分散式
+分辨率 解析度
+程序 程式
+服务器 伺服器
+等于 等於
+局域网 區域網
+上载 上傳
+计算机 電腦
+宏 巨集
+扫瞄仪 掃瞄器
+宽带 寬頻
+窗口 視窗
+数据库 資料庫
+公历 西曆
+奶酪 乳酪
+巨商 鉅賈
+手电 手電筒
+万历 萬曆
+永历 永曆
+词汇 辭彙
+保安 保全
+习用 慣用
+元音 母音
+任意球 自由球
+头球 頭槌
+入球 進球
+粒入球 顆進球
+打门 射門
+火锅盖帽 蓋火鍋
+打印机 印表機
+打印機 印表機
+字节 位元組
+字節 位元組
+打印 列印
+打印 列印
+硬件 硬體
+硬件 硬體
+二极管 二極體
+二極管 二極體
+三极管 三極體
+三極管 三極體
+数码 數位
+數碼 數位
+软件 軟體
+軟件 軟體
+网络 網路
+網絡 網路
+人工智能 人工智慧
+航天飞机 太空梭
+穿梭機 太空梭
+因特网 網際網路
+互聯網 網際網路
+机器人 機器人
+機械人 機器人
+移动电话 行動電話
+流動電話 行動電話
+调制解调器 數據機
+調制解調器 數據機
+短信 簡訊
+短訊 簡訊
+乌兹别克斯坦 烏茲別克
+乍得 查德
+乍得 查德
+也门 葉門
+也門 葉門
+伯利兹 貝里斯
+伯利茲 貝里斯
+佛得角 維德角
+佛得角 維德角
+克罗地亚 克羅埃西亞
+克羅地亞 克羅埃西亞
+冈比亚 甘比亞
+岡比亞 甘比亞
+几内亚比绍 幾內亞比索
+幾內亞比紹 幾內亞比索
+列支敦士登 列支敦斯登
+列支敦士登 列支敦斯登
+利比里亚 賴比瑞亞
+利比里亞 賴比瑞亞
+加纳 迦納
+加納 迦納
+加蓬 加彭
+加蓬 加彭
+博茨瓦纳 波札那
+博茨瓦納 波札那
+卡塔尔 卡達
+卡塔爾 卡達
+卢旺达 盧安達
+盧旺達 盧安達
+危地马拉 瓜地馬拉
+危地馬拉 瓜地馬拉
+厄瓜多尔 厄瓜多
+厄瓜多爾 厄瓜多
+厄立特里亚 厄利垂亞
+厄立特里亞 厄利垂亞
+吉布提 吉布地
+吉布堤 吉布地
+哈萨克斯坦 哈薩克
+哥斯达黎加 哥斯大黎加
+哥斯達黎加 哥斯大黎加
+图瓦卢 吐瓦魯
+圖瓦盧 吐瓦魯
+土库曼斯坦 土庫曼
+圣卢西亚 聖露西亞
+聖盧西亞 聖露西亞
+圣基茨和尼维斯 聖克里斯多福及尼維斯
+聖吉斯納域斯 聖克里斯多福及尼維斯
+圣文森特和格林纳丁斯 聖文森及格瑞那丁
+聖文森特和格林納丁斯 聖文森及格瑞那丁
+圣马力诺 聖馬利諾
+聖馬力諾 聖馬利諾
+圭亚那 蓋亞那
+圭亞那 蓋亞那
+坦桑尼亚 坦尚尼亞
+坦桑尼亞 坦尚尼亞
+埃塞俄比亚 衣索比亞
+埃塞俄比亞 衣索比亞
+基里巴斯 吉里巴斯
+基里巴斯 吉里巴斯
+塔吉克斯坦 塔吉克
+塞拉利昂 獅子山
+塞拉利昂 獅子山
+塞浦路斯 塞普勒斯
+塞浦路斯 塞普勒斯
+塞舌尔 塞席爾
+塞舌爾 塞席爾
+多米尼加 多明尼加
+多明尼加共和國 多明尼加
+多米尼加联邦 多米尼克
+多明尼加聯邦 多米尼克
+安提瓜和巴布达 安地卡及巴布達
+安提瓜和巴布達 安地卡及巴布達
+尼日利亚 奈及利亞
+尼日利亞 奈及利亞
+尼日尔 尼日
+尼日爾 尼日
+巴巴多斯 巴貝多
+巴巴多斯 巴貝多
+巴布亚新几内亚 巴布亞紐幾內亞
+巴布亞新畿內亞 巴布亞紐幾內亞
+布基纳法索 布吉納法索
+布基納法索 布吉納法索
+布隆迪 蒲隆地
+布隆迪 蒲隆地
+希腊 希臘
+帕劳 帛琉
+意大利 義大利
+意大利 義大利
+所罗门群岛 索羅門群島
+所羅門群島 索羅門群島
+文莱 汶萊
+斯威士兰 史瓦濟蘭
+斯威士蘭 史瓦濟蘭
+斯洛文尼亚 斯洛維尼亞
+斯洛文尼亞 斯洛維尼亞
+新西兰 紐西蘭
+新西蘭 紐西蘭
+朝鲜 北韓
+格林纳达 格瑞那達
+格林納達 格瑞那達
+格鲁吉亚 喬治亞
+格魯吉亞 喬治亞
+梵蒂冈 教廷
+梵蒂岡 教廷
+毛里塔尼亚 茅利塔尼亞
+毛里塔尼亞 茅利塔尼亞
+毛里求斯 模里西斯
+毛里裘斯 模里西斯
+沙特阿拉伯 沙烏地阿拉伯
+沙地阿拉伯 沙烏地阿拉伯
+波斯尼亚和黑塞哥维那 波士尼亞赫塞哥維納
+波斯尼亞黑塞哥維那 波士尼亞赫塞哥維納
+津巴布韦 辛巴威
+津巴布韋 辛巴威
+洪都拉斯 宏都拉斯
+洪都拉斯 宏都拉斯
+特立尼达和托巴哥 千里達托貝哥
+特立尼達和多巴哥 千里達托貝哥
+瑙鲁 諾魯
+瑙魯 諾魯
+瓦努阿图 萬那杜
+瓦努阿圖 萬那杜
+溫納圖萬 那杜
+科摩罗 葛摩
+科摩羅 葛摩
+科特迪瓦 象牙海岸
+突尼斯 突尼西亞
+索马里 索馬利亞
+索馬里 索馬利亞
+老挝 寮國
+老撾 寮國
+肯尼亚 肯亞
+肯雅 肯亞
+苏里南 蘇利南
+莫桑比克 莫三比克
+莱索托 賴索托
+萊索托 賴索托
+贝宁 貝南
+貝寧 貝南
+赞比亚 尚比亞
+贊比亞 尚比亞
+阿塞拜疆 亞塞拜然
+阿塞拜疆 亞塞拜然
+阿拉伯联合酋长国 阿拉伯聯合大公國
+阿拉伯聯合酋長國 阿拉伯聯合大公國
+韩国 南韓
+马尔代夫 馬爾地夫
+馬爾代夫 馬爾地夫
+马耳他 馬爾他
+马里 馬利
+馬里 馬利
+方便面 速食麵
+快速面 速食麵
+即食麵 速食麵
+薯仔 土豆
+蹦极跳 笨豬跳
+绑紧跳 笨豬跳
+冷菜 冷盤
+凉菜 冷盤
+的士 計程車
+出租车 計程車
+巴士 公車
+公共汽车 公車
+台球 撞球
+桌球 撞球
+雪糕 冰淇淋
+卫生 衛生
+衞生 衛生
+平治 賓士
+奔驰 賓士
+積架 捷豹
+福士 福斯
+雪铁龙 雪鐵龍
+马自达 馬自達
+萬事得 馬自達
+布什 布希
+布殊 布希
+克林顿 柯林頓
+克林頓 柯林頓
+萨达姆 海珊
+薩達姆 海珊
+凡高 梵谷
+狄安娜 黛安娜
+戴安娜 黛安娜
+赫拉 希拉 \ No newline at end of file
diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual
new file mode 100644
index 00000000..da069310
--- /dev/null
+++ b/includes/zhtable/trad2simp.manual
@@ -0,0 +1,15 @@
+U+056a5嚥|U+054bd咽|
+U+0585a塚|U+051a2冢|
+U+05dbd嶽|U+05cb3岳|
+U+04e99亙|U+04e98亘|
+U+081e5臥|U+05367卧|
+U+04f48佈|U+05e03布|
+U+06dd2淒|U+051c4凄|
+U+06de8淨|U+051c0净|
+U+05147兇|U+051f6凶|
+U+04f48佈|U+05e03布|
+U+06c59汙|U+06c61污|
+U+056ae嚮|U+05411向|
+U+09031週|U+05468周|
+U+0904a遊|U+06e38游|
+U+06de9淩|U+051cc凌|
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
new file mode 100644
index 00000000..b2fec815
--- /dev/null
+++ b/includes/zhtable/tradphrases.manual
@@ -0,0 +1,149 @@
+一隻
+三隻
+四隻
+五隻
+六隻
+七隻
+八隻
+九隻
+十隻
+百隻
+千隻
+萬隻
+億隻
+並存著
+乾絲
+乾著急
+体育鍛鍊
+借著
+偷雞不著
+几絲
+划著
+划著走
+別著
+刮著
+千絲萬縷
+參与
+參与者
+參合
+參考價值
+參與
+參與人員
+參與制
+參與感
+參與者
+參觀團
+參觀團體
+參閱
+吃著不盡
+合著
+合著者
+吊帶褲
+吊掛著
+吊著
+吊褲
+吊褲帶
+向著
+嚴絲合縫
+回絲
+回著
+塗著
+壟斷價格
+壟斷資產
+壟斷集團
+姜絲
+帶團參加
+干著急
+幾絲
+彆著
+怎麼著
+憑藉著
+接著說
+擔著
+擔負著
+敘說著
+斗轉參橫
+旋繞著
+板著臉
+標志著
+正當著
+沈著
+沖著
+派團參加
+涂著
+湊合著
+瀰漫著
+為著
+煙斗絲
+率團參加
+畫著
+當著
+發著
+直接參与
+睡著了
+秋褲
+積极參与
+積极參加
+簽著
+系著
+絕對參照
+絲來線去
+絲布
+絲板
+絲瓜布
+絲絨布
+絲線
+絲織廠
+絲蟲
+緊繃著
+繃著
+繃著臉
+繃著臉兒
+繫著
+罵著
+肉絲麵
+背向著
+菌絲体
+菌絲體
+著兒
+著書立說
+著色軟體
+著重指出
+著錄
+著錄規則
+薑絲
+藉著
+蘊含著
+蘊涵著
+衝著
+被覆著
+覆著
+覆蓋著
+訴說著
+說著
+請參閱
+謝絕參觀
+豎著
+豐濱
+豐濱鄉
+象徵著
+這么著
+這麼著
+那麼著
+配合著
+醞釀著
+錄著
+鍛鍊出
+鍛鍊身体
+關係著
+雞絲
+雞絲麵
+面朝著
+面臨著
+顯著標志
+颳著
+髮絲
+鬥著
+鬧著玩儿
+鬧著玩兒
+鯰魚