diff options
Diffstat (limited to 'includes')
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> </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 = ' ', $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.'×)'; + } + 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, ' ', $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> '; + $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); + $r .= ' </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 .= ' '; + } else { + $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $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 = ' <strong>[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'rollbacklink' ), + 'action=rollback&from=' . urlencode( $username ) . + '&token=' . urlencode( $wgUser->editToken( array( $this->mTitle->getPrefixedText(), $username ) ) ) ) . + ']</strong>'; + } else { + $rollback = ''; + } + if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed( 'patrol' ) ) { + $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']'; + } else { + $patrol = ''; + } + + $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ), + 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' ); + if ( $this->mNewRev->isCurrent() ) { + $nextlink = ' '; + } else { + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), + 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + } + + $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" . + $sk->revUserTools( $this->mOldRev ) . "<br />" . + $sk->revComment( $this->mOldRev ) . "<br />" . + $prevlink; + $newHeader = "<strong>{$this->mNewtitle}</strong><br />" . + $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . $patrol; + + $this->showDiff( $oldHeader, $newHeader ); + $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + + if( !$this->mNewRev->isCurrent() ) { + $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + } + + $this->loadNewText(); + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + $wgOut->addSecondaryWikiText( $this->mNewtext ); + + if( !$this->mNewRev->isCurrent() ) { + $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + } + + wfProfileOut( $fname ); + } + + /** + * Show the first revision of an article. Uses normal diff headers in + * contrast to normal "old revision" display style. + */ + function showFirstRevision() { + global $wgOut, $wgUser; + + $fname = 'DifferenceEngine::showFirstRevision'; + wfProfileIn( $fname ); + + # Get article text from the DB + # + if ( ! $this->loadNewText() ) { + $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . + "{$this->mNewid})"; + $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); + $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); + $wgOut->addWikitext( $mtext ); + wfProfileOut( $fname ); + return; + } + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + + # Check if user is allowed to look at this page. If not, bail out. + # + if ( !( $this->mTitle->userCanRead() ) ) { + $wgOut->loginToUse(); + $wgOut->output(); + wfProfileOut( $fname ); + exit; + } + + # Prepare the header box + # + $sk = $wgUser->getSkin(); + + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" . + $sk->revUserTools( $this->mNewRev ) . "<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . "</div>\n"; + + $wgOut->addHTML( $header ); + + $wgOut->setSubtitle( wfMsg( 'difference' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + + # Show current revision + # + $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + $wgOut->addSecondaryWikiText( $this->mNewtext ); + + wfProfileOut( $fname ); + } + + /** + * Get the diff text, send it to $wgOut + * Returns false if the diff could not be generated, otherwise returns true + */ + function showDiff( $otitle, $ntitle ) { + global $wgOut; + $diff = $this->getDiff( $otitle, $ntitle ); + if ( $diff === false ) { + $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) ); + return false; + } else { + $wgOut->addHTML( $diff ); + return true; + } + } + + /** + * Get diff table, including header + * Note that the interface has changed, it's no longer static. + * Returns false on error + */ + function getDiff( $otitle, $ntitle ) { + $body = $this->getDiffBody(); + if ( $body === false ) { + return false; + } else { + return $this->addHeader( $body, $otitle, $ntitle ); + } + } + + /** + * Get the diff table body, without header + * Results are cached + * Returns false on error + */ + function getDiffBody() { + global $wgMemc, $wgDBname; + $fname = 'DifferenceEngine::getDiffBody'; + wfProfileIn( $fname ); + + // Cacheable? + $key = false; + if ( $this->mOldid && $this->mNewid ) { + // Try cache + $key = "$wgDBname:diff:oldid:{$this->mOldid}:newid:{$this->mNewid}"; + $difftext = $wgMemc->get( $key ); + if ( $difftext ) { + wfIncrStats( 'diff_cache_hit' ); + $difftext = $this->localiseLineNumbers( $difftext ); + $difftext .= "\n<!-- diff cache key $key -->\n"; + wfProfileOut( $fname ); + return $difftext; + } + } + + if ( !$this->loadText() ) { + wfProfileOut( $fname ); + return false; + } + + $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + + // Save to cache for 7 days + if ( $key !== false && $difftext !== false ) { + wfIncrStats( 'diff_cache_miss' ); + $wgMemc->set( $key, $difftext, 7*86400 ); + } else { + wfIncrStats( 'diff_uncacheable' ); + } + // Replace line numbers with the text in the user's language + if ( $difftext !== false ) { + $difftext = $this->localiseLineNumbers( $difftext ); + } + wfProfileOut( $fname ); + return $difftext; + } + + /** + * Generate a diff, no caching + * $otext and $ntext must be already segmented + */ + function generateDiffBody( $otext, $ntext ) { + global $wgExternalDiffEngine, $wgContLang; + $fname = 'DifferenceEngine::generateDiffBody'; + + $otext = str_replace( "\r\n", "\n", $otext ); + $ntext = str_replace( "\r\n", "\n", $ntext ); + + if ( $wgExternalDiffEngine == 'wikidiff' ) { + # For historical reasons, external diff engine expects + # input text to be HTML-escaped already + $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); + $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); + if( !function_exists( 'wikidiff_do_diff' ) ) { + dl('php_wikidiff.so'); + } + return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ); + } + + if ( $wgExternalDiffEngine == 'wikidiff2' ) { + # Better external diff engine, the 2 may some day be dropped + # This one does the escaping and segmenting itself + if ( !function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( "$fname-dl" ); + @dl('php_wikidiff2.so'); + wfProfileOut( "$fname-dl" ); + } + if ( function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( 'wikidiff2_do_diff' ); + $text = wikidiff2_do_diff( $otext, $ntext, 2 ); + wfProfileOut( 'wikidiff2_do_diff' ); + return $text; + } + } + if ( $wgExternalDiffEngine !== false ) { + # Diff via the shell + global $wgTmpDirectory; + $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); + $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); + + $tempFile1 = fopen( $tempName1, "w" ); + if ( !$tempFile1 ) { + wfProfileOut( $fname ); + return false; + } + $tempFile2 = fopen( $tempName2, "w" ); + if ( !$tempFile2 ) { + wfProfileOut( $fname ); + return false; + } + fwrite( $tempFile1, $otext ); + fwrite( $tempFile2, $ntext ); + fclose( $tempFile1 ); + fclose( $tempFile2 ); + $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); + wfProfileIn( "$fname-shellexec" ); + $difftext = wfShellExec( $cmd ); + wfProfileOut( "$fname-shellexec" ); + unlink( $tempName1 ); + unlink( $tempName2 ); + return $difftext; + } + + # Native PHP diff + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + $diffs =& new Diff( $ota, $nta ); + $formatter =& new TableDiffFormatter(); + return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); + } + + + /** + * Replace line numbers with the text in the user's language + */ + function localiseLineNumbers( $text ) { + return preg_replace_callback( '/<!--LINE (\d+)-->/', + array( &$this, 'localiseLineNumbersCb' ), $text ); + } + + function localiseLineNumbersCb( $matches ) { + global $wgLang; + return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); + } + + /** + * Add the header to a diff body + */ + function addHeader( $diff, $otitle, $ntitle ) { + $out = " + <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'> + <tr> + <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td> + <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td> + </tr> + $diff + </table> + "; + return $out; + } + + /** + * Use specified text instead of loading from the database + */ + function setText( $oldText, $newText ) { + $this->mOldtext = $oldText; + $this->mNewtext = $newText; + $this->mTextLoaded = 2; + } + + /** + * Load revision metadata for the specified articles. If newid is 0, then compare + * the old article in oldid to the current article; if oldid is 0, then + * compare the current article to the immediately previous one (ignoring the + * value of newid). + * + * If oldid is false, leave the corresponding revision object set + * to false. This is impossible via ordinary user input, and is provided for + * API convenience. + */ + function loadRevisionData() { + global $wgLang; + if ( $this->mRevisionsLoaded ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mRevisionsLoaded = true; + } + + // Load the new revision object + if( $this->mNewid ) { + $this->mNewRev = Revision::newFromId( $this->mNewid ); + } else { + $this->mNewRev = Revision::newFromTitle( $this->mTitle ); + } + + if( is_null( $this->mNewRev ) ) { + return false; + } + + // Set assorted variables + $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true ); + $this->mNewPage = $this->mNewRev->getTitle(); + if( $this->mNewRev->isCurrent() ) { + $newLink = $this->mNewPage->escapeLocalUrl(); + $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); + + $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)</strong>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + + } else { + $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); + $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); + + $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a></strong>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + } + + // Load the old revision object + $this->mOldRev = false; + if( $this->mOldid ) { + $this->mOldRev = Revision::newFromId( $this->mOldid ); + } elseif ( $this->mOldid === 0 ) { + $rev = $this->mNewRev->getPrevious(); + if( $rev ) { + $this->mOldid = $rev->getId(); + $this->mOldRev = $rev; + } else { + // No previous revision; mark to show as first-version only. + $this->mOldid = false; + $this->mOldRev = false; + } + }/* elseif ( $this->mOldid === false ) leave mOldRev false; */ + + if( is_null( $this->mOldRev ) ) { + return false; + } + + if ( $this->mOldRev ) { + $this->mOldPage = $this->mOldRev->getTitle(); + + $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true ); + $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); + $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); + $this->mOldtitle = "<strong><a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) + . "</a></strong> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + } + + return true; + } + + /** + * Load the text of the revisions, as well as revision data. + */ + function loadText() { + if ( $this->mTextLoaded == 2 ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mTextLoaded = 2; + } + + if ( !$this->loadRevisionData() ) { + return false; + } + if ( $this->mOldRev ) { + // FIXME: permission tests + $this->mOldtext = $this->mOldRev->getText(); + if ( $this->mOldtext === false ) { + return false; + } + } + if ( $this->mNewRev ) { + $this->mNewtext = $this->mNewRev->getText(); + if ( $this->mNewtext === false ) { + return false; + } + } + return true; + } + + /** + * Load the text of the new revision, not the old one + */ + function loadNewText() { + if ( $this->mTextLoaded >= 1 ) { + return true; + } else { + $this->mTextLoaded = 1; + } + if ( !$this->loadRevisionData() ) { + return false; + } + $this->mNewtext = $this->mNewRev->getText(); + return true; + } + + +} + +// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) +// +// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> +// You may copy this code freely under the conditions of the GPL. +// + +define('USE_ASSERTS', function_exists('assert')); + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp { + var $type; + var $orig; + var $closing; + + function reverse() { + trigger_error('pure virtual', E_USER_ERROR); + } + + function norig() { + return $this->orig ? sizeof($this->orig) : 0; + } + + function nclosing() { + return $this->closing ? sizeof($this->closing) : 0; + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Copy extends _DiffOp { + var $type = 'copy'; + + function _DiffOp_Copy ($orig, $closing = false) { + if (!is_array($closing)) + $closing = $orig; + $this->orig = $orig; + $this->closing = $closing; + } + + function reverse() { + return new _DiffOp_Copy($this->closing, $this->orig); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Delete extends _DiffOp { + var $type = 'delete'; + + function _DiffOp_Delete ($lines) { + $this->orig = $lines; + $this->closing = false; + } + + function reverse() { + return new _DiffOp_Add($this->orig); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Add extends _DiffOp { + var $type = 'add'; + + function _DiffOp_Add ($lines) { + $this->closing = $lines; + $this->orig = false; + } + + function reverse() { + return new _DiffOp_Delete($this->closing); + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffOp_Change extends _DiffOp { + var $type = 'change'; + + function _DiffOp_Change ($orig, $closing) { + $this->orig = $orig; + $this->closing = $closing; + } + + function reverse() { + return new _DiffOp_Change($this->closing, $this->orig); + } +} + + +/** + * Class used internally by Diff to actually compute the diffs. + * + * The algorithm used here is mostly lifted from the perl module + * Algorithm::Diff (version 1.06) by Ned Konz, which is available at: + * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip + * + * More ideas are taken from: + * http://www.ics.uci.edu/~eppstein/161/960229.html + * + * Some ideas are (and a bit of code) are from from analyze.c, from GNU + * diffutils-2.7, which can be found at: + * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz + * + * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations) + * are my own. + * + * Line length limits for robustness added by Tim Starling, 2005-08-31 + * + * @author Geoffrey T. Dairiki, Tim Starling + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _DiffEngine +{ + function diff ($from_lines, $to_lines) { + $fname = '_DiffEngine::diff'; + wfProfileIn( $fname ); + + $n_from = sizeof($from_lines); + $n_to = sizeof($to_lines); + + $this->xchanged = $this->ychanged = array(); + $this->xv = $this->yv = array(); + $this->xind = $this->yind = array(); + unset($this->seq); + unset($this->in_seq); + unset($this->lcs); + + // Skip leading common lines. + for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { + if ($from_lines[$skip] !== $to_lines[$skip]) + break; + $this->xchanged[$skip] = $this->ychanged[$skip] = false; + } + // Skip trailing common lines. + $xi = $n_from; $yi = $n_to; + for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { + if ($from_lines[$xi] !== $to_lines[$yi]) + break; + $this->xchanged[$xi] = $this->ychanged[$yi] = false; + } + + // Ignore lines which do not exist in both files. + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $xhash[$this->_line_hash($from_lines[$xi])] = 1; + } + + for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { + $line = $to_lines[$yi]; + if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) ) + continue; + $yhash[$this->_line_hash($line)] = 1; + $this->yv[] = $line; + $this->yind[] = $yi; + } + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $line = $from_lines[$xi]; + if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) ) + continue; + $this->xv[] = $line; + $this->xind[] = $xi; + } + + // Find the LCS. + $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv)); + + // Merge edits when possible + $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged); + $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged); + + // Compute the edit operations. + $edits = array(); + $xi = $yi = 0; + while ($xi < $n_from || $yi < $n_to) { + USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]); + USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]); + + // Skip matching "snake". + $copy = array(); + while ( $xi < $n_from && $yi < $n_to + && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { + $copy[] = $from_lines[$xi++]; + ++$yi; + } + if ($copy) + $edits[] = new _DiffOp_Copy($copy); + + // Find deletes & adds. + $delete = array(); + while ($xi < $n_from && $this->xchanged[$xi]) + $delete[] = $from_lines[$xi++]; + + $add = array(); + while ($yi < $n_to && $this->ychanged[$yi]) + $add[] = $to_lines[$yi++]; + + if ($delete && $add) + $edits[] = new _DiffOp_Change($delete, $add); + elseif ($delete) + $edits[] = new _DiffOp_Delete($delete); + elseif ($add) + $edits[] = new _DiffOp_Add($add); + } + wfProfileOut( $fname ); + return $edits; + } + + /** + * Returns the whole line if it's small enough, or the MD5 hash otherwise + */ + function _line_hash( $line ) { + if ( strlen( $line ) > MAX_DIFF_XREF_LENGTH ) { + return md5( $line ); + } else { + return $line; + } + } + + + /* Divide the Largest Common Subsequence (LCS) of the sequences + * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally + * sized segments. + * + * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an + * array of NCHUNKS+1 (X, Y) indexes giving the diving points between + * sub sequences. The first sub-sequence is contained in [X0, X1), + * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note + * that (X0, Y0) == (XOFF, YOFF) and + * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). + * + * This function assumes that the first lines of the specified portions + * of the two files do not match, and likewise that the last lines do not + * match. The caller must trim matching lines from the beginning and end + * of the portions it is going to specify. + */ + function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) { + $fname = '_DiffEngine::_diag'; + wfProfileIn( $fname ); + $flip = false; + + if ($xlim - $xoff > $ylim - $yoff) { + // Things seems faster (I'm not sure I understand why) + // when the shortest sequence in X. + $flip = true; + list ($xoff, $xlim, $yoff, $ylim) + = array( $yoff, $ylim, $xoff, $xlim); + } + + if ($flip) + for ($i = $ylim - 1; $i >= $yoff; $i--) + $ymatches[$this->xv[$i]][] = $i; + else + for ($i = $ylim - 1; $i >= $yoff; $i--) + $ymatches[$this->yv[$i]][] = $i; + + $this->lcs = 0; + $this->seq[0]= $yoff - 1; + $this->in_seq = array(); + $ymids[0] = array(); + + $numer = $xlim - $xoff + $nchunks - 1; + $x = $xoff; + for ($chunk = 0; $chunk < $nchunks; $chunk++) { + wfProfileIn( "$fname-chunk" ); + if ($chunk > 0) + for ($i = 0; $i <= $this->lcs; $i++) + $ymids[$i][$chunk-1] = $this->seq[$i]; + + $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks); + for ( ; $x < $x1; $x++) { + $line = $flip ? $this->yv[$x] : $this->xv[$x]; + if (empty($ymatches[$line])) + continue; + $matches = $ymatches[$line]; + reset($matches); + while (list ($junk, $y) = each($matches)) + if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + USE_ASSERTS && assert($k > 0); + $ymids[$k] = $ymids[$k-1]; + break; + } + while (list ($junk, $y) = each($matches)) { + if ($y > $this->seq[$k-1]) { + USE_ASSERTS && assert($y < $this->seq[$k]); + // Optimization: this is a common case: + // next match is just replacing previous match. + $this->in_seq[$this->seq[$k]] = false; + $this->seq[$k] = $y; + $this->in_seq[$y] = 1; + } else if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + USE_ASSERTS && assert($k > 0); + $ymids[$k] = $ymids[$k-1]; + } + } + } + wfProfileOut( "$fname-chunk" ); + } + + $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); + $ymid = $ymids[$this->lcs]; + for ($n = 0; $n < $nchunks - 1; $n++) { + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); + $y1 = $ymid[$n] + 1; + $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); + } + $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); + + wfProfileOut( $fname ); + return array($this->lcs, $seps); + } + + function _lcs_pos ($ypos) { + $fname = '_DiffEngine::_lcs_pos'; + wfProfileIn( $fname ); + + $end = $this->lcs; + if ($end == 0 || $ypos > $this->seq[$end]) { + $this->seq[++$this->lcs] = $ypos; + $this->in_seq[$ypos] = 1; + wfProfileOut( $fname ); + return $this->lcs; + } + + $beg = 1; + while ($beg < $end) { + $mid = (int)(($beg + $end) / 2); + if ( $ypos > $this->seq[$mid] ) + $beg = $mid + 1; + else + $end = $mid; + } + + USE_ASSERTS && assert($ypos != $this->seq[$end]); + + $this->in_seq[$this->seq[$end]] = false; + $this->seq[$end] = $ypos; + $this->in_seq[$ypos] = 1; + wfProfileOut( $fname ); + return $end; + } + + /* Find LCS of two sequences. + * + * The results are recorded in the vectors $this->{x,y}changed[], by + * storing a 1 in the element for each line that is an insertion + * or deletion (ie. is not in the LCS). + * + * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. + * + * Note that XLIM, YLIM are exclusive bounds. + * All line numbers are origin-0 and discarded lines are not counted. + */ + function _compareseq ($xoff, $xlim, $yoff, $ylim) { + $fname = '_DiffEngine::_compareseq'; + wfProfileIn( $fname ); + + // Slide down the bottom initial diagonal. + while ($xoff < $xlim && $yoff < $ylim + && $this->xv[$xoff] == $this->yv[$yoff]) { + ++$xoff; + ++$yoff; + } + + // Slide up the top initial diagonal. + while ($xlim > $xoff && $ylim > $yoff + && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { + --$xlim; + --$ylim; + } + + if ($xoff == $xlim || $yoff == $ylim) + $lcs = 0; + else { + // This is ad hoc but seems to work well. + //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); + //$nchunks = max(2,min(8,(int)$nchunks)); + $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; + list ($lcs, $seps) + = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks); + } + + if ($lcs == 0) { + // X and Y sequences have no common subsequence: + // mark all changed. + while ($yoff < $ylim) + $this->ychanged[$this->yind[$yoff++]] = 1; + while ($xoff < $xlim) + $this->xchanged[$this->xind[$xoff++]] = 1; + } else { + // Use the partitions to split this problem into subproblems. + reset($seps); + $pt1 = $seps[0]; + while ($pt2 = next($seps)) { + $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]); + $pt1 = $pt2; + } + } + wfProfileOut( $fname ); + } + + /* Adjust inserts/deletes of identical lines to join changes + * as much as possible. + * + * We do something when a run of changed lines include a + * line at one end and has an excluded, identical line at the other. + * We are free to choose which identical line is included. + * `compareseq' usually chooses the one at the beginning, + * but usually it is cleaner to consider the following identical line + * to be the "change". + * + * This is extracted verbatim from analyze.c (GNU diffutils-2.7). + */ + function _shift_boundaries ($lines, &$changed, $other_changed) { + $fname = '_DiffEngine::_shift_boundaries'; + wfProfileIn( $fname ); + $i = 0; + $j = 0; + + USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)'); + $len = sizeof($lines); + $other_len = sizeof($other_changed); + + while (1) { + /* + * Scan forwards to find beginning of another run of changes. + * Also keep track of the corresponding point in the other file. + * + * Throughout this code, $i and $j are adjusted together so that + * the first $i elements of $changed and the first $j elements + * of $other_changed both contain the same number of zeros + * (unchanged lines). + * Furthermore, $j is always kept so that $j == $other_len or + * $other_changed[$j] == false. + */ + while ($j < $other_len && $other_changed[$j]) + $j++; + + while ($i < $len && ! $changed[$i]) { + USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); + $i++; $j++; + while ($j < $other_len && $other_changed[$j]) + $j++; + } + + if ($i == $len) + break; + + $start = $i; + + // Find the end of this run of changes. + while (++$i < $len && $changed[$i]) + continue; + + do { + /* + * Record the length of this run of changes, so that + * we can later determine whether the run has grown. + */ + $runlength = $i - $start; + + /* + * Move the changed region back, so long as the + * previous unchanged line matches the last changed one. + * This merges with previous changed regions. + */ + while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { + $changed[--$start] = 1; + $changed[--$i] = false; + while ($start > 0 && $changed[$start - 1]) + $start--; + USE_ASSERTS && assert('$j > 0'); + while ($other_changed[--$j]) + continue; + USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); + } + + /* + * Set CORRESPONDING to the end of the changed run, at the last + * point where it corresponds to a changed run in the other file. + * CORRESPONDING == LEN means no such point has been found. + */ + $corresponding = $j < $other_len ? $i : $len; + + /* + * Move the changed region forward, so long as the + * first changed line matches the following unchanged one. + * This merges with following changed regions. + * Do this second, so that if there are no merges, + * the changed region is moved forward as far as possible. + */ + while ($i < $len && $lines[$start] == $lines[$i]) { + $changed[$start++] = false; + $changed[$i++] = 1; + while ($i < $len && $changed[$i]) + $i++; + + USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); + $j++; + if ($j < $other_len && $other_changed[$j]) { + $corresponding = $i; + while ($j < $other_len && $other_changed[$j]) + $j++; + } + } + } while ($runlength != $i - $start); + + /* + * If possible, move the fully-merged run of changes + * back to a corresponding run in the other file. + */ + while ($corresponding < $i) { + $changed[--$start] = 1; + $changed[--$i] = 0; + USE_ASSERTS && assert('$j > 0'); + while ($other_changed[--$j]) + continue; + USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); + } + } + wfProfileOut( $fname ); + } +} + +/** + * Class representing a 'diff' between two sequences of strings. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class Diff +{ + var $edits; + + /** + * Constructor. + * Computes diff between sequences of strings. + * + * @param $from_lines array An array of strings. + * (Typically these are lines from a file.) + * @param $to_lines array An array of strings. + */ + function Diff($from_lines, $to_lines) { + $eng = new _DiffEngine; + $this->edits = $eng->diff($from_lines, $to_lines); + //$this->_check($from_lines, $to_lines); + } + + /** + * Compute reversed Diff. + * + * SYNOPSIS: + * + * $diff = new Diff($lines1, $lines2); + * $rev = $diff->reverse(); + * @return object A Diff object representing the inverse of the + * original diff. + */ + function reverse () { + $rev = $this; + $rev->edits = array(); + foreach ($this->edits as $edit) { + $rev->edits[] = $edit->reverse(); + } + return $rev; + } + + /** + * Check for empty diff. + * + * @return bool True iff two sequences were identical. + */ + function isEmpty () { + foreach ($this->edits as $edit) { + if ($edit->type != 'copy') + return false; + } + return true; + } + + /** + * Compute the length of the Longest Common Subsequence (LCS). + * + * This is mostly for diagnostic purposed. + * + * @return int The length of the LCS. + */ + function lcs () { + $lcs = 0; + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $lcs += sizeof($edit->orig); + } + return $lcs; + } + + /** + * Get the original set of lines. + * + * This reconstructs the $from_lines parameter passed to the + * constructor. + * + * @return array The original sequence of strings. + */ + function orig() { + $lines = array(); + + foreach ($this->edits as $edit) { + if ($edit->orig) + array_splice($lines, sizeof($lines), 0, $edit->orig); + } + return $lines; + } + + /** + * Get the closing set of lines. + * + * This reconstructs the $to_lines parameter passed to the + * constructor. + * + * @return array The sequence of strings. + */ + function closing() { + $lines = array(); + + foreach ($this->edits as $edit) { + if ($edit->closing) + array_splice($lines, sizeof($lines), 0, $edit->closing); + } + return $lines; + } + + /** + * Check a Diff for validity. + * + * This is here only for debugging purposes. + */ + function _check ($from_lines, $to_lines) { + $fname = 'Diff::_check'; + wfProfileIn( $fname ); + if (serialize($from_lines) != serialize($this->orig())) + trigger_error("Reconstructed original doesn't match", E_USER_ERROR); + if (serialize($to_lines) != serialize($this->closing())) + trigger_error("Reconstructed closing doesn't match", E_USER_ERROR); + + $rev = $this->reverse(); + if (serialize($to_lines) != serialize($rev->orig())) + trigger_error("Reversed original doesn't match", E_USER_ERROR); + if (serialize($from_lines) != serialize($rev->closing())) + trigger_error("Reversed closing doesn't match", E_USER_ERROR); + + + $prevtype = 'none'; + foreach ($this->edits as $edit) { + if ( $prevtype == $edit->type ) + trigger_error("Edit sequence is non-optimal", E_USER_ERROR); + $prevtype = $edit->type; + } + + $lcs = $this->lcs(); + trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE); + wfProfileOut( $fname ); + } +} + +/** + * FIXME: bad name. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class MappedDiff extends Diff +{ + /** + * Constructor. + * + * Computes diff between sequences of strings. + * + * This can be used to compute things like + * case-insensitve diffs, or diffs which ignore + * changes in white-space. + * + * @param $from_lines array An array of strings. + * (Typically these are lines from a file.) + * + * @param $to_lines array An array of strings. + * + * @param $mapped_from_lines array This array should + * have the same size number of elements as $from_lines. + * The elements in $mapped_from_lines and + * $mapped_to_lines are what is actually compared + * when computing the diff. + * + * @param $mapped_to_lines array This array should + * have the same number of elements as $to_lines. + */ + function MappedDiff($from_lines, $to_lines, + $mapped_from_lines, $mapped_to_lines) { + $fname = 'MappedDiff::MappedDiff'; + wfProfileIn( $fname ); + + assert(sizeof($from_lines) == sizeof($mapped_from_lines)); + assert(sizeof($to_lines) == sizeof($mapped_to_lines)); + + $this->Diff($mapped_from_lines, $mapped_to_lines); + + $xi = $yi = 0; + for ($i = 0; $i < sizeof($this->edits); $i++) { + $orig = &$this->edits[$i]->orig; + if (is_array($orig)) { + $orig = array_slice($from_lines, $xi, sizeof($orig)); + $xi += sizeof($orig); + } + + $closing = &$this->edits[$i]->closing; + if (is_array($closing)) { + $closing = array_slice($to_lines, $yi, sizeof($closing)); + $yi += sizeof($closing); + } + } + wfProfileOut( $fname ); + } +} + +/** + * A class to format Diffs + * + * This class formats the diff in classic diff format. + * It is intended that this class be customized via inheritance, + * to obtain fancier outputs. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class DiffFormatter +{ + /** + * Number of leading context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + var $leading_context_lines = 0; + + /** + * Number of trailing context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + var $trailing_context_lines = 0; + + /** + * Format a diff. + * + * @param $diff object A Diff object. + * @return string The formatted output. + */ + function format($diff) { + $fname = 'DiffFormatter::format'; + wfProfileIn( $fname ); + + $xi = $yi = 1; + $block = false; + $context = array(); + + $nlead = $this->leading_context_lines; + $ntrail = $this->trailing_context_lines; + + $this->_start_diff(); + + foreach ($diff->edits as $edit) { + if ($edit->type == 'copy') { + if (is_array($block)) { + if (sizeof($edit->orig) <= $nlead + $ntrail) { + $block[] = $edit; + } + else{ + if ($ntrail) { + $context = array_slice($edit->orig, 0, $ntrail); + $block[] = new _DiffOp_Copy($context); + } + $this->_block($x0, $ntrail + $xi - $x0, + $y0, $ntrail + $yi - $y0, + $block); + $block = false; + } + } + $context = $edit->orig; + } + else { + if (! is_array($block)) { + $context = array_slice($context, sizeof($context) - $nlead); + $x0 = $xi - sizeof($context); + $y0 = $yi - sizeof($context); + $block = array(); + if ($context) + $block[] = new _DiffOp_Copy($context); + } + $block[] = $edit; + } + + if ($edit->orig) + $xi += sizeof($edit->orig); + if ($edit->closing) + $yi += sizeof($edit->closing); + } + + if (is_array($block)) + $this->_block($x0, $xi - $x0, + $y0, $yi - $y0, + $block); + + $end = $this->_end_diff(); + wfProfileOut( $fname ); + return $end; + } + + function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) { + $fname = 'DiffFormatter::_block'; + wfProfileIn( $fname ); + $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); + foreach ($edits as $edit) { + if ($edit->type == 'copy') + $this->_context($edit->orig); + elseif ($edit->type == 'add') + $this->_added($edit->closing); + elseif ($edit->type == 'delete') + $this->_deleted($edit->orig); + elseif ($edit->type == 'change') + $this->_changed($edit->orig, $edit->closing); + else + trigger_error('Unknown edit type', E_USER_ERROR); + } + $this->_end_block(); + wfProfileOut( $fname ); + } + + function _start_diff() { + ob_start(); + } + + function _end_diff() { + $val = ob_get_contents(); + ob_end_clean(); + return $val; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) { + if ($xlen > 1) + $xbeg .= "," . ($xbeg + $xlen - 1); + if ($ylen > 1) + $ybeg .= "," . ($ybeg + $ylen - 1); + + return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; + } + + function _start_block($header) { + echo $header; + } + + function _end_block() { + } + + function _lines($lines, $prefix = ' ') { + foreach ($lines as $line) + echo "$prefix $line\n"; + } + + function _context($lines) { + $this->_lines($lines); + } + + function _added($lines) { + $this->_lines($lines, '>'); + } + function _deleted($lines) { + $this->_lines($lines, '<'); + } + + function _changed($orig, $closing) { + $this->_deleted($orig); + echo "---\n"; + $this->_added($closing); + } +} + + +/** + * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 + * + */ + +define('NBSP', ' '); // iso-8859-x non-breaking space. + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class _HWLDF_WordAccumulator { + function _HWLDF_WordAccumulator () { + $this->_lines = array(); + $this->_line = ''; + $this->_group = ''; + $this->_tag = ''; + } + + function _flushGroup ($new_tag) { + if ($this->_group !== '') { + if ($this->_tag == 'mark') + $this->_line .= '<span class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</span>'; + else + $this->_line .= htmlspecialchars ( $this->_group ); + } + $this->_group = ''; + $this->_tag = $new_tag; + } + + function _flushLine ($new_tag) { + $this->_flushGroup($new_tag); + if ($this->_line != '') + array_push ( $this->_lines, $this->_line ); + else + # make empty lines visible by inserting an NBSP + array_push ( $this->_lines, NBSP ); + $this->_line = ''; + } + + function addWords ($words, $tag = '') { + if ($tag != $this->_tag) + $this->_flushGroup($tag); + + foreach ($words as $word) { + // new-line should only come as first char of word. + if ($word == '') + continue; + if ($word[0] == "\n") { + $this->_flushLine($tag); + $word = substr($word, 1); + } + assert(!strstr($word, "\n")); + $this->_group .= $word; + } + } + + function getLines() { + $this->_flushLine('~done'); + return $this->_lines; + } +} + +/** + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class WordLevelDiff extends MappedDiff +{ + function WordLevelDiff ($orig_lines, $closing_lines) { + $fname = 'WordLevelDiff::WordLevelDiff'; + wfProfileIn( $fname ); + + list ($orig_words, $orig_stripped) = $this->_split($orig_lines); + list ($closing_words, $closing_stripped) = $this->_split($closing_lines); + + $this->MappedDiff($orig_words, $closing_words, + $orig_stripped, $closing_stripped); + wfProfileOut( $fname ); + } + + function _split($lines) { + $fname = 'WordLevelDiff::_split'; + wfProfileIn( $fname ); + + $words = array(); + $stripped = array(); + $first = true; + foreach ( $lines as $line ) { + # If the line is too long, just pretend the entire line is one big word + # This prevents resource exhaustion problems + if ( $first ) { + $first = false; + } else { + $words[] = "\n"; + $stripped[] = "\n"; + } + if ( strlen( $line ) > MAX_DIFF_LINE ) { + $words[] = $line; + $stripped[] = $line; + } else { + if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', + $line, $m)) + { + $words = array_merge( $words, $m[0] ); + $stripped = array_merge( $stripped, $m[1] ); + } + } + } + wfProfileOut( $fname ); + return array($words, $stripped); + } + + function orig () { + $fname = 'WordLevelDiff::orig'; + wfProfileIn( $fname ); + $orig = new _HWLDF_WordAccumulator; + + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $orig->addWords($edit->orig); + elseif ($edit->orig) + $orig->addWords($edit->orig, 'mark'); + } + $lines = $orig->getLines(); + wfProfileOut( $fname ); + return $lines; + } + + function closing () { + $fname = 'WordLevelDiff::closing'; + wfProfileIn( $fname ); + $closing = new _HWLDF_WordAccumulator; + + foreach ($this->edits as $edit) { + if ($edit->type == 'copy') + $closing->addWords($edit->closing); + elseif ($edit->closing) + $closing->addWords($edit->closing, 'mark'); + } + $lines = $closing->getLines(); + wfProfileOut( $fname ); + return $lines; + } +} + +/** + * Wikipedia Table style diff formatter. + * @todo document + * @private + * @package MediaWiki + * @subpackage DifferenceEngine + */ +class TableDiffFormatter extends DiffFormatter +{ + function TableDiffFormatter() { + $this->leading_context_lines = 2; + $this->trailing_context_lines = 2; + } + + function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { + $r = '<tr><td colspan="2" align="left"><strong><!--LINE '.$xbeg."--></strong></td>\n" . + '<td colspan="2" align="left"><strong><!--LINE '.$ybeg."--></strong></td></tr>\n"; + return $r; + } + + function _start_block( $header ) { + echo $header; + } + + function _end_block() { + } + + function _lines( $lines, $prefix=' ', $color='white' ) { + } + + # HTML-escape parameter before calling this + function addedLine( $line ) { + return "<td>+</td><td class='diff-addedline'>{$line}</td>"; + } + + # HTML-escape parameter before calling this + function deletedLine( $line ) { + return "<td>-</td><td class='diff-deletedline'>{$line}</td>"; + } + + # HTML-escape parameter before calling this + function contextLine( $line ) { + return "<td> </td><td class='diff-context'>{$line}</td>"; + } + + function emptyLine() { + return '<td colspan="2"> </td>'; + } + + function _added( $lines ) { + foreach ($lines as $line) { + echo '<tr>' . $this->emptyLine() . + $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + } + } + + function _deleted($lines) { + foreach ($lines as $line) { + echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) . + $this->emptyLine() . "</tr>\n"; + } + } + + function _context( $lines ) { + foreach ($lines as $line) { + echo '<tr>' . + $this->contextLine( htmlspecialchars ( $line ) ) . + $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + } + } + + function _changed( $orig, $closing ) { + $fname = 'TableDiffFormatter::_changed'; + wfProfileIn( $fname ); + + $diff = new WordLevelDiff( $orig, $closing ); + $del = $diff->orig(); + $add = $diff->closing(); + + # Notice that WordLevelDiff returns HTML-escaped output. + # Hence, we will be calling addedLine/deletedLine without HTML-escaping. + + while ( $line = array_shift( $del ) ) { + $aline = array_shift( $add ); + echo '<tr>' . $this->deletedLine( $line ) . + $this->addedLine( $aline ) . "</tr>\n"; + } + foreach ($add as $line) { # If any leftovers + echo '<tr>' . $this->emptyLine() . + $this->addedLine( $line ) . "</tr>\n"; + } + wfProfileOut( $fname ); + } +} + +?> diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php new file mode 100644 index 00000000..b857fa66 --- /dev/null +++ b/includes/DjVuImage.php @@ -0,0 +1,214 @@ +<?php +/** + * Support for detecting/validating DjVu image files and getting + * some basic file metadata (resolution etc) + * + * File format docs are available in source package for DjVuLibre: + * http://djvulibre.djvuzone.org/ + * + * + * Copyright (C) 2006 Brion Vibber <brion@pobox.com> + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @package MediaWiki + */ + +class DjVuImage { + function __construct( $filename ) { + $this->mFilename = $filename; + } + + /** + * Check if the given file is indeed a valid DjVu image file + * @return bool + */ + public function isValid() { + $info = $this->getInfo(); + return $info !== false; + } + + + /** + * Return data in the style of getimagesize() + * @return array or false on failure + */ + public function getImageSize() { + $data = $this->getInfo(); + + if( $data !== false ) { + $width = $data['width']; + $height = $data['height']; + + return array( $width, $height, 'DjVu', + "width=\"$width\" height=\"$height\"" ); + } + return false; + } + + // --------- + + /** + * For debugging; dump the IFF chunk structure + */ + function dump() { + $file = fopen( $this->mFilename, 'rb' ); + $header = fread( $file, 12 ); + extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); + echo "$chunk $chunkLength\n"; + $this->dumpForm( $file, $chunkLength, 1 ); + fclose( $file ); + } + + private function dumpForm( $file, $length, $indent ) { + $start = ftell( $file ); + $secondary = fread( $file, 4 ); + echo str_repeat( ' ', $indent * 4 ) . "($secondary)\n"; + while( ftell( $file ) - $start < $length ) { + $chunkHeader = fread( $file, 8 ); + if( $chunkHeader == '' ) { + break; + } + extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); + echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; + + if( $chunk == 'FORM' ) { + $this->dumpForm( $file, $chunkLength, $indent + 1 ); + } else { + fseek( $file, $chunkLength, SEEK_CUR ); + if( $chunkLength & 1 == 1 ) { + // Padding byte between chunks + fseek( $file, 1, SEEK_CUR ); + } + } + } + } + + function getInfo() { + $file = fopen( $this->mFilename, 'rb' ); + if( $file === false ) { + wfDebug( __METHOD__ . ": missing or failed file read\n" ); + return false; + } + + $header = fread( $file, 16 ); + $info = false; + + if( strlen( $header ) < 16 ) { + wfDebug( __METHOD__ . ": too short file header\n" ); + } else { + extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); + + if( $magic != 'AT&T' ) { + wfDebug( __METHOD__ . ": not a DjVu file\n" ); + } elseif( $subtype == 'DJVU' ) { + // Single-page document + $info = $this->getPageInfo( $file, $formLength ); + } elseif( $subtype == 'DJVM' ) { + // Multi-page document + $info = $this->getMultiPageInfo( $file, $formLength ); + } else { + wfDebug( __METHOD__ . ": unrecognized DJVU file type '$formType'\n" ); + } + } + fclose( $file ); + return $info; + } + + private function readChunk( $file ) { + $header = fread( $file, 8 ); + if( strlen( $header ) < 8 ) { + return array( false, 0 ); + } else { + extract( unpack( 'a4chunk/Nlength', $header ) ); + return array( $chunk, $length ); + } + } + + private function skipChunk( $file, $chunkLength ) { + fseek( $file, $chunkLength, SEEK_CUR ); + + if( $chunkLength & 0x01 == 1 && !feof( $file ) ) { + // padding byte + fseek( $file, 1, SEEK_CUR ); + } + } + + private function getMultiPageInfo( $file, $formLength ) { + // For now, we'll just look for the first page in the file + // and report its information, hoping others are the same size. + $start = ftell( $file ); + do { + list( $chunk, $length ) = $this->readChunk( $file ); + if( !$chunk ) { + break; + } + + if( $chunk == 'FORM' ) { + $subtype = fread( $file, 4 ); + if( $subtype == 'DJVU' ) { + wfDebug( __METHOD__ . ": found first subpage\n" ); + return $this->getPageInfo( $file, $length ); + } + $this->skipChunk( $file, $length - 4 ); + } else { + wfDebug( __METHOD__ . ": skipping '$chunk' chunk\n" ); + $this->skipChunk( $file, $length ); + } + } while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength ); + + wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" ); + return false; + } + + private function getPageInfo( $file, $formLength ) { + list( $chunk, $length ) = $this->readChunk( $file ); + if( $chunk != 'INFO' ) { + wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" ); + return false; + } + + if( $length < 9 ) { + wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" ); + return false; + } + $data = fread( $file, $length ); + if( strlen( $data ) < $length ) { + wfDebug( __METHOD__ . ": INFO chunk cut off\n" ); + return false; + } + + extract( unpack( + 'nwidth/' . + 'nheight/' . + 'Cminor/' . + 'Cmajor/' . + 'vresolution/' . + 'Cgamma', $data ) ); + # Newer files have rotation info in byte 10, but we don't use it yet. + + return array( + 'width' => $width, + 'height' => $height, + 'version' => "$major.$minor", + 'resolution' => $resolution, + 'gamma' => $gamma / 10.0 ); + } +} + + +?>
\ No newline at end of file diff --git a/includes/EditPage.php b/includes/EditPage.php new file mode 100644 index 00000000..d43a1202 --- /dev/null +++ b/includes/EditPage.php @@ -0,0 +1,1864 @@ +<?php +/** + * Contain the EditPage class + * @package MediaWiki + */ + +/** + * Splitting edit page/HTML interface from Article... + * The actual database and text munging is still in Article, + * but it should get easier to call those from alternate + * interfaces. + * + * @package MediaWiki + */ + +class EditPage { + var $mArticle; + var $mTitle; + var $mMetaData = ''; + var $isConflict = false; + var $isCssJsSubpage = false; + var $deletedSinceEdit = false; + var $formtype; + var $firsttime; + var $lastDelete; + var $mTokenOk = false; + var $mTriedSave = false; + var $tooBig = false; + var $kblength = false; + var $missingComment = false; + var $missingSummary = false; + var $allowBlankSummary = false; + var $autoSumm = ''; + var $hookError = ''; + + # Form values + var $save = false, $preview = false, $diff = false; + var $minoredit = false, $watchthis = false, $recreate = false; + var $textbox1 = '', $textbox2 = '', $summary = ''; + var $edittime = '', $section = '', $starttime = ''; + var $oldid = 0, $editintro = '', $scrolltop = null; + + /** + * @todo document + * @param $article + */ + function EditPage( $article ) { + $this->mArticle =& $article; + global $wgTitle; + $this->mTitle =& $wgTitle; + } + + /** + * Fetch initial editing page content. + */ + private function getContent() { + global $wgRequest, $wgParser; + + # Get variables from query string :P + $section = $wgRequest->getVal( 'section' ); + $preload = $wgRequest->getVal( 'preload' ); + + wfProfileIn( __METHOD__ ); + + $text = ''; + if( !$this->mTitle->exists() ) { + + # If requested, preload some text. + $text = $this->getPreloadedText( $preload ); + + # We used to put MediaWiki:Newarticletext here if + # $text was empty at this point. + # This is now shown above the edit box instead. + } else { + // FIXME: may be better to use Revision class directly + // But don't mess with it just yet. Article knows how to + // fetch the page record from the high-priority server, + // which is needed to guarantee we don't pick up lagged + // information. + + $text = $this->mArticle->getContent(); + + if( $section != '' ) { + if( $section == 'new' ) { + $text = $this->getPreloadedText( $preload ); + } else { + $text = $wgParser->getSection( $text, $section ); + } + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Get the contents of a page from its title and remove includeonly tags + * + * @param $preload String: the title of the page. + * @return string The contents of the page. + */ + private function getPreloadedText($preload) { + if ( $preload === '' ) + return ''; + else { + $preloadTitle = Title::newFromText( $preload ); + if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) { + $rev=Revision::newFromTitle($preloadTitle); + if ( is_object( $rev ) ) { + $text = $rev->getText(); + // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing + // its own mini-parser! -ævar + $text = preg_replace( '~</?includeonly>~', '', $text ); + return $text; + } else + return ''; + } + } + } + + /** + * This is the function that extracts metadata from the article body on the first view. + * To turn the feature on, set $wgUseMetadataEdit = true ; in LocalSettings + * and set $wgMetadataWhitelist to the *full* title of the template whitelist + */ + function extractMetaDataFromArticle () { + global $wgUseMetadataEdit , $wgMetadataWhitelist , $wgLang ; + $this->mMetaData = '' ; + if ( !$wgUseMetadataEdit ) return ; + if ( $wgMetadataWhitelist == '' ) return ; + $s = '' ; + $t = $this->getContent(); + + # MISSING : <nowiki> filtering + + # Categories and language links + $t = explode ( "\n" , $t ) ; + $catlow = strtolower ( $wgLang->getNsText ( NS_CATEGORY ) ) ; + $cat = $ll = array() ; + foreach ( $t AS $key => $x ) + { + $y = trim ( strtolower ( $x ) ) ; + while ( substr ( $y , 0 , 2 ) == '[[' ) + { + $y = explode ( ']]' , trim ( $x ) ) ; + $first = array_shift ( $y ) ; + $first = explode ( ':' , $first ) ; + $ns = array_shift ( $first ) ; + $ns = trim ( str_replace ( '[' , '' , $ns ) ) ; + if ( strlen ( $ns ) == 2 OR strtolower ( $ns ) == $catlow ) + { + $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]' ; + if ( strtolower ( $ns ) == $catlow ) $cat[] = $add ; + else $ll[] = $add ; + $x = implode ( ']]' , $y ) ; + $t[$key] = $x ; + $y = trim ( strtolower ( $x ) ) ; + } + } + } + if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n" ; + if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n" ; + $t = implode ( "\n" , $t ) ; + + # Load whitelist + $sat = array () ; # stand-alone-templates; must be lowercase + $wl_title = Title::newFromText ( $wgMetadataWhitelist ) ; + $wl_article = new Article ( $wl_title ) ; + $wl = explode ( "\n" , $wl_article->getContent() ) ; + foreach ( $wl AS $x ) + { + $isentry = false ; + $x = trim ( $x ) ; + while ( substr ( $x , 0 , 1 ) == '*' ) + { + $isentry = true ; + $x = trim ( substr ( $x , 1 ) ) ; + } + if ( $isentry ) + { + $sat[] = strtolower ( $x ) ; + } + + } + + # Templates, but only some + $t = explode ( '{{' , $t ) ; + $tl = array () ; + foreach ( $t AS $key => $x ) + { + $y = explode ( '}}' , $x , 2 ) ; + if ( count ( $y ) == 2 ) + { + $z = $y[0] ; + $z = explode ( '|' , $z ) ; + $tn = array_shift ( $z ) ; + if ( in_array ( strtolower ( $tn ) , $sat ) ) + { + $tl[] = '{{' . $y[0] . '}}' ; + $t[$key] = $y[1] ; + $y = explode ( '}}' , $y[1] , 2 ) ; + } + else $t[$key] = '{{' . $x ; + } + else if ( $key != 0 ) $t[$key] = '{{' . $x ; + else $t[$key] = $x ; + } + if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ) ; + $t = implode ( '' , $t ) ; + + $t = str_replace ( "\n\n\n" , "\n" , $t ) ; + $this->mArticle->mContent = $t ; + $this->mMetaData = $s ; + } + + function submit() { + $this->edit(); + } + + /** + * This is the function that gets called for "action=edit". It + * sets up various member variables, then passes execution to + * another function, usually showEditForm() + * + * The edit form is self-submitting, so that when things like + * preview and edit conflicts occur, we get the same form back + * with the extra stuff added. Only when the final submission + * is made and all is well do we actually save and redirect to + * the newly-edited page. + */ + function edit() { + global $wgOut, $wgUser, $wgRequest, $wgTitle; + global $wgEmailConfirmToEdit; + + if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + return; + + $fname = 'EditPage::edit'; + wfProfileIn( $fname ); + wfDebug( "$fname: enter\n" ); + + // this is not an article + $wgOut->setArticleFlag(false); + + $this->importFormData( $wgRequest ); + $this->firsttime = false; + + if( $this->live ) { + $this->livePreview(); + wfProfileOut( $fname ); + return; + } + + if ( ! $this->mTitle->userCanEdit() ) { + wfDebug( "$fname: user can't edit\n" ); + $wgOut->readOnlyPage( $this->getContent(), true ); + wfProfileOut( $fname ); + return; + } + wfDebug( "$fname: Checking blocks\n" ); + if ( !$this->preview && !$this->diff && $wgUser->isBlockedFrom( $this->mTitle, !$this->save ) ) { + # When previewing, don't check blocked state - will get caught at save time. + # Also, check when starting edition is done against slave to improve performance. + wfDebug( "$fname: user is blocked\n" ); + $this->blockedPage(); + wfProfileOut( $fname ); + return; + } + if ( !$wgUser->isAllowed('edit') ) { + if ( $wgUser->isAnon() ) { + wfDebug( "$fname: user must log in\n" ); + $this->userNotLoggedInPage(); + wfProfileOut( $fname ); + return; + } else { + wfDebug( "$fname: read-only page\n" ); + $wgOut->readOnlyPage( $this->getContent(), true ); + wfProfileOut( $fname ); + return; + } + } + if ($wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed()) { + wfDebug("$fname: user must confirm e-mail address\n"); + $this->userNotConfirmedPage(); + wfProfileOut($fname); + return; + } + if ( !$this->mTitle->userCanCreate() && !$this->mTitle->exists() ) { + wfDebug( "$fname: no create permission\n" ); + $this->noCreatePermission(); + wfProfileOut( $fname ); + return; + } + if ( wfReadOnly() ) { + wfDebug( "$fname: read-only mode is engaged\n" ); + if( $this->save || $this->preview ) { + $this->formtype = 'preview'; + } else if ( $this->diff ) { + $this->formtype = 'diff'; + } else { + $wgOut->readOnlyPage( $this->getContent() ); + wfProfileOut( $fname ); + return; + } + } else { + if ( $this->save ) { + $this->formtype = 'save'; + } else if ( $this->preview ) { + $this->formtype = 'preview'; + } else if ( $this->diff ) { + $this->formtype = 'diff'; + } else { # First time through + $this->firsttime = true; + if( $this->previewOnOpen() ) { + $this->formtype = 'preview'; + } else { + $this->extractMetaDataFromArticle () ; + $this->formtype = 'initial'; + } + } + } + + wfProfileIn( "$fname-business-end" ); + + $this->isConflict = false; + // css / js subpages of user pages get a special treatment + $this->isCssJsSubpage = $wgTitle->isCssJsSubpage(); + $this->isValidCssJsSubpage = $wgTitle->isValidCssJsSubpage(); + + /* Notice that we can't use isDeleted, because it returns true if article is ever deleted + * no matter it's current state + */ + $this->deletedSinceEdit = false; + if ( $this->edittime != '' ) { + /* Note that we rely on logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. This is done on every preview and save request. Move it further down + * to only perform it on saves + */ + if ( $this->mTitle->isDeleted() ) { + $this->lastDelete = $this->getLastDelete(); + if ( !is_null($this->lastDelete) ) { + $deletetime = $this->lastDelete->log_timestamp; + if ( ($deletetime - $this->starttime) > 0 ) { + $this->deletedSinceEdit = true; + } + } + } + } + + if(!$this->mTitle->getArticleID() && ('initial' == $this->formtype || $this->firsttime )) { # new article + $this->showIntro(); + } + if( $this->mTitle->isTalkPage() ) { + $wgOut->addWikiText( wfMsg( 'talkpagetext' ) ); + } + + # Attempt submission here. This will check for edit conflicts, + # and redundantly check for locked database, blocked IPs, etc. + # that edit() already checked just in case someone tries to sneak + # in the back door with a hand-edited submission URL. + + if ( 'save' == $this->formtype ) { + if ( !$this->attemptSave() ) { + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + return; + } + } + + # First time through: get contents, set time for conflict + # checking, etc. + if ( 'initial' == $this->formtype || $this->firsttime ) { + $this->initialiseForm(); + if( !$this->mTitle->getArticleId() ) + wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); + } + + $this->showEditForm(); + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + } + + /** + * Return true if this page should be previewed when the edit form + * is initially opened. + * @return bool + * @private + */ + function previewOnOpen() { + global $wgUser; + return $this->section != 'new' && + ( ( $wgUser->getOption( 'previewonfirst' ) && $this->mTitle->exists() ) || + ( $this->mTitle->getNamespace() == NS_CATEGORY && + !$this->mTitle->exists() ) ); + } + + /** + * @todo document + * @param $request + */ + function importFormData( &$request ) { + global $wgLang, $wgUser; + $fname = 'EditPage::importFormData'; + wfProfileIn( $fname ); + + if( $request->wasPosted() ) { + # These fields need to be checked for encoding. + # Also remove trailing whitespace, but don't remove _initial_ + # whitespace from the text boxes. This may be significant formatting. + $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); + $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); + $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); + # Truncate for whole multibyte characters. +5 bytes for ellipsis + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + + $this->edittime = $request->getVal( 'wpEdittime' ); + $this->starttime = $request->getVal( 'wpStarttime' ); + + $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); + + if( is_null( $this->edittime ) ) { + # If the form is incomplete, force to preview. + wfDebug( "$fname: Form data appears to be incomplete\n" ); + wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); + $this->preview = true; + } else { + /* Fallback for live preview */ + $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' ); + $this->diff = $request->getCheck( 'wpDiff' ); + + // Remember whether a save was requested, so we can indicate + // if we forced preview due to session failure. + $this->mTriedSave = !$this->preview; + + if ( $this->tokenOk( $request ) ) { + # Some browsers will not report any submit button + # if the user hits enter in the comment box. + # The unmarked state will be assumed to be a save, + # if the form seems otherwise complete. + wfDebug( "$fname: Passed token check.\n" ); + } else { + # Page might be a hack attempt posted from + # an external site. Preview instead of saving. + wfDebug( "$fname: Failed token check; forcing preview\n" ); + $this->preview = true; + } + } + $this->save = ! ( $this->preview OR $this->diff ); + if( !preg_match( '/^\d{14}$/', $this->edittime )) { + $this->edittime = null; + } + + if( !preg_match( '/^\d{14}$/', $this->starttime )) { + $this->starttime = null; + } + + $this->recreate = $request->getCheck( 'wpRecreate' ); + + $this->minoredit = $request->getCheck( 'wpMinoredit' ); + $this->watchthis = $request->getCheck( 'wpWatchthis' ); + + # Don't force edit summaries when a user is editing their own user or talk page + if( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && $this->mTitle->getText() == $wgUser->getName() ) { + $this->allowBlankSummary = true; + } else { + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ); + } + + $this->autoSumm = $request->getText( 'wpAutoSummary' ); + } else { + # Not a posted form? Start with nothing. + wfDebug( "$fname: Not a posted form.\n" ); + $this->textbox1 = ''; + $this->textbox2 = ''; + $this->mMetaData = ''; + $this->summary = ''; + $this->edittime = ''; + $this->starttime = wfTimestampNow(); + $this->preview = false; + $this->save = false; + $this->diff = false; + $this->minoredit = false; + $this->watchthis = false; + $this->recreate = false; + } + + $this->oldid = $request->getInt( 'oldid' ); + + # Section edit can come from either the form or a link + $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); + + $this->live = $request->getCheck( 'live' ); + $this->editintro = $request->getText( 'editintro' ); + + wfProfileOut( $fname ); + } + + /** + * Make sure the form isn't faking a user's credentials. + * + * @param $request WebRequest + * @return bool + * @private + */ + function tokenOk( &$request ) { + global $wgUser; + if( $wgUser->isAnon() ) { + # Anonymous users may not have a session + # open. Don't tokenize. + $this->mTokenOk = true; + } else { + $this->mTokenOk = $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); + } + return $this->mTokenOk; + } + + /** */ + function showIntro() { + global $wgOut, $wgUser; + $addstandardintro=true; + if($this->editintro) { + $introtitle=Title::newFromText($this->editintro); + if(isset($introtitle) && $introtitle->userCanRead()) { + $rev=Revision::newFromTitle($introtitle); + if($rev) { + $wgOut->addSecondaryWikiText($rev->getText()); + $addstandardintro=false; + } + } + } + if($addstandardintro) { + if ( $wgUser->isLoggedIn() ) + $wgOut->addWikiText( wfMsg( 'newarticletext' ) ); + else + $wgOut->addWikiText( wfMsg( 'newarticletextanon' ) ); + } + } + + /** + * Attempt submission + * @return bool false if output is done, true if the rest of the form should be displayed + */ + function attemptSave() { + global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut; + global $wgMaxArticleSize; + + $fname = 'EditPage::attemptSave'; + wfProfileIn( $fname ); + wfProfileIn( "$fname-checks" ); + + # Reintegrate metadata + if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; + $this->mMetaData = '' ; + + # Check for spam + if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { + $this->spamPage ( $matches[0] ); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) { + # Error messages or other handling should be performed by the filter function + wfProfileOut( $fname ); + wfProfileOut( "$fname-checks" ); + return false; + } + if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) { + # Error messages etc. could be handled within the hook... + wfProfileOut( $fname ); + wfProfileOut( "$fname-checks" ); + return false; + } elseif( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + wfProfileOut( "$fname-checks " ); + wfProfileOut( $fname ); + return true; + } + if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + # Check block state against master, thus 'false'. + $this->blockedPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + // Error will be displayed by showEditForm() + $this->tooBig = true; + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } + + if ( !$wgUser->isAllowed('edit') ) { + if ( $wgUser->isAnon() ) { + $this->userNotLoggedInPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + else { + $wgOut->readOnlyPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + } + + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + if ( $wgUser->pingLimiter() ) { + $wgOut->rateLimited(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + + # If the article has been deleted while editing, don't save it without + # confirmation + if ( $this->deletedSinceEdit && !$this->recreate ) { + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } + + wfProfileOut( "$fname-checks" ); + + # If article is new, insert it. + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); + if ( 0 == $aid ) { + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCanCreate() ) { + wfDebug( "$fname: no create permission\n" ); + $this->noCreatePermission(); + wfProfileOut( $fname ); + return; + } + + # Don't save a new article if it's blank. + if ( ( '' == $this->textbox1 ) ) { + $wgOut->redirect( $this->mTitle->getFullURL() ); + wfProfileOut( $fname ); + return false; + } + + # If no edit comment was given when creating a new page, and what's being + # created is a redirect, be smart and fill in a neat auto-comment + if( $this->summary == '' ) { + $rt = Title::newFromRedirect( $this->textbox1 ); + if( is_object( $rt ) ) + $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); + } + + $isComment=($this->section=='new'); + $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, + $this->minoredit, $this->watchthis, false, $isComment); + + wfProfileOut( $fname ); + return false; + } + + # Article exists. Check for edit conflict. + + $this->mArticle->clear(); # Force reload of dates, etc. + $this->mArticle->forUpdate( true ); # Lock the article + + if( $this->mArticle->getTimestamp() != $this->edittime ) { + $this->isConflict = true; + if( $this->section == 'new' ) { + if( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( "EditPage::editForm duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( "EditPage::editForm conflict suppressed; new section\n" ); + } + } + } + $userid = $wgUser->getID(); + + if ( $this->isConflict) { + wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . + $this->mArticle->getTimestamp() . "'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); + } + else { + wfDebug( "EditPage::editForm getting section '$this->section'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); + } + if( is_null( $text ) ) { + wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $text = $this->textbox1; + } + + # Suppress edit conflict with self, except for section edits where merging is required. + if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) { + wfDebug( "Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } else { + # switch from section editing to normal editing in edit conflict + if($this->isConflict) { + # Attempt merge + if( $this->mergeChangesInto( $text ) ){ + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( "Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( "Keeping edit conflict, failed merge.\n" ); + } + } + } + + if ( $this->isConflict ) { + wfProfileOut( $fname ); + return true; + } + + # If no edit comment was given when turning a page into a redirect, be smart + # and fill in a neat auto-comment + if( $this->summary == '' ) { + $rt = Title::newFromRedirect( $this->textbox1 ); + if( is_object( $rt ) ) + $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); + } + + # Handle the user preference to force summaries here + if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + wfProfileOut( $fname ); + return( true ); + } + } + + # All's well + wfProfileIn( "$fname-sectionanchor" ); + $sectionanchor = ''; + if( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + return true; + } + if( $this->summary != '' ) { + $sectionanchor = $this->sectionAnchor( $this->summary ); + } + } elseif( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # we can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this + if($hasmatch and strlen($matches[2]) > 0) { + $sectionanchor = $this->sectionAnchor( $matches[2] ); + } + } + wfProfileOut( "$fname-sectionanchor" ); + + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $text; + $this->section = ''; + + // Check for length errors again now that the section is merged in + $this->kblength = (int)(strlen( $text ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + $this->tooBig = true; + wfProfileOut( $fname ); + return true; + } + + # update the article here + if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, + $this->watchthis, '', $sectionanchor ) ) { + wfProfileOut( $fname ); + return false; + } else { + $this->isConflict = true; + } + wfProfileOut( $fname ); + return true; + } + + /** + * Initialise form fields in the object + * Called on the first invocation, e.g. when a user clicks an edit link + */ + function initialiseForm() { + $this->edittime = $this->mArticle->getTimestamp(); + $this->textbox1 = $this->getContent(); + $this->summary = ''; + if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) + $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ) ; + wfProxyCheck(); + } + + /** + * Send the edit form and related headers to $wgOut + * @param $formCallback Optional callable that takes an OutputPage + * parameter; will be called during form output + * near the top, for captchas and the like. + */ + function showEditForm( $formCallback=null ) { + global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize; + + $fname = 'EditPage::showEditForm'; + wfProfileIn( $fname ); + + $sk =& $wgUser->getSkin(); + + wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; + + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + # Enabled article-related sidebar, toplinks, etc. + $wgOut->setArticleRelated( true ); + + if ( $this->isConflict ) { + $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); + $wgOut->setPageTitle( $s ); + $wgOut->addWikiText( wfMsg( 'explainconflict' ) ); + + $this->textbox2 = $this->textbox1; + $this->textbox1 = $this->getContent(); + $this->edittime = $this->mArticle->getTimestamp(); + } else { + + if( $this->section != '' ) { + if( $this->section == 'new' ) { + $s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() ); + } else { + $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); + if( !$this->preview && !$this->diff ) { + preg_match( "/^(=+)(.+)\\1/mi", + $this->textbox1, + $matches ); + if( !empty( $matches[2] ) ) { + $this->summary = "/* ". trim($matches[2])." */ "; + } + } + } + } else { + $s = wfMsg( 'editing', $this->mTitle->getPrefixedText() ); + } + $wgOut->setPageTitle( $s ); + + if ( $this->missingComment ) { + $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); + } + + if( $this->missingSummary ) { + $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); + } + + if( !$this->hookError == '' ) { + $wgOut->addWikiText( $this->hookError ); + } + + if ( !$this->checkUnicodeCompliantBrowser() ) { + $wgOut->addWikiText( wfMsg( 'nonunicodebrowser') ); + } + if ( isset( $this->mArticle ) + && isset( $this->mArticle->mRevision ) + && !$this->mArticle->mRevision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); + $wgOut->addWikiText( wfMsg( 'editingold' ) ); + } + } + + if( wfReadOnly() ) { + $wgOut->addWikiText( wfMsg( 'readonlywarning' ) ); + } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) { + $wgOut->addWikiText( wfMsg( 'anoneditwarning' ) ); + } else { + if( $this->isCssJsSubpage && $this->formtype != 'preview' ) { + # Check the skin exists + if( $this->isValidCssJsSubpage ) { + $wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); + } + } + } + + if( $this->mTitle->isProtected( 'edit' ) ) { + # Is the protection due to the namespace, e.g. interface text? + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # Yes; remind the user + $notice = wfMsg( 'editinginterface' ); + } elseif( $this->mTitle->isSemiProtected() ) { + # No; semi protected + $notice = wfMsg( 'semiprotectedpagewarning' ); + if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) { + $notice = ''; + } + } else { + # No; regular protection + $notice = wfMsg( 'protectedpagewarning' ); + } + $wgOut->addWikiText( $notice ); + } + + if ( $this->kblength === false ) { + $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + } + if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { + $wgOut->addWikiText( wfMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ) ); + } elseif( $this->kblength > 29 ) { + $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) ); + } + + $rows = $wgUser->getIntOption( 'rows' ); + $cols = $wgUser->getIntOption( 'cols' ); + + $ew = $wgUser->getOption( 'editwidth' ); + if ( $ew ) $ew = " style=\"width:100%\""; + else $ew = ''; + + $q = 'action=submit'; + #if ( "no" == $redirect ) { $q .= "&redirect=no"; } + $action = $this->mTitle->escapeLocalURL( $q ); + + $summary = wfMsg('summary'); + $subject = wfMsg('subject'); + $minor = wfMsgExt('minoredit', array('parseinline')); + $watchthis = wfMsgExt('watchthis', array('parseinline')); + + $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), + wfMsgExt('cancel', array('parseinline')) ); + $edithelpurl = $sk->makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); + $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. + htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. + htmlspecialchars( wfMsg( 'newwindow' ) ); + + global $wgRightsText; + $copywarn = "<div id=\"editpage-copywarn\">\n" . + wfMsg( $wgRightsText ? 'copyrightwarning' : 'copyrightwarning2', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', + $wgRightsText ) . "\n</div>"; + + if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { + # prepare toolbar for edit buttons + $toolbar = $this->getEditToolbar(); + } else { + $toolbar = ''; + } + + // activate checkboxes if user wants them to be always active + if( !$this->preview && !$this->diff ) { + # Sort out the "watch" checkbox + if( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } + + if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; + } + + $minoredithtml = ''; + + if ( $wgUser->isAllowed('minoredit') ) { + $minoredithtml = + "<input tabindex='3' type='checkbox' value='1' name='wpMinoredit'".($this->minoredit?" checked='checked'":""). + " accesskey='".wfMsg('accesskey-minoredit')."' id='wpMinoredit' />\n". + "<label for='wpMinoredit' title='".wfMsg('tooltip-minoredit')."'>{$minor}</label>\n"; + } + + $watchhtml = ''; + + if ( $wgUser->isLoggedIn() ) { + $watchhtml = "<input tabindex='4' type='checkbox' name='wpWatchthis'". + ($this->watchthis?" checked='checked'":""). + " accesskey=\"".htmlspecialchars(wfMsg('accesskey-watch'))."\" id='wpWatchthis' />\n". + "<label for='wpWatchthis' title=\"" . + htmlspecialchars(wfMsg('tooltip-watch'))."\">{$watchthis}</label>\n"; + } + + $checkboxhtml = $minoredithtml . $watchhtml; + + if ( $wgUser->getOption( 'previewontop' ) ) { + + if ( 'preview' == $this->formtype ) { + $this->showPreview(); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); + } + + if ( 'diff' == $this->formtype ) { + $wgOut->addHTML( $this->getDiff() ); + } + } + + + # if this is a comment, show a subject line at the top, which is also the edit summary. + # Otherwise, show a summary field at the bottom + $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME + if( $this->section == 'new' ) { + $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $editsummary = ''; + } else { + $commentsubject = ''; + $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + } + + # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display + if( !$this->preview && !$this->diff ) { + $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); + } + $templates = $this->formatTemplates(); + + global $wgUseMetadataEdit ; + if ( $wgUseMetadataEdit ) { + $metadata = $this->mMetaData ; + $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; + $top = wfMsgWikiHtml( 'metadata_help' ); + $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ; + } + else $metadata = "" ; + + $hidden = ''; + $recreate = ''; + if ($this->deletedSinceEdit) { + if ( 'save' != $this->formtype ) { + $wgOut->addWikiText( wfMsg('deletedwhileediting')); + } else { + // Hide the toolbar and edit area, use can click preview to get it back + // Add an confirmation checkbox and explanation. + $toolbar = ''; + $hidden = 'type="hidden" style="display:none;"'; + $recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment )); + $recreate .= + "<br /><input tabindex='1' type='checkbox' value='1' name='wpRecreate' id='wpRecreate' />". + "<label for='wpRecreate' title='".wfMsg('tooltip-recreate')."'>". wfMsg('recreate')."</label>"; + } + } + + $temp = array( + 'id' => 'wpSave', + 'name' => 'wpSave', + 'type' => 'submit', + 'tabindex' => '5', + 'value' => wfMsg('savearticle'), + 'accesskey' => wfMsg('accesskey-save'), + 'title' => wfMsg('tooltip-save'), + ); + $buttons['save'] = wfElement('input', $temp, ''); + $temp = array( + 'id' => 'wpDiff', + 'name' => 'wpDiff', + 'type' => 'submit', + 'tabindex' => '7', + 'value' => wfMsg('showdiff'), + 'accesskey' => wfMsg('accesskey-diff'), + 'title' => wfMsg('tooltip-diff'), + ); + $buttons['diff'] = wfElement('input', $temp, ''); + + global $wgLivePreview; + if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showpreview'), + 'accesskey' => '', + 'title' => wfMsg('tooltip-preview'), + 'style' => 'display: none;', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $temp = array( + 'id' => 'wpLivePreview', + 'name' => 'wpLivePreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showlivepreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => '', + 'onclick' => $this->doLivePreviewScript(), + ); + $buttons['live'] = wfElement('input', $temp, ''); + } else { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => '6', + 'value' => wfMsg('showpreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => wfMsg('tooltip-preview'), + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['live'] = ''; + } + + $safemodehtml = $this->checkUnicodeCompliantBrowser() + ? "" + : "<input type='hidden' name=\"safemode\" value='1' />\n"; + + $wgOut->addHTML( <<<END +{$toolbar} +<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> +END +); + + if( is_callable( $formCallback ) ) { + call_user_func_array( $formCallback, array( &$wgOut ) ); + } + + // Put these up at the top to ensure they aren't lost on early form submission + $wgOut->addHTML( " +<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> +<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n +<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n +<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + + $wgOut->addHTML( <<<END +$recreate +{$commentsubject} +<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' +cols='{$cols}'{$ew} $hidden> +END +. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ) . +" +</textarea> + " ); + + $wgOut->addWikiText( $copywarn ); + $wgOut->addHTML( " +{$metadata} +{$editsummary} +{$checkboxhtml} +{$safemodehtml} +"); + + $wgOut->addHTML( +"<div class='editButtons'> + {$buttons['save']} + {$buttons['preview']} + {$buttons['live']} + {$buttons['diff']} + <span class='editHelp'>{$cancel} | {$edithelp}</span> +</div><!-- editButtons --> +</div><!-- editOptions -->"); + + $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); + + $wgOut->addHTML( " +<div class='templatesUsed'> +{$templates} +</div> +" ); + + if ( $wgUser->isLoggedIn() ) { + /** + * To make it harder for someone to slip a user a page + * which submits an edit form to the wiki without their + * knowledge, a random token is associated with the login + * session. If it's not passed back with the submission, + * we won't save the page, or render user JavaScript and + * CSS previews. + */ + $token = htmlspecialchars( $wgUser->editToken() ); + $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); + } + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag here. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + if( $this->missingSummary ) { + $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" ); + } + + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in a hidden field. + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); + + if ( $this->isConflict ) { + require_once( "DifferenceEngine.php" ); + $wgOut->addWikiText( '==' . wfMsg( "yourdiff" ) . '==' ); + + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $this->textbox2, $this->textbox1 ); + $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); + + $wgOut->addWikiText( '==' . wfMsg( "yourtext" ) . '==' ); + $wgOut->addHTML( "<textarea tabindex=6 id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}' wrap='virtual'>" + . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); + } + $wgOut->addHTML( "</form>\n" ); + if ( !$wgUser->getOption( 'previewontop' ) ) { + + if ( $this->formtype == 'preview') { + $this->showPreview(); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); + } + + if ( $this->formtype == 'diff') { + $wgOut->addHTML( $this->getDiff() ); + } + + } + + wfProfileOut( $fname ); + } + + /** + * Append preview output to $wgOut. + * Includes category rendering if this is a category page. + * @private + */ + function showPreview() { + global $wgOut; + $wgOut->addHTML( '<div id="wikiPreview">' ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->openShowCategory(); + } + $previewOutput = $this->getPreviewText(); + $wgOut->addHTML( $previewOutput ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->closeShowCategory(); + } + $wgOut->addHTML( "<br style=\"clear:both;\" />\n" ); + $wgOut->addHTML( '</div>' ); + } + + /** + * Prepare a list of templates used by this page. Returns HTML. + */ + function formatTemplates() { + global $wgUser; + + $fname = 'EditPage::formatTemplates'; + wfProfileIn( $fname ); + + $sk =& $wgUser->getSkin(); + + $outText = ''; + $templates = $this->mArticle->getUsedTemplates(); + if ( count( $templates ) > 0 ) { + # Do a batch existence check + $batch = new LinkBatch; + foreach( $templates as $title ) { + $batch->addObj( $title ); + } + $batch->execute(); + + # Construct the HTML + $outText = '<br />'. wfMsgExt( 'templatesused', array( 'parseinline' ) ) . '<ul>'; + foreach ( $templates as $titleObj ) { + $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . '</li>'; + } + $outText .= '</ul>'; + } + wfProfileOut( $fname ); + return $outText; + } + + /** + * Live Preview lets us fetch rendered preview page content and + * add it to the page without refreshing the whole page. + * If not supported by the browser it will fall through to the normal form + * submission method. + * + * This function outputs a script tag to support live preview, and + * returns an onclick handler which should be added to the attributes + * of the preview button + */ + function doLivePreviewScript() { + global $wgStylePath, $wgJsMimeType, $wgOut, $wgTitle; + $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . + htmlspecialchars( $wgStylePath . '/common/preview.js' ) . + '"></script>' . "\n" ); + $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); + return "return !livePreview(" . + "getElementById('wikiPreview')," . + "editform.wpTextbox1.value," . + '"' . $liveAction . '"' . ")"; + } + + function getLastDelete() { + $dbr =& wfGetDB( DB_SLAVE ); + $fname = 'EditPage::getLastDelete'; + $res = $dbr->select( + array( 'logging', 'user' ), + array( 'log_type', + 'log_action', + 'log_timestamp', + 'log_user', + 'log_namespace', + 'log_title', + 'log_comment', + 'log_params', + 'user_name', ), + array( 'log_namespace' => $this->mTitle->getNamespace(), + 'log_title' => $this->mTitle->getDBkey(), + 'log_type' => 'delete', + 'log_action' => 'delete', + 'user_id=log_user' ), + $fname, + array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); + + if($dbr->numRows($res) == 1) { + while ( $x = $dbr->fetchObject ( $res ) ) + $data = $x; + $dbr->freeResult ( $res ) ; + } else { + $data = null; + } + return $data; + } + + /** + * @todo document + */ + function getPreviewText() { + global $wgOut, $wgUser, $wgTitle, $wgParser; + + $fname = 'EditPage::getPreviewText'; + wfProfileIn( $fname ); + + if ( $this->mTriedSave && !$this->mTokenOk ) { + $msg = 'session_fail_preview'; + } else { + $msg = 'previewnote'; + } + $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . + "<div class='previewnote'>" . $wgOut->parse( wfMsg( $msg ) ) . "</div>\n"; + if ( $this->isConflict ) { + $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + } + + $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions->setEditSection( false ); + + global $wgRawHtml; + if( $wgRawHtml && !$this->mTokenOk ) { + // Could be an offsite preview attempt. This is very unsafe if + // HTML is enabled, as it could be an attack. + return $wgOut->parse( "<div class='previewnote'>" . + wfMsg( 'session_fail_preview_html' ) . "</div>" ); + } + + # don't parse user css/js, show message about preview + # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here + + if ( $this->isCssJsSubpage ) { + if(preg_match("/\\.css$/", $wgTitle->getText() ) ) { + $previewtext = wfMsg('usercsspreview'); + } else if(preg_match("/\\.js$/", $wgTitle->getText() ) ) { + $previewtext = wfMsg('userjspreview'); + } + $parserOptions->setTidy(true); + $parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions ); + $wgOut->addHTML( $parserOutput->mText ); + wfProfileOut( $fname ); + return $previewhead; + } else { + # if user want to see preview when he edit an article + if( $wgUser->getOption('previewonfirst') and ($this->textbox1 == '')) { + $this->textbox1 = $this->getContent(); + } + + $toparse = $this->textbox1; + + # If we're adding a comment, we need to show the + # summary as the headline + if($this->section=="new" && $this->summary!="") { + $toparse="== {$this->summary} ==\n\n".$toparse; + } + + if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ; + $parserOptions->setTidy(true); + $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n", + $wgTitle, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $wgOut->addParserOutputNoText( $parserOutput ); + + wfProfileOut( $fname ); + return $previewhead . $previewHTML; + } + } + + /** + * Call the stock "user is blocked" page + */ + function blockedPage() { + global $wgOut, $wgUser; + $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' + + # If the user made changes, preserve them when showing the markup + # (This happens when a user is blocked during edit, for instance) + $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); + $source = $first ? $this->getContent() : $this->textbox1; + + # Spit out the source or the user's modified version + $rows = $wgUser->getOption( 'rows' ); + $cols = $wgUser->getOption( 'cols' ); + $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); + $wgOut->addHtml( '<hr />' ); + $wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) ); + $wgOut->addHtml( wfElement( 'textarea', $attribs, $source ) ); + } + + /** + * Produce the stock "please login to edit pages" page + */ + function userNotLoggedInPage() { + global $wgUser, $wgOut; + $skin = $wgUser->getSkin(); + + $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() ); + + $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); + $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); + } + + /** + * Creates a basic error page which informs the user that + * they have to validate their email address before being + * allowed to edit. + */ + function userNotConfirmedPage() { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'confirmedittitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addWikiText( wfMsg( 'confirmedittext' ) ); + $wgOut->returnToMain( false ); + } + + /** + * Produce the stock "your edit contains spam" page + * + * @param $match Text which triggered one or more filters + */ + function spamPage( $match = false ) { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); + if ( $match ) + $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); + + $wgOut->returnToMain( false ); + } + + /** + * @private + * @todo document + */ + function mergeChangesInto( &$editText ){ + $fname = 'EditPage::mergeChangesInto'; + wfProfileIn( $fname ); + + $db =& wfGetDB( DB_MASTER ); + + // This is the revision the editor started from + $baseRevision = Revision::loadFromTimestamp( + $db, $this->mArticle->mTitle, $this->edittime ); + if( is_null( $baseRevision ) ) { + wfProfileOut( $fname ); + return false; + } + $baseText = $baseRevision->getText(); + + // The current state, we want to merge updates into it + $currentRevision = Revision::loadFromTitle( + $db, $this->mArticle->mTitle ); + if( is_null( $currentRevision ) ) { + wfProfileOut( $fname ); + return false; + } + $currentText = $currentRevision->getText(); + + if( wfMerge( $baseText, $editText, $currentText, $result ) ){ + $editText = $result; + wfProfileOut( $fname ); + return true; + } else { + wfProfileOut( $fname ); + return false; + } + } + + /** + * Check if the browser is on a blacklist of user-agents known to + * mangle UTF-8 data on form submission. Returns true if Unicode + * should make it through, false if it's known to be a problem. + * @return bool + * @private + */ + function checkUnicodeCompliantBrowser() { + global $wgBrowserBlackList; + if( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { + // No User-Agent header sent? Trust it by default... + return true; + } + $currentbrowser = $_SERVER["HTTP_USER_AGENT"]; + foreach ( $wgBrowserBlackList as $browser ) { + if ( preg_match($browser, $currentbrowser) ) { + return false; + } + } + return true; + } + + /** + * Format an anchor fragment as it would appear for a given section name + * @param string $text + * @return string + * @private + */ + function sectionAnchor( $text ) { + $headline = Sanitizer::decodeCharReferences( $text ); + # strip out HTML + $headline = preg_replace( '/<.*?' . '>/', '', $headline ); + $headline = trim( $headline ); + $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); + $replacearray = array( + '%3A' => ':', + '%' => '.' + ); + return str_replace( + array_keys( $replacearray ), + array_values( $replacearray ), + $sectionanchor ); + } + + /** + * Shows a bulletin board style toolbar for common editing functions. + * It can be disabled in the user preferences. + * The necessary JavaScript code can be found in style/wikibits.js. + */ + function getEditToolbar() { + global $wgStylePath, $wgContLang, $wgJsMimeType; + + /** + * toolarray an array of arrays which each include the filename of + * the button image (without path), the opening tag, the closing tag, + * and optionally a sample text that is inserted between the two when no + * selection is highlighted. + * The tip text is shown when the user moves the mouse over the button. + * + * Already here are accesskeys (key), which are not used yet until someone + * can figure out a way to make them work in IE. However, we should make + * sure these keys are not defined on the edit page. + */ + $toolarray=array( + array( 'image'=>'button_bold.png', + 'open' => "\'\'\'", + 'close' => "\'\'\'", + 'sample'=> wfMsg('bold_sample'), + 'tip' => wfMsg('bold_tip'), + 'key' => 'B' + ), + array( 'image'=>'button_italic.png', + 'open' => "\'\'", + 'close' => "\'\'", + 'sample'=> wfMsg('italic_sample'), + 'tip' => wfMsg('italic_tip'), + 'key' => 'I' + ), + array( 'image'=>'button_link.png', + 'open' => '[[', + 'close' => ']]', + 'sample'=> wfMsg('link_sample'), + 'tip' => wfMsg('link_tip'), + 'key' => 'L' + ), + array( 'image'=>'button_extlink.png', + 'open' => '[', + 'close' => ']', + 'sample'=> wfMsg('extlink_sample'), + 'tip' => wfMsg('extlink_tip'), + 'key' => 'X' + ), + array( 'image'=>'button_headline.png', + 'open' => "\\n== ", + 'close' => " ==\\n", + 'sample'=> wfMsg('headline_sample'), + 'tip' => wfMsg('headline_tip'), + 'key' => 'H' + ), + array( 'image'=>'button_image.png', + 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", + 'close' => ']]', + 'sample'=> wfMsg('image_sample'), + 'tip' => wfMsg('image_tip'), + 'key' => 'D' + ), + array( 'image' =>'button_media.png', + 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'close' => ']]', + 'sample'=> wfMsg('media_sample'), + 'tip' => wfMsg('media_tip'), + 'key' => 'M' + ), + array( 'image' =>'button_math.png', + 'open' => "<math>", + 'close' => "<\\/math>", + 'sample'=> wfMsg('math_sample'), + 'tip' => wfMsg('math_tip'), + 'key' => 'C' + ), + array( 'image' =>'button_nowiki.png', + 'open' => "<nowiki>", + 'close' => "<\\/nowiki>", + 'sample'=> wfMsg('nowiki_sample'), + 'tip' => wfMsg('nowiki_tip'), + 'key' => 'N' + ), + array( 'image' =>'button_sig.png', + 'open' => '--~~~~', + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('sig_tip'), + 'key' => 'Y' + ), + array( 'image' =>'button_hr.png', + 'open' => "\\n----\\n", + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('hr_tip'), + 'key' => 'R' + ) + ); + $toolbar = "<div id='toolbar'>\n"; + $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; + + foreach($toolarray as $tool) { + + $image=$wgStylePath.'/common/images/'.$tool['image']; + $open=$tool['open']; + $close=$tool['close']; + $sample = wfEscapeJsString( $tool['sample'] ); + + // Note that we use the tip both for the ALT tag and the TITLE tag of the image. + // Older browsers show a "speedtip" type message only for ALT. + // Ideally these should be different, realistically they + // probably don't need to be. + $tip = wfEscapeJsString( $tool['tip'] ); + + #$key = $tool["key"]; + + $toolbar.="addButton('$image','$tip','$open','$close','$sample');\n"; + } + + $toolbar.="/*]]>*/\n</script>"; + $toolbar.="\n</div>"; + return $toolbar; + } + + /** + * Output preview text only. This can be sucked into the edit page + * via JavaScript, and saves the server time rendering the skin as + * well as theoretically being more robust on the client (doesn't + * disturb the edit box's undo history, won't eat your text on + * failure, etc). + * + * @todo This doesn't include category or interlanguage links. + * Would need to enhance it a bit, maybe wrap them in XML + * or something... that might also require more skin + * initialization, so check whether that's a problem. + */ + function livePreview() { + global $wgOut; + $wgOut->disable(); + header( 'Content-type: text/xml' ); + header( 'Cache-control: no-cache' ); + # FIXME + echo $this->getPreviewText( ); + /* To not shake screen up and down between preview and live-preview */ + echo "<br style=\"clear:both;\" />\n"; + } + + + /** + * Get a diff between the current contents of the edit box and the + * version of the page we're editing from. + * + * If this is a section edit, we'll replace the section as for final + * save and then make a comparison. + * + * @return string HTML + */ + function getDiff() { + require_once( 'DifferenceEngine.php' ); + $oldtext = $this->mArticle->fetchContent(); + $newtext = $this->mArticle->replaceSection( + $this->section, $this->textbox1, $this->summary, $this->edittime ); + $newtext = $this->mArticle->preSaveTransform( $newtext ); + $oldtitle = wfMsgExt( 'currentrev', array('parseinline') ); + $newtitle = wfMsgExt( 'yourtext', array('parseinline') ); + if ( $oldtext !== false || $newtext != '' ) { + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $oldtext, $newtext ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); + } else { + $difftext = ''; + } + + return '<div id="wikiDiff">' . $difftext . '</div>'; + } + + /** + * Filter an input field through a Unicode de-armoring process if it + * came from an old browser with known broken Unicode editing issues. + * + * @param WebRequest $request + * @param string $field + * @return string + * @private + */ + function safeUnicodeInput( $request, $field ) { + $text = rtrim( $request->getText( $field ) ); + return $request->getBool( 'safemode' ) + ? $this->unmakesafe( $text ) + : $text; + } + + /** + * Filter an output field through a Unicode armoring process if it is + * going to an old browser with known broken Unicode editing issues. + * + * @param string $text + * @return string + * @private + */ + function safeUnicodeOutput( $text ) { + global $wgContLang; + $codedText = $wgContLang->recodeForEdit( $text ); + return $this->checkUnicodeCompliantBrowser() + ? $codedText + : $this->makesafe( $codedText ); + } + + /** + * A number of web browsers are known to corrupt non-ASCII characters + * in a UTF-8 text editing environment. To protect against this, + * detected browsers will be served an armored version of the text, + * with non-ASCII chars converted to numeric HTML character references. + * + * Preexisting such character references will have a 0 added to them + * to ensure that round-trips do not alter the original data. + * + * @param string $invalue + * @return string + * @private + */ + function makesafe( $invalue ) { + // Armor existing references for reversability. + $invalue = strtr( $invalue, array( "&#x" => "�" ) ); + + $bytesleft = 0; + $result = ""; + $working = 0; + for( $i = 0; $i < strlen( $invalue ); $i++ ) { + $bytevalue = ord( $invalue{$i} ); + if( $bytevalue <= 0x7F ) { //0xxx xxxx + $result .= chr( $bytevalue ); + $bytesleft = 0; + } elseif( $bytevalue <= 0xBF ) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if( $bytesleft <= 0 ) { + $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; + } + } elseif( $bytevalue <= 0xDF ) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif( $bytevalue <= 0xEF ) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** + * Reverse the previously applied transliteration of non-ASCII characters + * back to UTF-8. Used to protect data from corruption by broken web browsers + * as listed in $wgBrowserBlackList. + * + * @param string $invalue + * @return string + * @private + */ + function unmakesafe( $invalue ) { + $result = ""; + for( $i = 0; $i < strlen( $invalue ); $i++ ) { + if( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) { + $i += 3; + $hexstring = ""; + do { + $hexstring .= $invalue{$i}; + $i++; + } while( ctype_xdigit( $invalue{$i} ) && ( $i < strlen( $invalue ) ) ); + + // Do some sanity checks. These aren't needed for reversability, + // but should help keep the breakage down if the editor + // breaks one of the entities whilst editing. + if ((substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6)) { + $codepoint = hexdec($hexstring); + $result .= codepointToUtf8( $codepoint ); + } else { + $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); + } + } else { + $result .= substr( $invalue, $i, 1 ); + } + } + // reverse the transform that we made for reversability reasons. + return strtr( $result, array( "�" => "&#x" ) ); + } + + function noCreatePermission() { + global $wgOut; + $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); + $wgOut->addWikiText( wfMsg( 'nocreatetext' ) ); + } + +} + +?> diff --git a/includes/Exception.php b/includes/Exception.php new file mode 100644 index 00000000..1e24515b --- /dev/null +++ b/includes/Exception.php @@ -0,0 +1,193 @@ +<?php + +class MWException extends Exception +{ + function useOutputPage() { + return !empty( $GLOBALS['wgFullyInitialised'] ); + } + + function useMessageCache() { + global $wgLang; + return is_object( $wgLang ); + } + + function msg( $key, $fallback /*[, params...] */ ) { + $args = array_slice( func_get_args(), 2 ); + if ( $this->useMessageCache() ) { + return wfMsgReal( $key, $args ); + } else { + return wfMsgReplaceArgs( $fallback, $args ); + } + } + + function getHTML() { + return '<p>' . htmlspecialchars( $this->getMessage() ) . + '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . + "</p>\n"; + } + + function getText() { + return $this->getMessage() . + "\nBacktrace:\n" . $this->getTraceAsString() . "\n"; + } + + function getPageTitle() { + if ( $this->useMessageCache() ) { + return wfMsg( 'internalerror' ); + } else { + global $wgSitename; + return "$wgSitename error"; + } + } + + function reportHTML() { + global $wgOut; + if ( $this->useOutputPage() ) { + $wgOut->setPageTitle( $this->getPageTitle() ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->redirect( '' ); + $wgOut->clearHTML(); + $wgOut->addHTML( $this->getHTML() ); + $wgOut->output(); + } else { + echo $this->htmlHeader(); + echo $this->getHTML(); + echo $this->htmlFooter(); + } + } + + function reportText() { + echo $this->getText(); + } + + function report() { + global $wgCommandLineMode; + if ( $wgCommandLineMode ) { + $this->reportText(); + } else { + $this->reportHTML(); + } + } + + function htmlHeader() { + global $wgLogo, $wgSitename, $wgOutputEncoding; + + if ( !headers_sent() ) { + header( 'HTTP/1.0 500 Internal Server Error' ); + header( 'Content-type: text/html; charset='.$wgOutputEncoding ); + /* Don't cache error pages! They cause no end of trouble... */ + header( 'Cache-control: none' ); + header( 'Pragma: nocache' ); + } + $title = $this->getPageTitle(); + echo "<html> + <head> + <title>$title</title> + </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 == "<{$subsource}>" ) { + # Try again with first char lower case + $subsource = strtolower ( substr ( $subsource , 0 , 1 ) ) . substr ( $subsource , 1 ) ; + $source = wfMsgForContentNoTrans( $subsource ) ; + } + if ( $source == "<{$subsource}>" ) { + # Didn't work either, return blank text + $source = "" ; + } + return $source ; +} + +/** + * Fetch a message string value, but don't replace any keys yet. + * @param string $key + * @param bool $useDB + * @param bool $forContent + * @return string + * @private + */ +function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { + global $wgParser, $wgMsgParserOptions, $wgContLang, $wgMessageCache, $wgLang; + + if ( is_object( $wgMessageCache ) ) + $transstat = $wgMessageCache->getTransform(); + + if( is_object( $wgMessageCache ) ) { + if ( ! $transform ) + $wgMessageCache->disableTransform(); + $message = $wgMessageCache->get( $key, $useDB, $forContent ); + } else { + if( $forContent ) { + $lang = &$wgContLang; + } else { + $lang = &$wgLang; + } + + wfSuppressWarnings(); + + if( is_object( $lang ) ) { + $message = $lang->getMessage( $key ); + } else { + $message = false; + } + wfRestoreWarnings(); + if($message === false) + $message = Language::getMessage($key); + if ( $transform && strstr( $message, '{{' ) !== false ) { + $message = $wgParser->transformMsg($message, $wgMsgParserOptions); + } + } + + if ( is_object( $wgMessageCache ) && ! $transform ) + $wgMessageCache->setTransform( $transstat ); + + return $message; +} + +/** + * Replace message parameter keys on the given formatted output. + * + * @param string $message + * @param array $args + * @return string + * @private + */ +function wfMsgReplaceArgs( $message, $args ) { + # Fix windows line-endings + # Some messages are split with explode("\n", $msg) + $message = str_replace( "\r", '', $message ); + + // Replace arguments + if ( count( $args ) ) { + if ( is_array( $args[0] ) ) { + foreach ( $args[0] as $key => $val ) { + $message = str_replace( '$' . $key, $val, $message ); + } + } else { + foreach( $args as $n => $param ) { + $replacementKeys['$' . ($n + 1)] = $param; + } + $message = strtr( $message, $replacementKeys ); + } + } + + return $message; +} + +/** + * Return an HTML-escaped version of a message. + * Parameter replacements, if any, are done *after* the HTML-escaping, + * so parameters may contain HTML (eg links or form controls). Be sure + * to pre-escape them if you really do want plaintext, or just wrap + * the whole thing in htmlspecialchars(). + * + * @param string $key + * @param string ... parameters + * @return string + */ +function wfMsgHtml( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key, true ) ), $args ); +} + +/** + * Return an HTML version of message + * Parameter replacements, if any, are done *after* parsing the wiki-text message, + * so parameters may contain HTML (eg links or form controls). Be sure + * to pre-escape them if you really do want plaintext, or just wrap + * the whole thing in htmlspecialchars(). + * + * @param string $key + * @param string ... parameters + * @return string + */ +function wfMsgWikiHtml( $key ) { + global $wgOut; + $args = func_get_args(); + array_shift( $args ); + return wfMsgReplaceArgs( $wgOut->parse( wfMsgGetKey( $key, true ), /* can't be set to false */ true ), $args ); +} + +/** + * Returns message in the requested format + * @param string $key Key of the message + * @param array $options Processing rules: + * <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( '[', '|', ''', 'ISBN ', '://' , "\n=", '{{' ), + htmlspecialchars($text) ); + return $text; +} + +/** + * @todo document + */ +function wfQuotedPrintable( $string, $charset = '' ) { + # Probably incomplete; see RFC 2045 + if( empty( $charset ) ) { + global $wgInputEncoding; + $charset = $wgInputEncoding; + } + $charset = strtoupper( $charset ); + $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ? + + $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff='; + $replace = $illegal . '\t ?_'; + if( !preg_match( "/[$illegal]/", $string ) ) return $string; + $out = "=?$charset?Q?"; + $out .= preg_replace( "/([$replace])/e", 'sprintf("=%02X",ord("$1"))', $string ); + $out .= '?='; + return $out; +} + + +/** + * @todo document + * @return float + */ +function wfTime() { + return microtime(true); +} + +/** + * Sets dest to source and returns the original value of dest + * If source is NULL, it just returns the value, it doesn't set the variable + */ +function wfSetVar( &$dest, $source ) { + $temp = $dest; + if ( !is_null( $source ) ) { + $dest = $source; + } + return $temp; +} + +/** + * As for wfSetVar except setting a bit + */ +function wfSetBit( &$dest, $bit, $state = true ) { + $temp = (bool)($dest & $bit ); + if ( !is_null( $state ) ) { + if ( $state ) { + $dest |= $bit; + } else { + $dest &= ~$bit; + } + } + return $temp; +} + +/** + * This function takes two arrays as input, and returns a CGI-style string, e.g. + * "days=7&limit=100". Options in the first array override options in the second. + * Options set to "" will not be output. + */ +function wfArrayToCGI( $array1, $array2 = NULL ) +{ + if ( !is_null( $array2 ) ) { + $array1 = $array1 + $array2; + } + + $cgi = ''; + foreach ( $array1 as $key => $value ) { + if ( '' !== $value ) { + if ( '' != $cgi ) { + $cgi .= '&'; + } + $cgi .= urlencode( $key ) . '=' . urlencode( $value ); + } + } + return $cgi; +} + +/** + * This is obsolete, use SquidUpdate::purge() + * @deprecated + */ +function wfPurgeSquidServers ($urlArr) { + SquidUpdate::purge( $urlArr ); +} + +/** + * Windows-compatible version of escapeshellarg() + * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() + * function puts single quotes in regardless of OS + */ +function wfEscapeShellArg( ) { + $args = func_get_args(); + $first = true; + $retVal = ''; + foreach ( $args as $arg ) { + if ( !$first ) { + $retVal .= ' '; + } else { + $first = false; + } + + if ( wfIsWindows() ) { + // Escaping for an MSVC-style command line parser + // Ref: http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // Double the backslashes before any double quotes. Escape the double quotes. + $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); + $arg = ''; + $delim = false; + foreach ( $tokens as $token ) { + if ( $delim ) { + $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; + } else { + $arg .= $token; + } + $delim = !$delim; + } + // Double the backslashes before the end of the string, because + // we will soon add a quote + if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { + $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); + } + + // Add surrounding quotes + $retVal .= '"' . $arg . '"'; + } else { + $retVal .= escapeshellarg( $arg ); + } + } + return $retVal; +} + +/** + * wfMerge attempts to merge differences between three texts. + * Returns true for a clean merge and false for failure or a conflict. + */ +function wfMerge( $old, $mine, $yours, &$result ){ + global $wgDiff3; + + # This check may also protect against code injection in + # case of broken installations. + if(! file_exists( $wgDiff3 ) ){ + wfDebug( "diff3 not found\n" ); + return false; + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); + $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + fwrite( $oldtextFile, $old ); fclose( $oldtextFile ); + fwrite( $mytextFile, $mine ); fclose( $mytextFile ); + fwrite( $yourtextFile, $yours ); fclose( $yourtextFile ); + + # Check for a conflict + $cmd = $wgDiff3 . ' -a --overlap-only ' . + wfEscapeShellArg( $mytextName ) . ' ' . + wfEscapeShellArg( $oldtextName ) . ' ' . + wfEscapeShellArg( $yourtextName ); + $handle = popen( $cmd, 'r' ); + + if( fgets( $handle, 1024 ) ){ + $conflict = true; + } else { + $conflict = false; + } + pclose( $handle ); + + # Merge differences + $cmd = $wgDiff3 . ' -a -e --merge ' . + wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName ); + $handle = popen( $cmd, 'r' ); + $result = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $result .= $data; + } while ( true ); + pclose( $handle ); + unlink( $mytextName ); unlink( $oldtextName ); unlink( $yourtextName ); + + if ( $result === '' && $old !== '' && $conflict == false ) { + wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); + $conflict = true; + } + return ! $conflict; +} + +/** + * @todo document + */ +function wfVarDump( $var ) { + global $wgOut; + $s = str_replace("\n","<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 == '<'. $name . ';>' || $notice == '-' ) { + wfProfileOut( $fname ); + return( false ); + } + + $cachedNotice = $parserMemc->get( $wgDBname . ':' . $name ); + if( is_array( $cachedNotice ) ) { + if( md5( $notice ) == $cachedNotice['hash'] ) { + $notice = $cachedNotice['html']; + } else { + $needParse = true; + } + } else { + $needParse = true; + } + + if( $needParse ) { + if( is_object( $wgOut ) ) { + $parsed = $wgOut->parse( $notice ); + $parserMemc->set( $wgDBname . ':' . $name, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); + $notice = $parsed; + } else { + wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); + $notice = ''; + } + } + + wfProfileOut( $fname ); + return $notice; +} + +function wfGetNamespaceNotice() { + global $wgTitle; + + # Paranoia + if ( !isset( $wgTitle ) || !is_object( $wgTitle ) ) + return ""; + + $fname = 'wfGetNamespaceNotice'; + wfProfileIn( $fname ); + + $key = "namespacenotice-" . $wgTitle->getNsText(); + $namespaceNotice = wfGetCachedNotice( $key ); + if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "<p><" ) { + $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 === "<$msg>"; +} + +/** + * Find out whether or not a mixed variable exists in a string + * + * @param mixed needle + * @param string haystack + * @return bool + */ +function in_string( $needle, $str ) { + return strpos( $str, $needle ) !== false; +} + +function wfSpecialList( $page, $details ) { + global $wgContLang; + $details = $details ? ' ' . $wgContLang->getDirMark() . "($details)" : ""; + return $page . $details; +} + +/** + * Returns a regular expression of url protocols + * + * @return string + */ +function wfUrlProtocols() { + global $wgUrlProtocols; + + // Support old-style $wgUrlProtocols strings, for backwards compatibility + // with LocalSettings files from 1.5 + if ( is_array( $wgUrlProtocols ) ) { + $protocols = array(); + foreach ($wgUrlProtocols as $protocol) + $protocols[] = preg_quote( $protocol, '/' ); + + return implode( '|', $protocols ); + } else { + return $wgUrlProtocols; + } +} + +/** + * Execute a shell command, with time and memory limits mirrored from the PHP + * configuration if supported. + * @param $cmd Command line, properly escaped for shell. + * @param &$retval optional, will receive the program's exit code. + * (non-zero is usually failure) + * @return collected stdout as a string (trailing newlines stripped) + */ +function wfShellExec( $cmd, &$retval=null ) { + global $IP, $wgMaxShellMemory; + + if( ini_get( 'safe_mode' ) ) { + wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + $retval = 1; + return "Unable to run external programs in safe mode."; + } + + if ( php_uname( 's' ) == 'Linux' ) { + $time = ini_get( 'max_execution_time' ); + $mem = intval( $wgMaxShellMemory ); + + if ( $time > 0 && $mem > 0 ) { + $script = "$IP/bin/ulimit.sh"; + if ( is_executable( $script ) ) { + $cmd = escapeshellarg( $script ) . " $time $mem $cmd"; + } + } + } elseif ( php_uname( 's' ) == 'Windows NT' ) { + # This is a hack to work around PHP's flawed invocation of cmd.exe + # http://news.php.net/php.internals/21796 + $cmd = '"' . $cmd . '"'; + } + wfDebug( "wfShellExec: $cmd\n" ); + + $output = array(); + $retval = 1; // error by default? + $lastline = exec( $cmd, $output, $retval ); + return implode( "\n", $output ); + +} + +/** + * This function works like "use VERSION" in Perl, the program will die with a + * backtrace if the current version of PHP is less than the version provided + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases, and might depend on other versions of PHP than the main code + * + * Note: PHP might die due to parsing errors in some cases before it ever + * manages to call this function, such is life + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUsePHP( $req_ver ) { + $php_ver = PHP_VERSION; + + if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) + throw new MWException( "PHP $req_ver required--this is only $php_ver" ); +} + +/** + * This function works like "use VERSION" in Perl except it checks the version + * of MediaWiki, the program will die with a backtrace if the current version + * of MediaWiki is less than the version provided. + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUseMW( $req_ver ) { + global $wgVersion; + + if ( version_compare( $wgVersion, (string)$req_ver, '<' ) ) + throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" ); +} + +/** + * Escape a string to make it suitable for inclusion in a preg_replace() + * replacement parameter. + * + * @param string $string + * @return string + */ +function wfRegexReplacement( $string ) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '$', '\\$', $string ); + return $string; +} + +/** + * Return the final portion of a pathname. + * Reimplemented because PHP5's basename() is buggy with multibyte text. + * http://bugs.php.net/bug.php?id=33898 + * + * PHP's basename() only considers '\' a pathchar on Windows and Netware. + * We'll consider it so always, as we don't want \s in our Unix paths either. + * + * @param string $path + * @return string + */ +function wfBaseName( $path ) { + if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) { + return $matches[1]; + } else { + return ''; + } +} + +/** + * Make a URL index, appropriate for the el_index field of externallinks. + */ +function wfMakeUrlIndex( $url ) { + wfSuppressWarnings(); + $bits = parse_url( $url ); + wfRestoreWarnings(); + if ( !$bits || $bits['scheme'] !== 'http' ) { + return false; + } + // Reverse the labels in the hostname, convert to lower case + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + // Add an extra dot to the end + if ( substr( $reversedHost, -1, 1 ) !== '.' ) { + $reversedHost .= '.'; + } + // Reconstruct the pseudo-URL + $index = "http://$reversedHost"; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) $index .= '?' . $bits['query']; + if ( isset( $bits['fragment'] ) ) $index .= '#' . $bits['fragment']; + return $index; +} + +/** + * Do any deferred updates and clear the list + * TODO: This could be in Wiki.php if that class made any sense at all + */ +function wfDoUpdates() +{ + global $wgPostCommitUpdateList, $wgDeferredUpdateList; + foreach ( $wgDeferredUpdateList as $update ) { + $update->doUpdate(); + } + foreach ( $wgPostCommitUpdateList as $update ) { + $update->doUpdate(); + } + $wgDeferredUpdateList = array(); + $wgPostCommitUpdateList = array(); +} + +/** + * More or less "markup-safe" explode() + * Ignores any instances of the separator inside <...> + * @param string $separator + * @param string $text + * @return array + */ +function wfExplodeMarkup( $separator, $text ) { + $placeholder = "\x00"; + + // Just in case... + $text = str_replace( $placeholder, '', $text ); + + // Trim stuff + $replacer = new ReplacerCallback( $separator, $placeholder ); + $cleaned = preg_replace_callback( '/(<.*?>)/', array( $replacer, 'go' ), $text ); + + $items = explode( $separator, $cleaned ); + foreach( $items as $i => $str ) { + $items[$i] = str_replace( $placeholder, $separator, $str ); + } + + return $items; +} + +class ReplacerCallback { + function ReplacerCallback( $from, $to ) { + $this->from = $from; + $this->to = $to; + } + + function go( $matches ) { + return str_replace( $this->from, $this->to, $matches[1] ); + } +} + + +/** + * Convert an arbitrarily-long digit string from one numeric base + * to another, optionally zero-padding to a minimum column width. + * + * Supports base 2 through 36; digit values 10-36 are represented + * as lowercase letters a-z. Input is case-insensitive. + * + * @param $input string of digits + * @param $sourceBase int 2-36 + * @param $destBase int 2-36 + * @param $pad int 1 or greater + * @return string or false on invalid input + */ +function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1 ) { + if( $sourceBase < 2 || + $sourceBase > 36 || + $destBase < 2 || + $destBase > 36 || + $pad < 1 || + $sourceBase != intval( $sourceBase ) || + $destBase != intval( $destBase ) || + $pad != intval( $pad ) || + !is_string( $input ) || + $input == '' ) { + return false; + } + + $digitChars = '0123456789abcdefghijklmnopqrstuvwxyz'; + $inDigits = array(); + $outChars = ''; + + // Decode and validate input string + $input = strtolower( $input ); + for( $i = 0; $i < strlen( $input ); $i++ ) { + $n = strpos( $digitChars, $input{$i} ); + if( $n === false || $n > $sourceBase ) { + return false; + } + $inDigits[] = $n; + } + + // Iterate over the input, modulo-ing out an output digit + // at a time until input is gone. + while( count( $inDigits ) ) { + $work = 0; + $workDigits = array(); + + // Long division... + foreach( $inDigits as $digit ) { + $work *= $sourceBase; + $work += $digit; + + if( $work < $destBase ) { + // Gonna need to pull another digit. + if( count( $workDigits ) ) { + // Avoid zero-padding; this lets us find + // the end of the input very easily when + // length drops to zero. + $workDigits[] = 0; + } + } else { + // Finally! Actual division! + $workDigits[] = intval( $work / $destBase ); + + // Isn't it annoying that most programming languages + // don't have a single divide-and-remainder operator, + // even though the CPU implements it that way? + $work = $work % $destBase; + } + } + + // All that division leaves us with a remainder, + // which is conveniently our next output digit. + $outChars .= $digitChars[$work]; + + // And we continue! + $inDigits = $workDigits; + } + + while( strlen( $outChars ) < $pad ) { + $outChars .= '0'; + } + + return strrev( $outChars ); +} + +/** + * Create an object with a given name and an array of construct parameters + * @param string $name + * @param array $p parameters + */ +function wfCreateObject( $name, $p ){ + $p = array_values( $p ); + switch ( count( $p ) ) { + case 0: + return new $name; + case 1: + return new $name( $p[0] ); + case 2: + return new $name( $p[0], $p[1] ); + case 3: + return new $name( $p[0], $p[1], $p[2] ); + case 4: + return new $name( $p[0], $p[1], $p[2], $p[3] ); + case 5: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4] ); + case 6: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4], $p[5] ); + default: + throw new MWException( "Too many arguments to construtor in wfCreateObject" ); + } +} + +/** + * Aliases for modularized functions + */ +function wfGetHTTP( $url, $timeout = 'default' ) { + return Http::get( $url, $timeout ); +} +function wfIsLocalURL( $url ) { + return Http::isLocalURL( $url ); +} + +?> diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php new file mode 100644 index 00000000..47703b20 --- /dev/null +++ b/includes/HTMLCacheUpdate.php @@ -0,0 +1,230 @@ +<?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( /*   */ "\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 = '§ion='.$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 = '§ion='.$section; + $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"'; + $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint ); + + if( $wgContLang->isRTL() ) { + $farside = 'left'; + $nearside = 'right'; + } else { + $farside = 'right'; + $nearside = 'left'; + } + return "<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 '<' . htmlspecialchars($key) . '>'; + } + # If cache initialization was deferred, start it now. + if( $this->mDeferred && !$this->mDisable && $useDB ) { + $this->load(); + } + + $message = false; + if( !$this->mDisable && $useDB ) { + $title = $lang->ucfirst( $key ); + if(!$isfullkey && ($langcode != $wgContLanguageCode) ) { + $title .= '/' . $langcode; + } + $message = $this->getFromCache( $title ); + } + # Try the extension array + if( $message === false && array_key_exists( $key, $this->mExtensionMessages ) ) { + if ( isset( $this->mExtensionMessages[$key][$langcode] ) ) { + $message = $this->mExtensionMessages[$key][$langcode]; + } elseif ( isset( $this->mExtensionMessages[$key]['en'] ) ) { + $message = $this->mExtensionMessages[$key]['en']; + } + } + + # Try the array in the language object + if( $message === false ) { + wfSuppressWarnings(); + $message = $lang->getMessage( $key ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } + + # Try the English array + if( $message === false && $langcode != 'en' ) { + wfSuppressWarnings(); + $message = Language::getMessage( $key ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } + + # Is this a custom message? Try the default language in the db... + if( ($message === false || $message === '-' ) && + !$this->mDisable && $useDB && + !$isfullkey && ($langcode != $wgContLanguageCode) ) { + $message = $this->getFromCache( $lang->ucfirst( $key ) ); + } + + # Final fallback + if( $message === false ) { + return '<' . htmlspecialchars($key) . '>'; + } + + # Replace brace tags + $message = $this->transform( $message ); + return $message; + } + + function getFromCache( $title ) { + $message = false; + + # Try the cache + if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) { + return $this->mCache[$title]; + } + + # Try individual message cache + if ( $this->mUseCache ) { + $message = $this->mMemc->get( $this->mMemcKey . ':' . $title ); + if ( $message == '###NONEXISTENT###' ) { + return false; + } elseif( !is_null( $message ) ) { + $this->mCache[$title] = $message; + return $message; + } else { + $message = false; + } + } + + # Call message Hooks, in case they are defined + wfRunHooks('MessagesPreLoad',array($title,&$message)); + + # If it wasn't in the cache, load each message from the DB individually + $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + if( $revision ) { + $message = $revision->getText(); + if ($this->mUseCache) { + $this->mCache[$title]=$message; + /* individual messages may be often + recached until proper purge code exists + */ + $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 ); + } + } else { + # Negative caching + # Use some special text instead of false, because false gets converted to '' somewhere + $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry ); + } + + return $message; + } + + function transform( $message ) { + global $wgParser; + if ( !$this->mParser && isset( $wgParser ) ) { + # Do some initialisation so that we don't have to do it twice + $wgParser->firstCallInit(); + # Clone it and store it + $this->mParser = clone $wgParser; + } + if ( !$this->mDisableTransform && $this->mParser ) { + if( strpos( $message, '{{' ) !== false ) { + $message = $this->mParser->transformMsg( $message, $this->mParserOptions ); + } + } + return $message; + } + + function disable() { $this->mDisable = true; } + function enable() { $this->mDisable = false; } + function disableTransform() { $this->mDisableTransform = true; } + function enableTransform() { $this->mDisableTransform = false; } + function setTransform( $x ) { $this->mDisableTransform = $x; } + function getTransform() { return $this->mDisableTransform; } + + /** + * Add a message to the cache + * + * @param mixed $key + * @param mixed $value + * @param string $lang The messages language, English by default + */ + function addMessage( $key, $value, $lang = 'en' ) { + $this->mExtensionMessages[$key][$lang] = $value; + } + + /** + * Add an associative array of message to the cache + * + * @param array $messages An associative array of key => values to be added + * @param string $lang The messages language, English by default + */ + function addMessages( $messages, $lang = 'en' ) { + wfProfileIn( __METHOD__ ); + foreach ( $messages as $key => $value ) { + $this->addMessage( $key, $value, $lang ); + } + wfProfileOut( __METHOD__ ); + } + + /** + * Clear all stored messages. Mainly used after a mass rebuild. + */ + function clear() { + if( $this->mUseCache ) { + $this->mMemc->delete( $this->mMemcKey ); + } + } +} +?> diff --git a/includes/Metadata.php b/includes/Metadata.php new file mode 100644 index 00000000..af40ab21 --- /dev/null +++ b/includes/Metadata.php @@ -0,0 +1,362 @@ +<?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 \\2', + # french spaces, Guillemet-right + '/(\\302\\253) /' => '\\1 ', + ); + $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); + + # only once and last + $text = $this->doBlockLevels( $text, $linestart ); + + $this->replaceLinkHolders( $text ); + + # the position of the parserConvert() call should not be changed. it + # assumes that the links are all replaced and the only thing left + # is the <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<div\\5>\\6</div>\\8\\9', + # remove empty italic or bold tag pairs, some + # introduced by rules above + '/<([bi])><\/\\1>/' => '' + ); + + $text = preg_replace( + array_keys( $tidyregs ), + array_values( $tidyregs ), + $text ); + } + + wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); + + $this->mOutput->setText( $text ); + wfProfileOut( $fname ); + + return $this->mOutput; + } + + /** + * Get a random string + * + * @private + * @static + */ + function getRandomString() { + return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff)); + } + + function &getTitle() { return $this->mTitle; } + function getOptions() { return $this->mOptions; } + + function getFunctionLang() { + global $wgLang, $wgContLang; + return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang; + } + + /** + * Replaces all occurrences of HTML-style comments and the given tags + * in the text with a random marker and returns teh next text. The output + * parameter $matches will be an associative array filled with data in + * the form: + * 'UNIQ-xxxxx' => array( + * 'element', + * 'tag content', + * array( 'param' => 'x' ), + * '<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 ö aren't valid in URLs anyway + # This was changed in August 2004 + $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; + + # Register link in the output object. + # Replace unnecessary URL escape codes with the referenced character + # This prevents spammers from hiding links from the filters + $pasteurized = Parser::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); + } + + wfProfileOut( $fname ); + return $s; + } + + /** + * Replace anything that looks like a URL with a link + * @private + */ + function replaceFreeExternalLinks( $text ) { + global $wgContLang; + $fname = 'Parser::replaceFreeExternalLinks'; + wfProfileIn( $fname ); + + $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + $s = array_shift( $bits ); + $i = 0; + + $sk =& $this->mOptions->getSkin(); + + while ( $i < count( $bits ) ){ + $protocol = $bits[$i++]; + $remainder = $bits[$i++]; + + if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { + # Found some characters after the protocol that look promising + $url = $protocol . $m[1]; + $trail = $m[2]; + + # special case: handle urls as url args: + # http://www.example.com/foo?=http://www.example.com/bar + if(strlen($trail) == 0 && + isset($bits[$i]) && + preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && + preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) + { + # add protocol, arg + $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link + $i += 2; + $trail = $m[2]; + } + + # The characters '<' and '>' (which were escaped by + # removeHTMLtags()) should not be included in + # URLs, per RFC 2396. + if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { + $trail = substr($url, $m2[0][1]) . $trail; + $url = substr($url, 0, $m2[0][1]); + } + + # Move trailing punctuation to $trail + $sep = ',;\.:!?'; + # If there is no left bracket, then consider right brackets fair game too + if ( strpos( $url, '(' ) === false ) { + $sep .= ')'; + } + + $numSepChars = strspn( strrev( $url ), $sep ); + if ( $numSepChars ) { + $trail = substr( $url, -$numSepChars ) . $trail; + $url = substr( $url, 0, -$numSepChars ); + } + + # Normalize any HTML entities in input. They will be + # re-escaped by makeExternalLink() or maybeMakeExternalImage() + $url = Sanitizer::decodeCharReferences( $url ); + + # Escape any control characters introduced by the above step + $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + + # Is this an external image? + $text = $this->maybeMakeExternalImage( $url ); + if ( $text === false ) { + # Not an image, make a link + $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() ); + # Register it in the output object... + # Replace unnecessary URL escape codes with their equivalent characters + $pasteurized = Parser::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); + } + $s .= $text . $trail; + } else { + $s .= $protocol . $remainder; + } + } + wfProfileOut( $fname ); + return $s; + } + + /** + * Replace unusual URL escape codes with their equivalent characters + * @param string + * @return string + * @static + * @fixme This can merge genuinely required bits in the path or query string, + * breaking legit URLs. A proper fix would treat the various parts of + * the URL differently; as a workaround, just use the output for + * statistical records, not for actual linking/output. + */ + function replaceUnusualEscapes( $url ) { + return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', + array( 'Parser', 'replaceUnusualEscapesCallback' ), $url ); + } + + /** + * Callback function used in replaceUnusualEscapes(). + * Replaces unusual URL escape codes with their equivalent character + * @static + * @private + */ + function replaceUnusualEscapesCallback( $matches ) { + $char = urldecode( $matches[0] ); + $ord = ord( $char ); + // Is it an unsafe or HTTP reserved character according to RFC 1738? + if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { + // No, shouldn't be escaped + return $char; + } else { + // Yes, leave it escaped + return $matches[0]; + } + } + + /** + * make an image if it's allowed, either through the global + * option or through the exception + * @private + */ + function maybeMakeExternalImage( $url ) { + $sk =& $this->mOptions->getSkin(); + $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); + $imagesexception = !empty($imagesfrom); + $text = false; + if ( $this->mOptions->getAllowExternalImages() + || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { + if ( preg_match( EXT_IMAGE_REGEX, $url ) ) { + # Image found + $text = $sk->makeExternalImage( htmlspecialchars( $url ) ); + } + } + return $text; + } + + /** + * Process [[ ]] wikilinks + * + * @private + */ + function replaceInternalLinks( $s ) { + global $wgContLang; + static $fname = 'Parser::replaceInternalLinks' ; + + wfProfileIn( $fname ); + + wfProfileIn( $fname.'-setup' ); + static $tc = FALSE; + # the % is needed to support urlencoded titles as well + if ( !$tc ) { $tc = Title::legalChars() . '#%'; } + + $sk =& $this->mOptions->getSkin(); + + #split the entire text string on occurences of [[ + $a = explode( '[[', ' ' . $s ); + #get the first element (all text up to first [[), and remove the space we added + $s = array_shift( $a ); + $s = substr( $s, 1 ); + + # Match a link having the form [[namespace:link|alternate]]trail + static $e1 = FALSE; + if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; } + # Match cases where there is no "]]", which might still be images + static $e1_img = FALSE; + if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; } + # Match the end of a line for a word that's not followed by whitespace, + # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched + $e2 = wfMsgForContent( 'linkprefix' ); + + $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); + + if( is_null( $this->mTitle ) ) { + throw new MWException( 'nooo' ); + } + $nottalk = !$this->mTitle->isTalkPage(); + + if ( $useLinkPrefixExtension ) { + if ( preg_match( $e2, $s, $m ) ) { + $first_prefix = $m[2]; + } else { + $first_prefix = false; + } + } else { + $prefix = ''; + } + + $selflink = $this->mTitle->getPrefixedText(); + wfProfileOut( $fname.'-setup' ); + + $checkVariantLink = sizeof($wgContLang->getVariants())>1; + $useSubpages = $this->areSubpagesAllowed(); + + # Loop for each link + for ($k = 0; isset( $a[$k] ); $k++) { + $line = $a[$k]; + if ( $useLinkPrefixExtension ) { + wfProfileIn( $fname.'-prefixhandling' ); + if ( preg_match( $e2, $s, $m ) ) { + $prefix = $m[2]; + $s = $m[1]; + } else { + $prefix=''; + } + # first link + if($first_prefix) { + $prefix = $first_prefix; + $first_prefix = false; + } + wfProfileOut( $fname.'-prefixhandling' ); + } + + $might_be_img = false; + + if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt + $text = $m[2]; + # If we get a ] at the beginning of $m[3] that means we have a link that's something like: + # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, + # the real problem is with the $e1 regex + # See bug 1300. + # + # Still some problems for cases where the ] is meant to be outside punctuation, + # and no image is in sight. See bug 2095. + # + if( $text !== '' && + preg_match( "/^\](.*)/s", $m[3], $n ) && + strpos($text, '[') !== false + ) + { + $text .= ']'; # so that replaceExternalLinks($text) works later + $m[3] = $n[1]; + } + # fix up urlencoded title texts + if(preg_match('/%/', $m[1] )) + # Should anchors '#' also be rejected? + $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + $trail = $m[3]; + } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption + $might_be_img = true; + $text = $m[2]; + if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]); + $trail = ""; + } else { # Invalid form; output directly + $s .= $prefix . '[[' . $line ; + continue; + } + + # Don't allow internal links to pages containing + # PROTO: where PROTO is a valid URL protocol; these + # should be external links. + if (preg_match('/^(\b(?:' . wfUrlProtocols() . '))/', $m[1])) { + $s .= $prefix . '[[' . $line ; + continue; + } + + # Make subpage if necessary + if( $useSubpages ) { + $link = $this->maybeDoSubpageLink( $m[1], $text ); + } else { + $link = $m[1]; + } + + $noforce = (substr($m[1], 0, 1) != ':'); + if (!$noforce) { + # Strip off leading ':' + $link = substr($link, 1); + } + + $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) ); + if( !$nt ) { + $s .= $prefix . '[[' . $line; + continue; + } + + #check other language variants of the link + #if the article does not exist + if( $checkVariantLink + && $nt->getArticleID() == 0 ) { + $wgContLang->findVariantLink($link, $nt); + } + + $ns = $nt->getNamespace(); + $iw = $nt->getInterWiki(); + + if ($might_be_img) { # if this is actually an invalid link + if ($ns == NS_IMAGE && $noforce) { #but might be an image + $found = false; + while (isset ($a[$k+1]) ) { + #look at the next 'line' to see if we can close it there + $spliced = array_splice( $a, $k + 1, 1 ); + $next_line = array_shift( $spliced ); + if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) { + # the first ]] closes the inner link, the second the image + $found = true; + $text .= '[[' . $m[1]; + $trail = $m[2]; + break; + } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) { + #if there's exactly one ]] that's fine, we'll keep looking + $text .= '[[' . $m[0]; + } else { + #if $next_line is invalid too, we need look no further + $text .= '[[' . $next_line; + break; + } + } + if ( !$found ) { + # we couldn't find the end of this imageLink, so output it raw + #but don't ignore what might be perfectly normal links in the text we've examined + $text = $this->replaceInternalLinks($text); + $s .= $prefix . '[[' . $link . '|' . $text; + # note: no $trail, because without an end, there *is* no trail + continue; + } + } else { #it's not an image, so output it raw + $s .= $prefix . '[[' . $link . '|' . $text; + # note: no $trail, because without an end, there *is* no trail + continue; + } + } + + $wasblank = ( '' == $text ); + if( $wasblank ) $text = $link; + + + # Link not escaped by : , create the various objects + if( $noforce ) { + + # Interwikis + if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { + $this->mOutput->addLanguageLink( $nt->getFullText() ); + $s = rtrim($s . "\n"); + $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + continue; + } + + if ( $ns == NS_IMAGE ) { + wfProfileIn( "$fname-image" ); + if ( !wfIsBadImage( $nt->getDBkey() ) ) { + # recursively parse links inside the image caption + # actually, this will parse them in any other parameters, too, + # but it might be hard to fix that, and it doesn't matter ATM + $text = $this->replaceExternalLinks($text); + $text = $this->replaceInternalLinks($text); + + # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them + $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); + + wfProfileOut( "$fname-image" ); + continue; + } else { + # We still need to record the image's presence on the page + $this->mOutput->addImage( $nt->getDBkey() ); + } + wfProfileOut( "$fname-image" ); + + } + + if ( $ns == NS_CATEGORY ) { + wfProfileIn( "$fname-category" ); + $s = rtrim($s . "\n"); # bug 87 + + if ( $wasblank ) { + if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { + $sortkey = $this->mTitle->getText(); + } else { + $sortkey = $this->mTitle->getPrefixedText(); + } + } else { + $sortkey = $text; + } + $sortkey = Sanitizer::decodeCharReferences( $sortkey ); + $sortkey = str_replace( "\n", '', $sortkey ); + $sortkey = $wgContLang->convertCategoryKey( $sortkey ); + $this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); + + /** + * Strip the whitespace Category links produce, see bug 87 + * @todo We might want to use trim($tmp, "\n") here. + */ + $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + + wfProfileOut( "$fname-category" ); + continue; + } + } + + if( ( $nt->getPrefixedText() === $selflink ) && + ( $nt->getFragment() === '' ) ) { + # Self-links are handled specially; generally de-link and change to bold. + $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); + continue; + } + + # Special and Media are pseudo-namespaces; no pages actually exist in them + if( $ns == NS_MEDIA ) { + $link = $sk->makeMediaLinkObj( $nt, $text ); + # Cloak with NOPARSE to avoid replacement in replaceExternalLinks + $s .= $prefix . $this->armorLinks( $link ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); + continue; + } elseif( $ns == NS_SPECIAL ) { + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + continue; + } elseif( $ns == NS_IMAGE ) { + $img = Image::newFromTitle( $nt ); + if( $img->exists() ) { + // Force a blue link if the file exists; may be a remote + // upload on the shared repository, and we want to see its + // auto-generated page. + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + continue; + } + } + $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); + } + wfProfileOut( $fname ); + return $s; + } + + /** + * Make a link placeholder. The text returned can be later resolved to a real link with + * replaceLinkHolders(). This is done for two reasons: firstly to avoid further + * parsing of interwiki links, and secondly to allow all extistence checks and + * article length checks (for stub links) to be bundled into a single query. + * + */ + function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + if ( ! is_object($nt) ) { + # Fail gracefully + $retVal = "<!-- 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 ( ", >, <) + * + * @param $in String: text that might contain HTML tags. + * @return string Escaped string + */ +function wfEscapeHTMLTagsOnly( $in ) { + return str_replace( + array( '"', '>', '<' ), + array( '"', '>', '<' ), + $in ); +} + +?> diff --git a/includes/ParserCache.php b/includes/ParserCache.php new file mode 100644 index 00000000..3ec7512f --- /dev/null +++ b/includes/ParserCache.php @@ -0,0 +1,127 @@ +<?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 .= '<'.$ext.'>'; + $ret .= $this->sub_makeXHTML($parser); + $ret .= '</'.$ext.'> '; + break; + + + # Table stuff + + case 'TABLE': + $ret .= $this->sub_makeXHTML($parser, 'table'); + break; + case 'TABLEROW': + $ret .= $this->sub_makeXHTML($parser, 'tr'); + break; + case 'TABLECELL': + $ret .= $this->sub_makeXHTML($parser, 'td'); + break; + case 'TABLEHEAD': + $ret .= $this->sub_makeXHTML($parser, 'th'); + break; + case 'CAPTION': + $ret .= $this->sub_makeXHTML($parser, 'caption'); + break; + case 'ATTRS': # SPECIAL CASE : returning attributes + return $this->getTheseAttrs(); + + + # Lists stuff + case 'LISTITEM': + if ($parser->mListType == 'dl') + $ret .= $this->sub_makeXHTML($parser, 'dd'); + else + $ret .= $this->sub_makeXHTML($parser, 'li'); + break; + case 'LIST': + $type = 'ol'; # Default + if ($this->attrs['TYPE'] == 'bullet') + $type = 'ul'; + else + if ($this->attrs['TYPE'] == 'indent') + $type = 'dl'; + $oldtype = $parser->mListType; + $parser->mListType = $type; + $ret .= $this->sub_makeXHTML($parser, $type); + $parser->mListType = $oldtype; + break; + + # Something else entirely + default: + $ret .= '<'.$n.'>'; + $ret .= $this->sub_makeXHTML($parser); + $ret .= '</'.$n.'> '; + } // switch($n) + + $ret = "\n{$ret}\n"; + $ret = str_replace("\n\n", "\n", $ret); + return $ret; + } + + /** + * A function for additional debugging output + */ + function myPrint() { + $ret = "<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 == '<' . $key . '>' ) + $text = ''; + $found = true; + } else { + // Get it from the DB + $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); + if ( $rev ) { + $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); + header( "Last-modified: $lastmod" ); + $text = $rev->getText(); + $found = true; + } + } + } + + # Bad title or page does not exist + if( !$found && $this->mContentType == 'text/x-wiki' ) { + # Don't return a 404 response for CSS or JavaScript; + # 404s aren't generally cached and it would create + # extra hits when user CSS/JS are on and the user doesn't + # have the pages. + header( "HTTP/1.0 404 Not Found" ); + } + + return $this->parseArticleText( $text ); + } + + function parseArticleText( $text ) { + if ( $text === '' ) + return ''; + else + if ( $this->mExpandTemplates ) { + global $wgTitle; + + $parser = new Parser(); + $parser->Options( new ParserOptions() ); // We don't want this to be user-specific + $parser->Title( $wgTitle ); + $parser->OutputType( OT_HTML ); + + return $parser->replaceVariables( $text ); + } else + return $text; + } +} +?> diff --git a/includes/RecentChange.php b/includes/RecentChange.php new file mode 100644 index 00000000..f320a47a --- /dev/null +++ b/includes/RecentChange.php @@ -0,0 +1,509 @@ +<?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( '>', '>', $rest ); + $close = ( $brace == '/>' ) ? ' /' : ''; + $text .= "<$slash$t$newparams$close>$rest"; + continue; + } + } + $text .= '<' . str_replace( '>', '>', $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( '>', '>', $rest ); + $text .= "<$slash$t$newparams$brace$rest"; + } else { + $text .= '<' . str_replace( '>', '>', $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" => ' ', + "\r" => ' ', + "\t" => '	', + ) ); + + 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( + '<' => '<', // This should never happen, + '>' => '>', // we've received invalid input + '"' => '"', // which should have been escaped. + '{' => '{', + '[' => '[', + "''" => '''', + 'ISBN' => 'ISBN', + 'RFC' => 'RFC', + 'PMID' => 'PMID', + '|' => '|', + '__' => '__', + ) ); + + # 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( ':', ':', $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( '"', '"', + 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 + * &-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 &foo;) + * + * @param string $name + * @return string + */ + function normalizeEntity( $name ) { + global $wgHtmlEntities; + if( isset( $wgHtmlEntities[$name] ) ) { + return "&$name;"; + } else { + return "&$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('<', '>', '"'), + $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 >, 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) . ' > '; + } + # 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}'> </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 .= '< '; + } + $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') . '" /> ' + . '<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§ion=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 == "<$message>" ) { + 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§ion=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 ('<'.$msgKey.'>' != $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'] == '<'.$key.'>'){ + $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> </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() ? '←' : '→'; + + 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() ? '←' : '→'; + + 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> </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 ← + $arr = $wgContLang->isRTL() ? ' ← ' : ' → '; + + # 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> </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> </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( + ' ', + '<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 is, getToggle() needs + # to be changed to out return its output in two parts. -ævar + $this->addRow( + ' ', + $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 .= " " ; + $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("< ".$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> </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×tamp=$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> </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 != '<loginlanguagelinks>' ) { + $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] = "<{$tags[$i]}>"; + $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() ? '‏' : '‎' ); + 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('< '.$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 é ā or 〗 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 == "<$key>" ) { + 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 == "<$key>" ) { + 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 ) .
+ ' ' .
+ 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 ) .
+ ' ' .
+ 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 ) .
+ ' ' .
+ 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') ?>" /> <?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 @@ +一隻 +三隻 +四隻 +五隻 +六隻 +七隻 +八隻 +九隻 +十隻 +百隻 +千隻 +萬隻 +億隻 +並存著 +乾絲 +乾著急 +体育鍛鍊 +借著 +偷雞不著 +几絲 +划著 +划著走 +別著 +刮著 +千絲萬縷 +參与 +參与者 +參合 +參考價值 +參與 +參與人員 +參與制 +參與感 +參與者 +參觀團 +參觀團體 +參閱 +吃著不盡 +合著 +合著者 +吊帶褲 +吊掛著 +吊著 +吊褲 +吊褲帶 +向著 +嚴絲合縫 +回絲 +回著 +塗著 +壟斷價格 +壟斷資產 +壟斷集團 +姜絲 +帶團參加 +干著急 +幾絲 +彆著 +怎麼著 +憑藉著 +接著說 +擔著 +擔負著 +敘說著 +斗轉參橫 +旋繞著 +板著臉 +標志著 +正當著 +沈著 +沖著 +派團參加 +涂著 +湊合著 +瀰漫著 +為著 +煙斗絲 +率團參加 +畫著 +當著 +發著 +直接參与 +睡著了 +秋褲 +積极參与 +積极參加 +簽著 +系著 +絕對參照 +絲來線去 +絲布 +絲板 +絲瓜布 +絲絨布 +絲線 +絲織廠 +絲蟲 +緊繃著 +繃著 +繃著臉 +繃著臉兒 +繫著 +罵著 +肉絲麵 +背向著 +菌絲体 +菌絲體 +著兒 +著書立說 +著色軟體 +著重指出 +著錄 +著錄規則 +薑絲 +藉著 +蘊含著 +蘊涵著 +衝著 +被覆著 +覆著 +覆蓋著 +訴說著 +說著 +請參閱 +謝絕參觀 +豎著 +豐濱 +豐濱鄉 +象徵著 +這么著 +這麼著 +那麼著 +配合著 +醞釀著 +錄著 +鍛鍊出 +鍛鍊身体 +關係著 +雞絲 +雞絲麵 +面朝著 +面臨著 +顯著標志 +颳著 +髮絲 +鬥著 +鬧著玩儿 +鬧著玩兒 +鯰魚 |