diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2006-10-11 18:12:39 +0000 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2006-10-11 18:12:39 +0000 |
commit | 183851b06bd6c52f3cae5375f433da720d410447 (patch) | |
tree | a477257decbf3360127f6739c2f9d0ec57a03d39 /includes |
MediaWiki 1.7.1 wiederhergestellt
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 ) { |